Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add planning board to triage party!! #269

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Use the drop-down labelled `Solo` on the top-right of any page to enable multi-p

NOTE: Multi-player works best if the "Resolution" field of each rule has a clear action to resolve the item and remove it from the list.

## Kanban mode (NEW)
## Kanban mode

![kanban mode](docs/images/kanban.png)

Expand All @@ -141,6 +141,28 @@ Best practices for designing a useful Kanban dashboard:
* If a collection should be displayed in Kanban form by default, specify `display: kanban` in its configuration.
* For velocity measurements and time estimate support, create a rule named `__velocity__` containing recently closed issues to include. See the example configuration.

## Planning Board mode (NEW)
![planning mode](docs/images/planning.png)

In v1.5.0, pages can now be displayed as a SCRUM dashboard.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe Scrum is the correct capitalization

The columns are based on the feature or Objective(O) the issue belongs to. Use rules to define features.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about 'objective' instead of 'Objective(O)'?

The rows are swim-lanes based on the target milestones.
To see a real-world example:

* [skaffold planning dashboard](http://tinyurl.com/skaffold-planning)
* [skaffold planning config](https://github.com/google/triage-party/blob/master/config/examples/skaffold.yaml#L142)

In the above example, skaffold has 2 Objectives and 2 features defined
The two objectives are
1) Simply onbaording and make inner devloop faster
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove these two lines

2) Extensibility

Best practices for designing a useful Planning dashboard:

* Rules should be designed and ordered in a way that represents progress: `Not started` -> `Started` -> `Under Review` -> `Completed`
* Rules work best when they are mutually excusive (no issue matches multiple rules)
* If a collection should be displayed in Planning form by default, specify `display: planning` in its configuration.

## Data freshness

![age screenshot](docs/images/age.png)
Expand Down
1 change: 1 addition & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ func main() {
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(filepath.Join(findPath(*siteDir), "static")))))
http.HandleFunc("/s/", s.Collection())
http.HandleFunc("/k/", s.Kanban())
http.HandleFunc("/p/", s.Planning())
http.HandleFunc("/healthz", s.Healthz())
http.HandleFunc("/threadz", s.Threadz())

Expand Down
72 changes: 72 additions & 0 deletions config/examples/skaffold.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,25 @@ collections:
- milestone-pr-needs-merge
- milestone-recently-closed

- id: planning
name: Planning
description: >
Skafold Team Planning Board.

This board is used by Skaffold team to plan upcoming releases.
The timeline and priority of issues listed here are subject to change.

Please add label planning/Q4-21 for an issue to appear on this board.

display: planning
rules:
- onboarding
- extensibility
- partner-requests
- skaffold-v2
- docs
- all-other-requests

- id: similar
name: Similar
description: Items which appear similar to one other. Review for duplicates or vague titles.
Expand Down Expand Up @@ -183,6 +202,7 @@ rules:
filters:
- tag: "!commented"
- tag: "recv"
- label: "!source/partnerships"
- created: +7d

issue-p0-followup-slo:
Expand Down Expand Up @@ -276,6 +296,7 @@ rules:
filters:
- tag: "!commented"
- tag: "recv"
- label: "!source/partnerships"
- created: -5d

# Issues that may need reprioritized
Expand Down Expand Up @@ -680,6 +701,57 @@ rules:
- tag: open-milestone
- state: closed
- updated: -21d
## Planing
onboarding:
name: >
Simple Onboarding, Faster dev.
Use Label - area/onboarding
type: issue
filters:
- label: "planning/Q4-21"
- label: "area/onboarding"

extensibility:
name: >
Extensibility - Deploy and Lifecyle.
Use Label area/extensibility
type: issue
filters:
- label: "planning/Q4-21"
- label: "area/extensibility"

skaffold-v2:
name: >
Skaffold V2 pipeline.
Use Label area/V2
type: issue
filters:
- label: "planning/Q4-21"
- label: "area/extensibility"

partner-requests:
name: >
Other partner requests.
Use Label source/partnerships
type: issue
filters:
- label: "planning/Q4-21"
- label: "source/partnerships"

docs:
name: >
Skaffold Docs.
Use label area/docs
type: issue
filters:
- label: "planning/Q4-21"
- label: "area/docs"

all-other-requests:
name: "Others"
type: issue
filters:
- label: "planning/Q4-21"

# Fix-It
fixit-prs:
Expand Down
2 changes: 1 addition & 1 deletion pkg/hubbub/analyze.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2021 Google Inc.
// Copyright 2020 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down
9 changes: 6 additions & 3 deletions pkg/site/kanban.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,12 @@ var unassigned = "zz_unassigned"

// Swimlane is a row in a Kanban display.
type Swimlane struct {
User *provider.User
Columns []*triage.RuleResult
Issues int
User *provider.User
Name string
Description string
URL string
Columns []*triage.RuleResult
Issues int
}

