From bd0f83f4257c5e53af32d6ffe0935785fcde3acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96zg=C3=BCr=20Kesim?= Date: Wed, 27 Dec 2023 03:06:11 +0100 Subject: [PATCH] init --- content.go | 6 ++ data.go | 118 ++++++++++++++++++++++++++++++++++++++ go.mod | 3 + issues.go | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++++ list.tmpl | 45 +++++++++++++++ main.go | 57 +++++++++++++++++++ projects.go | 1 + 7 files changed, 390 insertions(+) create mode 100644 content.go create mode 100644 data.go create mode 100644 go.mod create mode 100644 issues.go create mode 100644 list.tmpl create mode 100644 main.go create mode 100644 projects.go diff --git a/content.go b/content.go new file mode 100644 index 0000000..89d576a --- /dev/null +++ b/content.go @@ -0,0 +1,6 @@ +package main + +import "embed" + +//go:embed *.tmpl +var content embed.FS diff --git a/data.go b/data.go new file mode 100644 index 0000000..30bf4f6 --- /dev/null +++ b/data.go @@ -0,0 +1,118 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "html/template" + "io" + "log" + "net/http" + "strings" + "sync" + "time" +) + +type Data struct { + mux sync.RWMutex + url string + token string + num int + projectId int + filterId int + minimumVersion string + tmpl *template.Template + ctx context.Context + + Issues Issues + Timestamp time.Time + Lasterror error +} + +func NewData(ctx context.Context, url, token string, num int) *Data { + data := &Data{ + url: url, + token: token, + ctx: ctx, + num: num, + } + fm := map[string]any{ + "shorten": func(max int, s string) string { + if len(s) <= max { + return s + } + return s[:max] + "⋯" + }, + } + data.tmpl = template.Must(template.New("index").Funcs(fm).ParseFS(content, "*.tmpl")) + + return data +} + +func (d *Data) update() { + url := fmt.Sprintf("%s?project_id=%dfilter_id=%d&page_size=%d", + d.url, d.projectId, d.filterId, d.num) + req, e := http.NewRequestWithContext(d.ctx, "GET", url, nil) + if nil != e { + d.mux.Lock() + defer d.mux.Unlock() + d.Lasterror = e + return + } + req.Header.Add("Authorization", d.token) + + r, e := http.DefaultClient.Do(req) + if nil != e { + d.mux.Lock() + defer d.mux.Unlock() + d.Lasterror = e + return + } else if 200 != r.StatusCode { + d.mux.Lock() + defer d.mux.Unlock() + d.Lasterror = fmt.Errorf("Got unexpected status %s\n", r.Status) + return + } + + iss := struct{ Issues Issues }{} + e = json.NewDecoder(r.Body).Decode(&iss) + + d.mux.Lock() + defer d.mux.Unlock() + d.Lasterror = e + if nil != e { + return + } + d.Timestamp = time.Now() + + // Filter issues with old target versions out + var issues = Issues{} + for _, issue := range iss.Issues { + if strings.Compare(d.minimumVersion, issue.TargetVersion.Name) < 0 { + issues = append(issues, issue) + } + } + d.Issues = issues +} + +func (d *Data) printJSON(w io.Writer) { + d.mux.RLock() + defer d.mux.RUnlock() + + if nil == d.Issues { + fmt.Fprintln(w, "{}") + return + } + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + enc.Encode(d.Issues) +} + +func (d *Data) printTemplate(w io.Writer, name string) { + d.mux.RLock() + defer d.mux.RUnlock() + e := d.tmpl.ExecuteTemplate(w, name, d) + if nil != e { + log.Println(e) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c2b29fc --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module taler.net/dashboard + +go 1.21.5 diff --git a/issues.go b/issues.go new file mode 100644 index 0000000..c6e8148 --- /dev/null +++ b/issues.go @@ -0,0 +1,160 @@ +package main + +import ( + "sort" + "strings" + "time" +) + +type Issues []Issue + +type Issue struct { + Id uint32 + Summary string + Description string + Project KeyVal + Category KeyVal + TargetVersion KeyVal `json:"target_version"` + Reporter KeyVal + handler KeyVal + Status KeyVal + Resolution KeyVal + ViewState KeyVal `json:"view_state"` + Priority KeyVal + Severity KeyVal + Reproducibility KeyVal + Sticky bool + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Notes []Note + Relationships []Relationship + Tags []KeyVal + History []Change +} + +type KeyVal struct { + Id uint32 + Name string + Label string `json:",omitempty"` + Color string `json:",omitempty"` +} + +type Note struct { + Id uint32 + Text string + Reporter KeyVal + ViewState KeyVal `json:"view_state"` + Attachments []Attachment + Typ string `json:"type"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"created_at"` +} + +type Attachment struct { + Name string + Content []byte +} + +type Relationship struct { + Id uint32 + Typ KeyVal `json:"type"` + Issue struct { + Id uint32 + Summary string + Status KeyVal + Resolution KeyVal + Handler KeyVal + } +} + +type Change struct { + CreatedAt time.Time `json:"created_at"` + Message string + User KeyVal + Typ KeyVal `json:"type"` + Note struct{ id int32 } +} + +type ByCategory []Issue + +func (b ByCategory) Len() int { return len(b) } +func (b ByCategory) Less(i, j int) bool { + return strings.Compare(b[i].Category.Name, b[j].Category.Name) < 0 +} +func (b ByCategory) Swap(i, j int) { b[i], b[j] = b[j], b[i] } + +type ById []Issue + +func (b ById) Len() int { return len(b) } +func (b ById) Less(i, j int) bool { return b[i].Id < b[j].Id } +func (b ById) Swap(i, j int) { b[i], b[j] = b[j], b[i] } + +type ByTarget []Issue + +func (b ByTarget) Len() int { return len(b) } +func (b ByTarget) Less(i, j int) bool { + return strings.Compare(b[i].TargetVersion.Name, b[j].TargetVersion.Name) < 0 +} +func (b ByTarget) Swap(i, j int) { b[i], b[j] = b[j], b[i] } + +func (i Issues) Tags() (tags []string) { + var m = map[string]bool{} + for _, issue := range i { + for _, tag := range issue.Tags { + m[tag.Name] = true + } + } + for tag := range m { + tags = append(tags, tag) + } + sort.Strings(tags) + return +} + +func (i Issues) TargetVersions() (targets []string) { + var m = map[string]bool{} + for _, issue := range i { + m[issue.TargetVersion.Name] = true + } + for t := range m { + targets = append(targets, t) + } + sort.Strings(targets) + return +} + +func (i Issues) Categories() []string { + var m = map[string]bool{} + + for _, issue := range i { + m[issue.Category.Name] = true + } + + var r = []string{} + for c := range m { + r = append(r, c) + } + sort.Strings(r) + return r +} + +func (i Issues) ByCategory(cat string) (issues Issues) { + for _, issue := range i { + if issue.Category.Name == cat { + issues = append(issues, issue) + } + } + sort.Sort(ById(issues)) + return issues +} + +func (i Issues) ByCategoryAndTarget(cat, tar string) (issues Issues) { + for _, is := range i { + if is.TargetVersion.Name == tar && + is.Category.Name == cat { + issues = append(issues, is) + } + } + sort.Sort(ById(issues)) + return +} diff --git a/list.tmpl b/list.tmpl new file mode 100644 index 0000000..1a529bb --- /dev/null +++ b/list.tmpl @@ -0,0 +1,45 @@ + + GNU Taler Dashboard + + +

