This commit is contained in:
Özgür Kesim 2023-12-27 03:06:11 +01:00
commit bd0f83f425
Signed by: oec
GPG Key ID: F136A7F922D018D7
7 changed files with 390 additions and 0 deletions

6
content.go Normal file
View File

@ -0,0 +1,6 @@
package main
import "embed"
//go:embed *.tmpl
var content embed.FS

118
data.go Normal file
View File

@ -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)
}
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module taler.net/dashboard
go 1.21.5

160
issues.go Normal file
View File

@ -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
}

45
list.tmpl Normal file
View File

@ -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>

57
main.go Normal file
View File

@ -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))
}

1
projects.go Normal file
View File

@ -0,0 +1 @@
package main