func avatarWide(u *provider.User) template.HTML {
Expand Down
166 changes: 166 additions & 0 deletions pkg/site/planning.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright 2020 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package site

import (
"fmt"
"html/template"
"net/http"
"path/filepath"
"sort"
"strings"
"time"

"github.com/google/triage-party/pkg/hubbub"
"github.com/google/triage-party/pkg/provider"
"github.com/google/triage-party/pkg/triage"
"k8s.io/klog/v2"
)

const (
unplanned = "Unplanned"
)

// Planning shows a view of a collection.
tejal29 marked this conversation as resolved.
Show resolved Hide resolved
// Planning board can be used planning Sprints and futures releases
// categorized in swimlanes defining a feature or Objective.
// The planning board gives a view of all planned milestones with issues
// versus the current or selected milestone in the Kanban Board.
// It also highlights all new incoming issues not assigned to a milestone.

func (h *Handlers) Planning() http.HandlerFunc {
fmap := template.FuncMap{
"toJS": toJS,
"toYAML": toYAML,
"toJSfunc": toJSfunc,
"toDays": toDays,
"HumanDuration": humanDuration,
"UnixNano": unixNano,
"getAssignees": getAssignees,
"unAssignedOrAvatar": unAssignedOrAvatar,
"Class": className,
"isUnplanned": isUnplanned,
}

t := template.Must(template.New("planning").Funcs(fmap).ParseFiles(
filepath.Join(h.baseDir, "planning.tmpl"),
filepath.Join(h.baseDir, "base.tmpl"),
))

return func(w http.ResponseWriter, r *http.Request) {
klog.Infof("GET %s: %v", r.URL.Path, r.Header)

id := strings.TrimPrefix(r.URL.Path, "/p/")

p, err := h.collectionPage(r.Context(), id, isRefresh(r))
if err != nil {
http.Error(w, fmt.Sprintf("planning page for %q: %v", id, err), 500)
klog.Errorf("page: %v", err)

return
}

if p.CollectionResult.RuleResults != nil {
p.Description = p.Collection.Description
p.Swimlanes = groupByState(p.CollectionResult.RuleResults)
}

err = t.ExecuteTemplate(w, "base", p)

if err != nil {
klog.Errorf("tmpl: %v", err)
return
}
}
}

// unAssignedOrAvatar is used in a sticky note and hence
// wrapping "unAssigned
func unAssignedOrAvatar(u *provider.User) template.HTML {
if u.GetLogin() == unassigned {
return `🤷`
}
return avatar(u)
}

func getAssignees(co *hubbub.Conversation) []*provider.User {
if co.Assignees == nil || len(co.Assignees) == 0 {
return []*provider.User{{Login: &unassigned}}
}
return co.Assignees
}

func groupByState(results []*triage.RuleResult) []*Swimlane {
milestones := []time.Time{}
milestoneDueOnMap := map[time.Time]string{}
lanes := map[string]*Swimlane{
unplanned: {
Name: unplanned,
Columns: make([]*triage.RuleResult, len(results)),
},
}
seen := map[int]struct{}{}
for i, r := range results {
for _, co := range r.Items {
if _, ok := seen[co.ID]; ok {
continue
}
seen[co.ID] = struct{}{}
var state string
if co.Milestone == nil {
state = unplanned
} else {
state = *co.Milestone.Title
}
if lanes[state] == nil {
ts := co.Milestone.GetDueOn()
milestones = append(milestones, ts)
milestoneDueOnMap[ts] = state
lanes[state] = &Swimlane{
Name: state,
Description: fmt.Sprintf("Due on %s-%d (%d/%d) open",
ts.Month().String(), ts.Day(), co.Milestone.GetOpenIssues(),
co.Milestone.GetOpenIssues()+co.Milestone.GetClosedIssues()),
URL: *co.Milestone.HTMLURL,
Columns: make([]*triage.RuleResult, len(results)),
}
}
if lanes[state].Columns[i] == nil {
lanes[state].Columns[i] = &triage.RuleResult{
Rule: r.Rule,
Items: []*hubbub.Conversation{},
}
}
lanes[state].Columns[i].Items = append(lanes[state].Columns[i].Items, co)
lanes[state].Issues++
}
}

sl := []*Swimlane{lanes[unplanned]}

// sort lanes as per due date.
sort.Slice(milestones, func(i, j int) bool {
return milestones[i].Before(milestones[j])
})

for _, k := range milestones {
sl = append(sl, lanes[milestoneDueOnMap[k]])
}
return sl
}

func isUnplanned(name string) bool {
return name == unplanned
}
1 change: 1 addition & 0 deletions site/base.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
{{- range .Collections }}
{{- if not .Hidden }}
{{ if eq .Display "kanban" }}<a class="navbar-item {{ if eq $.ID .ID }}is-active{{ else }}is-inactive{{ end }}" href="/k/{{ .ID }}">{{ .Name }}</a>
{{ else if eq .Display "planning" }}<a class="navbar-item {{ if eq $.ID .ID }}is-active{{ else }}is-inactive{{ end }}" href="/p/{{ .ID }}{{ $.GetVars }}">{{ .Name }}</a>
{{ else }}<a class="navbar-item {{ if eq $.ID .ID }}is-active{{ else }}is-inactive{{ end }}" href="/s/{{ .ID }}{{ $.GetVars }}">{{ .Name }}</a>{{ end }}
{{ end }}
{{ end }}
Expand Down
10 changes: 7 additions & 3 deletions site/collection.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
Avg age: {{ .CollectionResult.AvgAge | toDays }},
Avg wait: {{ .CollectionResult.AvgCurrentHold | toDays }}
</span>

<span class="alt-view"><a href="/k/{{ .ID }}{{ $.GetVars }}">Kanban</a></span>

{{ if eq .ID "planning" }}
<span class="alt-view"><a href="/p/{{ .ID }}{{ $.GetVars }}">Planning</a></span>
{{ else }}
<span class="alt-view"><a href="/k/{{ .ID }}{{ $.GetVars }}">Planning</a></span>
{{ end }}
</div>
<script>
function openAllTabs() {
Expand Down Expand Up @@ -64,6 +66,8 @@
{{ if ne .Description "" }}
<div class="box description">
<pre>{{ .Description }}</pre>
</div><div class="box description">
<pre>{{ .Description }}</pre>
</div>
{{ end }}

Expand Down
Loading