GNU Taler Dashboard

+ Table view +

List View

+ Data from {{ .Timestamp.Format "02 Jan 06 15:04 MST"}} + {{ with .Lasterror }}, Last error: {{ . }} {{end}} + + +

+ {{ $issues := .Issues }} + {{ range $issues.Tags }} + + {{ end }} +

+ + {{ range $cat := $issues.Categories }} +

{{ . }}

+ {{ range $issues.ByCategory $cat }} +
+ {{.Id}} {{.Summary}} +
{{ .Description }}
+
+ {{ end }} + {{ end }} + end of dashboard + + diff --git a/main.go b/main.go new file mode 100644 index 0000000..f93cbda --- /dev/null +++ b/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "net/http" + "os" + "time" +) + +var ( + fl_url = flag.String("url", "https://bugs.gnunet.org/api/rest/issues", "URL to the issues") + fl_token = flag.String("token", os.Getenv("MANTIS_API_TOKEN"), "API-Token") + fl_port = flag.String("port", ":8080", "[ip]:port to serve") + fl_num = flag.Int("num", 100, "number of issues to retrieve at once") + fl_min = flag.String("min", "0.9.3", "minimum version for data") +) + +func main() { + flag.Parse() + fmt.Println("starting") + + var ctx = context.Background() + var data = NewData(ctx, *fl_url, *fl_token, *fl_num) + data.filterId = 230 + data.projectId = 23 + data.minimumVersion = *fl_min + + data.update() + go func() { + var ticker = time.NewTicker(15 * time.Second) + for range ticker.C { + select { + case <-ctx.Done(): + return + default: + fmt.Println("updating data") + data.update() + } + } + }() + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + data.printTemplate(w, "table.tmpl") + }) + http.HandleFunc("/list", func(w http.ResponseWriter, r *http.Request) { + data.printTemplate(w, "list.tmpl") + }) + http.HandleFunc("/json", func(w http.ResponseWriter, r *http.Request) { + data.printJSON(w) + }) + fmt.Println("ready to serve") + + log.Fatal(http.ListenAndServe(*fl_port, nil)) +} diff --git a/projects.go b/projects.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/projects.go @@ -0,0 +1 @@ +package main