mirror of https://github.com/golang/go.git
300 lines
7.0 KiB
Go
300 lines
7.0 KiB
Go
// Copyright 2012 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package dashboard
|
|
|
|
// This file handles the front page.
|
|
|
|
import (
|
|
"bytes"
|
|
"html/template"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"appengine"
|
|
"appengine/datastore"
|
|
"appengine/user"
|
|
)
|
|
|
|
func init() {
|
|
http.HandleFunc("/", handleFront)
|
|
http.HandleFunc("/favicon.ico", http.NotFound)
|
|
}
|
|
|
|
// maximum number of active CLs to show in person-specific tables.
|
|
const maxCLs = 100
|
|
|
|
func handleFront(w http.ResponseWriter, r *http.Request) {
|
|
c := appengine.NewContext(r)
|
|
|
|
data := &frontPageData{
|
|
Reviewers: personList,
|
|
User: user.Current(c).Email,
|
|
IsAdmin: user.IsAdmin(c),
|
|
}
|
|
var currentPerson string
|
|
u := data.User
|
|
you := "you"
|
|
if e := r.FormValue("user"); e != "" {
|
|
u = e
|
|
you = e
|
|
}
|
|
currentPerson, data.UserIsReviewer = emailToPerson[u]
|
|
if !data.UserIsReviewer {
|
|
currentPerson = u
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
errc := make(chan error, 10)
|
|
activeCLs := datastore.NewQuery("CL").
|
|
Filter("Closed =", false).
|
|
Order("-Modified")
|
|
|
|
tableFetch := func(index int, f func(tbl *clTable) error) {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
start := time.Now()
|
|
if err := f(&data.Tables[index]); err != nil {
|
|
errc <- err
|
|
}
|
|
data.Timing[index] = time.Now().Sub(start)
|
|
}()
|
|
}
|
|
|
|
data.Tables[0].Title = "CLs assigned to " + you + " for review"
|
|
if data.UserIsReviewer {
|
|
tableFetch(0, func(tbl *clTable) error {
|
|
q := activeCLs.Filter("Reviewer =", currentPerson).Limit(maxCLs)
|
|
tbl.Assignable = true
|
|
_, err := q.GetAll(c, &tbl.CLs)
|
|
return err
|
|
})
|
|
}
|
|
|
|
tableFetch(1, func(tbl *clTable) error {
|
|
q := activeCLs
|
|
if data.UserIsReviewer {
|
|
q = q.Filter("Author =", currentPerson)
|
|
} else {
|
|
q = q.Filter("Owner =", currentPerson)
|
|
}
|
|
q = q.Limit(maxCLs)
|
|
tbl.Title = "CLs sent by " + you
|
|
tbl.Assignable = true
|
|
_, err := q.GetAll(c, &tbl.CLs)
|
|
return err
|
|
})
|
|
|
|
tableFetch(2, func(tbl *clTable) error {
|
|
q := activeCLs.Limit(50)
|
|
tbl.Title = "Other active CLs"
|
|
tbl.Assignable = true
|
|
if _, err := q.GetAll(c, &tbl.CLs); err != nil {
|
|
return err
|
|
}
|
|
// filter
|
|
for i := len(tbl.CLs) - 1; i >= 0; i-- {
|
|
cl := tbl.CLs[i]
|
|
if cl.Owner == currentPerson || cl.Author == currentPerson || cl.Reviewer == currentPerson {
|
|
// Preserve order.
|
|
copy(tbl.CLs[i:], tbl.CLs[i+1:])
|
|
tbl.CLs = tbl.CLs[:len(tbl.CLs)-1]
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
tableFetch(3, func(tbl *clTable) error {
|
|
q := datastore.NewQuery("CL").
|
|
Filter("Closed =", true).
|
|
Order("-Modified").
|
|
Limit(10)
|
|
tbl.Title = "Recently closed CLs"
|
|
tbl.Assignable = false
|
|
_, err := q.GetAll(c, &tbl.CLs)
|
|
return err
|
|
})
|
|
|
|
// Not really a table fetch.
|
|
tableFetch(0, func(_ *clTable) error {
|
|
var err error
|
|
data.LogoutURL, err = user.LogoutURL(c, "/")
|
|
return err
|
|
})
|
|
|
|
wg.Wait()
|
|
|
|
select {
|
|
case err := <-errc:
|
|
c.Errorf("%v", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
default:
|
|
}
|
|
|
|
var b bytes.Buffer
|
|
if err := frontPage.ExecuteTemplate(&b, "front", &data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
io.Copy(w, &b)
|
|
}
|
|
|
|
type frontPageData struct {
|
|
Tables [4]clTable
|
|
Timing [4]time.Duration
|
|
|
|
Reviewers []string
|
|
UserIsReviewer bool
|
|
|
|
User, LogoutURL string // actual logged in user
|
|
IsAdmin bool
|
|
}
|
|
|
|
type clTable struct {
|
|
Title string
|
|
Assignable bool
|
|
CLs []*CL
|
|
}
|
|
|
|
var frontPage = template.Must(template.New("front").Funcs(template.FuncMap{
|
|
"selected": func(a, b string) string {
|
|
if a == b {
|
|
return "selected"
|
|
}
|
|
return ""
|
|
},
|
|
"shortemail": func(s string) string {
|
|
if i := strings.Index(s, "@"); i >= 0 {
|
|
s = s[:i]
|
|
}
|
|
return s
|
|
},
|
|
}).Parse(`
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<title>Go code reviews</title>
|
|
<link rel="icon" type="image/png" href="/static/icon.png" />
|
|
<style type="text/css">
|
|
body {
|
|
font-family: Helvetica, sans-serif;
|
|
}
|
|
img#gopherstamp {
|
|
float: right;
|
|
height: auto;
|
|
width: 250px;
|
|
}
|
|
h1, h2, h3 {
|
|
color: #777;
|
|
margin-bottom: 0;
|
|
}
|
|
table {
|
|
border-spacing: 0;
|
|
}
|
|
td {
|
|
vertical-align: top;
|
|
padding: 2px 5px;
|
|
}
|
|
tr.unreplied td.email {
|
|
border-left: 2px solid blue;
|
|
}
|
|
tr.pending td {
|
|
background: #fc8;
|
|
}
|
|
tr.failed td {
|
|
background: #f88;
|
|
}
|
|
tr.saved td {
|
|
background: #8f8;
|
|
}
|
|
.cls {
|
|
margin-top: 0;
|
|
}
|
|
a {
|
|
color: blue;
|
|
text-decoration: none; /* no link underline */
|
|
}
|
|
address {
|
|
font-size: 10px;
|
|
text-align: right;
|
|
}
|
|
.email {
|
|
font-family: monospace;
|
|
}
|
|
</style>
|
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
|
|
</head>
|
|
<body>
|
|
|
|
<img id="gopherstamp" src="/static/gopherstamp.jpg" />
|
|
<h1>Go code reviews</h1>
|
|
|
|
<table class="cls">
|
|
{{range $i, $tbl := .Tables}}
|
|
<tr><td colspan="5"><h3>{{$tbl.Title}}</h3></td></tr>
|
|
{{if .CLs}}
|
|
{{range $cl := .CLs}}
|
|
<tr id="cl-{{$cl.Number}}" class="{{if not $i}}{{if not .Reviewed}}unreplied{{end}}{{end}}">
|
|
<td class="email">{{$cl.DisplayOwner}}</td>
|
|
<td>
|
|
{{if $tbl.Assignable}}
|
|
<select id="cl-rev-{{$cl.Number}}" {{if not $.UserIsReviewer}}disabled{{end}}>
|
|
<option></option>
|
|
{{range $.Reviewers}}
|
|
<option {{selected . $cl.Reviewer}}>{{.}}</option>
|
|
{{end}}
|
|
</select>
|
|
<script type="text/javascript">
|
|
$(function() {
|
|
$('#cl-rev-{{$cl.Number}}').change(function() {
|
|
var r = $(this).val();
|
|
var row = $('tr#cl-{{$cl.Number}}');
|
|
row.addClass('pending');
|
|
$.post('/assign', {
|
|
'cl': '{{$cl.Number}}',
|
|
'r': r
|
|
}).success(function() {
|
|
row.removeClass('pending');
|
|
row.addClass('saved');
|
|
}).error(function() {
|
|
row.removeClass('pending');
|
|
row.addClass('failed');
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
{{end}}
|
|
</td>
|
|
<td>
|
|
<a href="http://codereview.appspot.com/{{.Number}}/" title="{{ printf "%s" .Description}}">{{.Number}}: {{.FirstLineHTML}}</a>
|
|
{{if and .LGTMs $tbl.Assignable}}<br /><span style="font-size: smaller;">LGTMs: {{.LGTMHTML}}</span>{{end}}
|
|
{{if and .NotLGTMs $tbl.Assignable}}<br /><span style="font-size: smaller; color: #f74545;">NOT LGTMs: {{.NotLGTMHTML}}</span>{{end}}
|
|
{{if .LastUpdateBy}}<br /><span style="font-size: smaller; color: #777777;">(<span title="{{.LastUpdateBy}}">{{.LastUpdateBy | shortemail}}</span>) {{.LastUpdate}}</span>{{end}}
|
|
</td>
|
|
<td title="Last modified">{{.ModifiedAgo}}</td>
|
|
<td>{{if $.IsAdmin}}<a href="/update-cl?cl={{.Number}}" title="Update this CL">⟳</a>{{end}}</td>
|
|
</tr>
|
|
{{end}}
|
|
{{else}}
|
|
<tr><td colspan="5"><em>none</em></td></tr>
|
|
{{end}}
|
|
{{end}}
|
|
</table>
|
|
|
|
<hr />
|
|
<address>
|
|
You are <span class="email">{{.User}}</span> · <a href="{{.LogoutURL}}">logout</a><br />
|
|
datastore timing: {{range .Timing}} {{.}}{{end}}
|
|
</address>
|
|
|
|
</body>
|
|
</html>
|
|
`))
|