devapp: add burndown chart to /release page

Using Chart.js and embedded JSON rendered within the template, the
total number of issues and the number of release blockers in the
current development milestone (hard-coded right now) are graphed in
a line chart.

Change-Id: Icaefc1ff46f976f857f22f07f2934ad32aaf9547
Reviewed-on: https://go-review.googlesource.com/c/150639
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
This commit is contained in:
Andrew Bonventre 2018-11-20 20:04:09 -05:00
Родитель ac5b419c29
Коммит 2b0e3d6570
3 изменённых файлов: 146 добавлений и 19 удалений

Просмотреть файл

@ -6,6 +6,7 @@ package main
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"log"
@ -24,8 +25,14 @@ const (
prefixProposal = "proposal:"
prefixDev = "[dev."
// The title of the current release milestone in GitHub.
curMilestoneTitle = "Go1.12"
)
// The start date of the current release milestone.
var curMilestoneStart = time.Date(2018, 8, 20, 0, 0, 0, 0, time.UTC)
// titleDirs returns a slice of prefix directories contained in a title. For
// devapp,maintner: my cool new change, it will return ["devapp", "maintner"].
// If there is no dir prefix, it will return nil.
@ -61,8 +68,9 @@ func titleDirs(title string) []string {
}
type releaseData struct {
LastUpdated string
Sections []section
LastUpdated string
Sections []section
BurndownJSON template.JS
// dirty is set if this data needs to be updated due to a corpus change.
dirty bool
@ -160,6 +168,19 @@ func (cl *gerritCL) ReviewURL() string {
return fmt.Sprintf("https://%s-review.googlesource.com/%d", subd, cl.Number)
}
// burndownData is encoded to JSON and embedded in the page for use when
// rendering a burndown chart using JavaScript.
type burndownData struct {
Milestone string `json:"milestone"`
Entries []burndownEntry `json:"entries"`
}
type burndownEntry struct {
DateStr string `json:"dateStr"` // "12-25"
Open int `json:"open"`
Blockers int `json:"blockers"`
}
func (s *server) updateReleaseData() {
log.Println("Updating release data ...")
s.cMu.Lock()
@ -217,21 +238,54 @@ func (s *server) updateReleaseData() {
})
dirToIssues := map[string][]*maintner.GitHubIssue{}
var curMilestoneIssues []*maintner.GitHubIssue
s.repo.ForeachIssue(func(issue *maintner.GitHubIssue) error {
// Issues in active milestones.
if !issue.Closed && issue.Milestone != nil && !issue.Milestone.Closed {
dirs := titleDirs(issue.Title)
if len(dirs) == 0 {
dirToIssues[""] = append(dirToIssues[""], issue)
} else {
for _, d := range dirs {
dirToIssues[d] = append(dirToIssues[d], issue)
}
// Only include issues in active milestones.
if issue.Milestone.IsUnknown() || issue.Milestone.Closed || issue.Milestone.IsNone() {
return nil
}
if issue.Milestone.Title == curMilestoneTitle {
curMilestoneIssues = append(curMilestoneIssues, issue)
}
// Only open issues are displayed on the page using dirToIssues.
if issue.Closed {
return nil
}
dirs := titleDirs(issue.Title)
if len(dirs) == 0 {
dirToIssues[""] = append(dirToIssues[""], issue)
} else {
for _, d := range dirs {
dirToIssues[d] = append(dirToIssues[d], issue)
}
}
return nil
})
bd := burndownData{Milestone: curMilestoneTitle}
for t, now := curMilestoneStart, time.Now(); t.Before(now); t = t.Add(24 * time.Hour) {
var e burndownEntry
for _, issue := range curMilestoneIssues {
if issue.Created.After(t) || (issue.Closed && issue.ClosedAt.Before(t)) {
continue
}
if issue.HasLabel("release-blocker") {
e.Blockers++
}
e.Open++
}
e.DateStr = t.Format("01-02")
bd.Entries = append(bd.Entries, e)
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(bd); err != nil {
log.Printf("json.Encode: %v", err)
}
s.data.release.BurndownJSON = template.JS(buf.String())
s.data.release.Sections = nil
s.appendOpenIssues(dirToIssues, issueToCLs)
s.appendPendingCLs(dirToCLs)

10
devapp/static/js/Chart.min.js поставляемый Normal file

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -22,7 +22,14 @@ a:visited {
margin: .5em 0 1em;
}
.Header {
display: flex;
flex-wrap: wrap;
font-weight: bold;
justify-content: space-between;
}
.BurndownChart {
height: 250px;
width: 500px;
}
.Section {
border-top: 1px solid #aaa;
@ -50,15 +57,20 @@ a:visited {
}
</style>
<header class="Header">
<div>Release dashboard</div>
<div>{{.LastUpdated}}</div>
<div class="Header-left">
<div>Release dashboard</div>
<div>{{.LastUpdated}}</div>
<ul class="CountSummary">
{{range .Sections}}
<li><a href="#{{.Title}}">{{.Count}} {{.Title}}</a></li>
{{end}}
</ul>
</div>
<div class="BurndownChart">
<canvas class="js-burndownChart"></canvas>
</div>
</header>
<main>
<ul class="CountSummary">
{{range .Sections}}
<li><a href="#{{.Title}}">{{.Count}} {{.Title}}</a></li>
{{end}}
</ul>
{{range .Sections}}
<section class="Section">
<h3 class="Section-title" id="{{.Title}}">{{.Title}}</h3>
@ -87,4 +99,55 @@ a:visited {
{{end}}
</section>
{{end}}
</main>
</main>
<script src="/js/Chart.min.js"></script>
<script>
const data = {{.BurndownJSON}}
new Chart(document.querySelector('.js-burndownChart'), {
type: 'line',
data: {
labels: data.entries.map(d => d.dateStr),
datasets: [
{
label: `${data.milestone} total issues`,
data: data.entries.map(d => d.open),
backgroundColor: 'transparent',
borderColor: 'rgba(54, 162, 235, 0.8)',
pointRadius: 0,
},
{
label: `${data.milestone} release blocking`,
data: data.entries.map(d => d.blockers),
backgroundColor: 'rgba(0,0,0,0)',
borderColor: 'rgba(255, 99, 132, 0.8)',
pointRadius: 0,
},
],
},
options: {
animation: {
duration: 0,
},
title: {
display: true,
text: `${data.milestone} Issues`,
position: 'left',
},
legend: {
display: false,
},
tooltips: {
intersect: false,
},
scales: {
yAxes: [
{
ticks: {
beginAtZero: true,
},
},
],
},
},
});
</script>