2023-12-27 03:06:11 +01:00
|
|
|
package main
|
|
|
|
|
2023-12-27 03:46:20 +01:00
|
|
|
/*
|
|
|
|
This file is part of taler-dashboard
|
|
|
|
Copyright (C) 2023 Özgür Kesim
|
|
|
|
|
|
|
|
taler-dashboard is free software; you can redistribute it and/or modify it
|
|
|
|
under the terms of the GNU Affero General Public License as published by the
|
|
|
|
Free Software Foundation; either version 3, or (at your option) any later
|
|
|
|
version.
|
|
|
|
|
|
|
|
taler-dashboard is distributed in the hope that it will be useful, but
|
|
|
|
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
|
|
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
|
|
|
License for more details.
|
|
|
|
|
|
|
|
You can receive a copy of the GNU Affero General Public License from
|
|
|
|
<http://www.gnu.org/licenses/>
|
|
|
|
|
|
|
|
@author Özgür Kesim <oec-taler@kesim.org>
|
|
|
|
*/
|
|
|
|
|
2023-12-27 03:06:11 +01:00
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"html/template"
|
|
|
|
"io"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
2023-12-27 13:53:36 +01:00
|
|
|
"runtime/debug"
|
|
|
|
"sort"
|
2023-12-27 03:06:11 +01:00
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Data struct {
|
|
|
|
mux sync.RWMutex
|
|
|
|
url string
|
|
|
|
token string
|
|
|
|
num int
|
|
|
|
projectId int
|
|
|
|
minimumVersion string
|
|
|
|
tmpl *template.Template
|
|
|
|
ctx context.Context
|
|
|
|
|
|
|
|
Issues Issues
|
2023-12-27 13:53:36 +01:00
|
|
|
Features Issues
|
2023-12-27 14:51:01 +01:00
|
|
|
Project Project
|
2023-12-27 03:06:11 +01:00
|
|
|
Timestamp time.Time
|
2023-12-27 04:18:58 +01:00
|
|
|
Freq time.Duration
|
2023-12-27 03:06:11 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-12-27 14:51:01 +01:00
|
|
|
func (d *Data) getProject() {
|
|
|
|
url := fmt.Sprintf("%s/projects/%d", d.url, d.projectId)
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
var project Project
|
|
|
|
p := struct{ Projects []Project }{}
|
|
|
|
e = json.NewDecoder(r.Body).Decode(&p)
|
|
|
|
|
|
|
|
if len(p.Projects) < 1 {
|
|
|
|
e = fmt.Errorf("no project found with id", d.projectId)
|
|
|
|
} else {
|
|
|
|
// filter out obsolete versions
|
|
|
|
project = p.Projects[0]
|
|
|
|
var versions Versions
|
|
|
|
for _, v := range project.Versions {
|
|
|
|
v := v
|
|
|
|
if !v.Obsolete && !v.Released {
|
|
|
|
versions = append(versions, v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
project.Versions = versions
|
|
|
|
}
|
|
|
|
|
|
|
|
d.mux.Lock()
|
|
|
|
defer d.mux.Unlock()
|
|
|
|
d.Lasterror = e
|
|
|
|
if nil != e {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
d.Timestamp = time.Now()
|
|
|
|
d.Project = project
|
|
|
|
|
2023-12-27 16:45:39 +01:00
|
|
|
log.Println("Got project details for id", d.projectId, "with", len(d.Project.Versions), "version entries")
|
2023-12-27 14:51:01 +01:00
|
|
|
}
|
|
|
|
|
2023-12-27 13:20:51 +01:00
|
|
|
const statusFilter = `status%5B%5D=10&status%5B%5D=20&status%5B%5D=30&status%5B%5D=40&status%5B%5D=50&severity%5B%5D=20`
|
|
|
|
|
|
|
|
var fields = []string{"id",
|
|
|
|
"description",
|
|
|
|
"summary",
|
|
|
|
"category",
|
|
|
|
"target_version",
|
|
|
|
"status",
|
|
|
|
"reporter",
|
|
|
|
"handler",
|
|
|
|
"resolution",
|
|
|
|
"priority",
|
|
|
|
"severity",
|
|
|
|
"created_at",
|
|
|
|
"updated_at",
|
|
|
|
"relationships",
|
|
|
|
"tags",
|
|
|
|
}
|
|
|
|
|
2023-12-27 14:51:01 +01:00
|
|
|
func (d *Data) getIssues() {
|
2023-12-27 13:20:51 +01:00
|
|
|
url := fmt.Sprintf("%s/issues?project_id=%d&page_size=%d&%s&select=%s",
|
|
|
|
d.url, d.projectId, d.num, statusFilter,
|
|
|
|
strings.Join(fields, ","))
|
2023-12-27 03:06:11 +01:00
|
|
|
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{}
|
2023-12-27 13:53:36 +01:00
|
|
|
var features = Issues{}
|
2023-12-27 14:51:01 +01:00
|
|
|
var open = 0
|
2023-12-27 03:06:11 +01:00
|
|
|
for _, issue := range iss.Issues {
|
2023-12-27 04:39:49 +01:00
|
|
|
if issue.Resolution.Name == "open" &&
|
|
|
|
strings.Compare(d.minimumVersion, issue.TargetVersion.Name) < 0 {
|
2023-12-27 14:51:01 +01:00
|
|
|
open++
|
2023-12-27 13:53:36 +01:00
|
|
|
if issue.Severity.Name == "feature" {
|
|
|
|
features = append(features, issue)
|
|
|
|
} else {
|
|
|
|
issues = append(issues, issue)
|
|
|
|
}
|
2023-12-27 03:06:11 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
d.Issues = issues
|
2023-12-27 13:53:36 +01:00
|
|
|
d.Features = features
|
2023-12-27 16:45:39 +01:00
|
|
|
log.Println("Got", len(iss.Issues), "entries, of which", open, "are open and relevant:", len(features), "features and", len(issues), "issues")
|
2023-12-27 13:20:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Data) Loop() {
|
2023-12-27 14:51:01 +01:00
|
|
|
d.getProject()
|
|
|
|
d.getIssues()
|
2023-12-27 13:20:51 +01:00
|
|
|
go func() {
|
|
|
|
var ticker = time.NewTicker(d.Freq)
|
|
|
|
for range ticker.C {
|
|
|
|
select {
|
|
|
|
case <-d.ctx.Done():
|
|
|
|
return
|
|
|
|
default:
|
2023-12-27 16:45:39 +01:00
|
|
|
log.Println("Updating data")
|
2023-12-27 14:51:01 +01:00
|
|
|
d.getProject()
|
|
|
|
d.getIssues()
|
2023-12-27 13:20:51 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
2023-12-27 03:06:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
2023-12-27 13:53:36 +01:00
|
|
|
|
|
|
|
func (d *Data) TargetVersions() (tv []string) {
|
|
|
|
d.mux.RLock()
|
|
|
|
defer d.mux.RUnlock()
|
|
|
|
|
|
|
|
var m = map[string]bool{}
|
|
|
|
for _, s := range d.Issues.TargetVersions() {
|
|
|
|
m[s] = true
|
|
|
|
}
|
|
|
|
for _, s := range d.Features.TargetVersions() {
|
|
|
|
m[s] = true
|
|
|
|
}
|
|
|
|
for s := range m {
|
|
|
|
tv = append(tv, s)
|
|
|
|
}
|
|
|
|
sort.Strings(tv)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-12-27 14:51:01 +01:00
|
|
|
func (d *Data) VersionsByDate() VersionsByDate {
|
|
|
|
return VersionsByDate(d.Project.Versions)
|
|
|
|
}
|
|
|
|
|
2023-12-27 13:53:36 +01:00
|
|
|
func (d *Data) Categories() (cs []string) {
|
|
|
|
d.mux.RLock()
|
|
|
|
defer d.mux.RUnlock()
|
|
|
|
|
|
|
|
var m = map[string]bool{}
|
|
|
|
for _, s := range d.Issues.Categories() {
|
|
|
|
m[s] = true
|
|
|
|
}
|
|
|
|
for _, s := range d.Features.Categories() {
|
|
|
|
m[s] = true
|
|
|
|
}
|
|
|
|
for s := range m {
|
|
|
|
cs = append(cs, s)
|
|
|
|
}
|
|
|
|
sort.Strings(cs)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Data) Tags() (ts []string) {
|
|
|
|
d.mux.RLock()
|
|
|
|
defer d.mux.RUnlock()
|
|
|
|
|
|
|
|
var m = map[string]bool{}
|
|
|
|
for _, s := range d.Issues.Tags() {
|
|
|
|
m[s] = true
|
|
|
|
}
|
|
|
|
for _, s := range d.Features.Tags() {
|
|
|
|
m[s] = true
|
|
|
|
}
|
|
|
|
for s := range m {
|
|
|
|
ts = append(ts, s)
|
|
|
|
}
|
|
|
|
sort.Strings(ts)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Data) Commit() string {
|
|
|
|
var version, timestamp = "<unknown>", "<unknown>"
|
|
|
|
if info, ok := debug.ReadBuildInfo(); ok {
|
|
|
|
for _, setting := range info.Settings {
|
|
|
|
switch setting.Key {
|
|
|
|
case "vcs.revision":
|
|
|
|
n := len(setting.Value)
|
|
|
|
if n > 8 {
|
|
|
|
n = 8
|
|
|
|
}
|
|
|
|
version = setting.Value[:n]
|
|
|
|
case "vcs.time":
|
|
|
|
timestamp = setting.Value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return version + " from " + timestamp
|
|
|
|
}
|