init
This commit is contained in:
commit
bd0f83f425
6
content.go
Normal file
6
content.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed *.tmpl
|
||||||
|
var content embed.FS
|
118
data.go
Normal file
118
data.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
160
issues.go
Normal file
160
issues.go
Normal 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
45
list.tmpl
Normal 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
57
main.go
Normal 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
1
projects.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package main
|
Loading…
Reference in New Issue
Block a user