aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorÖzgür Kesim <oec@codeblau.de>2023-12-27 03:06:11 +0100
committerÖzgür Kesim <oec@codeblau.de>2023-12-27 03:06:11 +0100
commitbd0f83f4257c5e53af32d6ffe0935785fcde3acf (patch)
tree6b968cd10d60629081041ec1895b4c5564f9390c
init
-rw-r--r--content.go6
-rw-r--r--data.go118
-rw-r--r--go.mod3
-rw-r--r--issues.go160
-rw-r--r--list.tmpl45
-rw-r--r--main.go57
-rw-r--r--projects.go1
7 files changed, 390 insertions, 0 deletions
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 @@
+<html>
+ <head><title>GNU Taler Dashboard</title></head>
+<style>
+body {
+ margin-left:15%;
+ margin-right:15%;
+ font-family:sans-serif;
+}
+h3 {
+ margin-left: -10%;
+ color: brown;
+}
+pre {
+ max-width: 100%;
+ overflow: scroll;
+ text-overflow: wrap,ellipsis;
+}
+</style>
+ <body>
+ <h1>GNU Taler Dashboard</h1>
+ <a href="/">Table view</a>
+ <h2>List View</h2>
+ Data from {{ .Timestamp.Format "02 Jan 06 15:04 MST"}}
+ {{ with .Lasterror }}, Last error: {{ . }} {{end}}
+
+
+ <p>
+ {{ $issues := .Issues }}
+ {{ range $issues.Tags }}
+ <button>{{ . }}</button>
+ {{ end }}
+ </p>
+
+ {{ range $cat := $issues.Categories }}
+ <h3>{{ . }}</h3>
+ {{ range $issues.ByCategory $cat }}
+ <details>
+ <summary><a href="https://bugs.gnunet.org/view.php?id={{.Id}}" target="_blank">{{.Id}}</a> {{.Summary}}</summary>
+ <pre>{{ .Description }}</pre>
+ </details>
+ {{ end }}
+ {{ end }}
+ <i>end of dashboard</i>
+ </body>
+</html>
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