1
0
Fork 0

Org and team charts

This commit is contained in:
Gregory Eremin 2015-03-08 18:17:56 +07:00
parent 311bd021a8
commit e6f934c5dc
9 changed files with 144 additions and 196 deletions

View File

@ -67,11 +67,11 @@ var Dashboard = React.createClass({
var OrgStats = React.createClass({ var OrgStats = React.createClass({
mixins: [Router.Navigation, Router.State], mixins: [Router.Navigation, Router.State],
render: function(){ render: function(){
var topTeams = "/api/stat/teams/top?org="+ this.getParams().org, var topRepos = "/api/stat/orgs/top?org="+ this.getParams().org +"&item=repo",
teamURL = "/app/"+ this.getParams().org +"/teams/"; repoURL = "/app/"+ this.getParams().org +"/repos/";
return ( return (
<section className="content"> <section className="content">
<BarChart api={topTeams} link={teamURL}/> <BarChart api={topRepos} link={repoURL}/>
</section> </section>
); );
} }
@ -80,8 +80,12 @@ var OrgStats = React.createClass({
var TeamStats = React.createClass({ var TeamStats = React.createClass({
mixins: [Router.Navigation, Router.State], mixins: [Router.Navigation, Router.State],
render: function(){ render: function(){
var topRepos = "/api/stat/teams/top?org="+ this.getParams().org +"&team="+ this.getParams().team +"&item=repo",
repoURL = "/app/"+ this.getParams().org +"/repos/";
return ( return (
<section className="content">Team stats!</section> <section className="content">
<BarChart api={topRepos} link={repoURL}/>
</section>
); );
} }
}); });

View File

@ -1,78 +0,0 @@
var Router = ReactRouter;
var BarChart = React.createClass({displayName: "BarChart",
barHeight: 40,
barMargin: 5,
getInitialState: function() {
return {points: [], max: 1};
},
componentDidMount: function() {
$.get(this.props.api, function(res){
var max = 1;
res.map(function(el) {
if (el.value > max) {
max = el.value
}
});
this.setState({points: res, max: max});
}.bind(this))
},
height: function() {
if (this.state.points.length === 0) {
return 0;
} else {
return this.y(this.state.points.length) - this.barMargin;
}
},
y: function(i) {
return i*(this.barHeight + this.barMargin);
},
render: function() {
return (
React.createElement("svg", {className: "barchart", width: "100%", height: this.height()},
this.state.points.map(this.renderBar)
)
);
},
renderBar: function(point, i) {
return (
React.createElement(Bar, {key: point.item, point: point, i: i, link: this.props.link,
y: this.y(i),
width: point.value/this.state.max,
height: this.barHeight})
);
}
});
var Bar = React.createClass({displayName: "Bar",
mixins: [Router.Navigation],
handleClick: function(e) {
this.transitionTo(this.props.link + this.props.point.item);
},
render: function() {
var p = this.props.point
w = this.props.width*500,
label = p.item + ": " + p.value,
tx = 10;
if (label.length*15 > w) {
tx = w + tx;
}
return (
React.createElement("g", {onClick: this.handleClick},
React.createElement("rect", {className: "bar", fill: Colors2[this.props.i],
width: this.props.width*500,
height: this.props.height,
x: "0", y: this.props.y, rx: "2", ry: "2"}),
React.createElement("rect", {className: "label_underlay", x: tx-6, y: this.props.y+10, height: 20, width: label.length*10+5, rx: "3", ry: "3"}),
React.createElement("text", {className: "label", x: tx, y: this.props.y + 26}, label)
)
);
}
});

View File

@ -10,10 +10,11 @@ var BarChart = React.createClass({
componentDidMount: function() { componentDidMount: function() {
$.get(this.props.api, function(res){ $.get(this.props.api, function(res){
res = res.slice(0, 15);
var max = 1; var max = 1;
res.map(function(el) { res.map(function(el) {
if (el.value > max) { if (el.commits > max) {
max = el.value max = el.commits
} }
}); });
this.setState({points: res, max: max}); this.setState({points: res, max: max});
@ -44,7 +45,7 @@ var BarChart = React.createClass({
return ( return (
<Bar key={point.item} point={point} i={i} link={this.props.link} <Bar key={point.item} point={point} i={i} link={this.props.link}
y={this.y(i)} y={this.y(i)}
width={point.value/this.state.max} width={point.commits/this.state.max}
height={this.barHeight} /> height={this.barHeight} />
); );
} }
@ -59,9 +60,10 @@ var Bar = React.createClass({
render: function() { render: function() {
var p = this.props.point var p = this.props.point
w = this.props.width*500, w = this.props.width*500,
label = p.item + ": " + p.value, label = p.item + ": " + p.commits,
lw = label.length*10 + 5,
tx = 10; tx = 10;
if (label.length*15 > w) { if (lw > w) {
tx = w + tx; tx = w + tx;
} }
return ( return (
@ -70,7 +72,7 @@ var Bar = React.createClass({
width={this.props.width*500} width={this.props.width*500}
height={this.props.height} height={this.props.height}
x="0" y={this.props.y} rx="2" ry="2" /> x="0" y={this.props.y} rx="2" ry="2" />
<rect className="label_underlay" x={tx-6} y={this.props.y+10} height={20} width={label.length*10+5} rx="3" ry="3" /> <rect className="label_underlay" x={tx-6} y={this.props.y+10} height={20} width={lw} rx="3" ry="3" />
<text className="label" x={tx} y={this.props.y + 26}>{label}</text> <text className="label" x={tx} y={this.props.y + 26}>{label}</text>
</g> </g>
); );

View File

@ -31,6 +31,17 @@ func mustSelect(dest interface{}, query string, args ...interface{}) {
} }
} }
func mustSelectN(dest interface{}, query string, params interface{}) {
var stmt *sqlx.NamedStmt
var err error
if stmt, err = db.PrepareNamed(query); err != nil {
panic(err)
}
if err = stmt.Select(dest, params); err != nil {
panic(err)
}
}
func measure(op string, start time.Time) { func measure(op string, start time.Time) {
duration := time.Since(start).Nanoseconds() duration := time.Since(start).Nanoseconds()
outcome := "succeeded" outcome := "succeeded"

View File

@ -1,57 +1,27 @@
package db package db
import ( import (
"fmt"
"time" "time"
) )
type ( type (
StatItem struct { StatItem struct {
Item string `json:"item"` Item string `json:"item"`
Value int `json:"value"` Commits int `json:"commits"`
Delta int `json:"delta"`
} }
StatPoint struct { StatPoint struct {
StatItem StatItem
Timestamp uint64 `json:"ts"` Week uint64 `json:"week"`
} }
) )
const orgReposTopQuery = ` const orgTopQuery = `
select select
c.repo as item, %s as item,
sum(c.commits) as value sum(c.commits) as commits,
from contribs c sum(c.additions) - sum(c.deletions) as delta
join members m on
c.author = m.user and
c.owner = m.org
where
m.id is not null and
c.owner = ? and
c.week >= ? and
c.week <= ?
group by item
order by value desc`
const orgReposActivityQuery = `
select
c.week as ts,
c.repo as item,
sum(c.commits) as value
from contribs c
join members m on
c.author = m.user and
c.owner = m.org
where
m.id is not null and
c.owner = ? and
c.week >= ? and
c.week <= ?
group by ts, item
order by ts, item`
const orgTeamsTopQuery = `
select
t.name as item,
sum(c.commits) value
from contribs c from contribs c
join members m on join members m on
c.author = m.user and c.author = m.user and
@ -60,17 +30,18 @@ join teams t on
m.team_id = t.id m.team_id = t.id
where where
m.id is not null and m.id is not null and
c.owner = ? and c.owner = :org and
c.week >= ? and c.week >= :from and
c.week <= ? c.week <= :to
group by item group by item
order by value desc` order by %s desc`
const orgTeamsActivityQuery = ` const orgActivityQuery = `
select select
c.week as ts, %s as item,
t.name as item, sum(c.commits) as commits,
sum(c.commits) as value sum(c.additions) - sum(c.deletions) as delta,
c.week as week
from contribs c from contribs c
join members m on join members m on
c.author = m.user and c.author = m.user and
@ -79,54 +50,73 @@ join teams t on
m.team_id = t.id m.team_id = t.id
where where
m.id is not null and m.id is not null and
c.owner = ? and c.owner = :org and
c.week >= ? and c.week >= :from and
c.week <= ? c.week <= :to
group by ts, item group by item, week
order by ts, item` order by week, %s desc`
const orgUsersTopQuery = ` const teamTopQuery = `
select select
c.author as item, %s as item,
sum(c.commits) value sum(c.commits) as commits,
sum(c.additions) - sum(c.deletions) as delta
from contribs c from contribs c
join members m on join members m on
c.author = m.user and c.author = m.user and
c.owner = m.org c.owner = m.org
join teams t on
m.team_id = t.id and
t.name = :team
where where
m.id is not null and m.id is not null and
c.owner = ? and c.owner = :org and
c.week >= ? and c.week >= :from and
c.week <= ? c.week <= :to
group by item group by item
order by value desc` order by %s desc`
func StatOrgReposTop(org string, from, to int64) (res []StatItem) { const teamActivityQuery = `
defer measure("StatOrgReposTop", time.Now()) select
mustSelect(&res, orgReposTopQuery, org, from, to) %s as item,
sum(c.commits) as commits,
sum(c.additions) - sum(c.deletions) as delta,
c.week as week
from contribs c
join members m on
c.author = m.user and
c.owner = m.org
join teams t on
m.team_id = t.id and
t.name = :team
where
m.id is not null and
c.owner = :org and
c.week >= :from and
c.week <= :to
group by item, week
order by week, %s desc`
func StatOrgTop(p map[string]interface{}) (res []StatItem) {
defer measure("StatOrgTop", time.Now())
mustSelectN(&res, fmt.Sprintf(orgTopQuery, p["item"], p["sort"]), p)
return return
} }
func StatOrgReposActivity(org string, from, to int64) (res []StatPoint) { func StatOrgActivity(p map[string]interface{}) (res []StatPoint) {
defer measure("StatOrgReposActivity", time.Now()) defer measure("StatOrgActivity", time.Now())
mustSelect(&res, orgReposActivityQuery, org, from, to) mustSelectN(&res, fmt.Sprintf(orgActivityQuery, p["item"], p["sort"]), p)
return return
} }
func StatOrgTeamsTop(org string, from, to int64) (res []StatItem) { func StatTeamTop(p map[string]interface{}) (res []StatItem) {
defer measure("StatOrgTeamsTop", time.Now()) defer measure("StatTeamTop", time.Now())
mustSelect(&res, orgTeamsTopQuery, org, from, to) mustSelectN(&res, fmt.Sprintf(teamTopQuery, p["item"], p["sort"]), p)
return return
} }
func StatOrgTeamsActivity(org string, from, to int64) (res []StatPoint) { func StatTeamActivity(p map[string]interface{}) (res []StatPoint) {
defer measure("StatOrgTeamsActivity", time.Now()) defer measure("StatTeamActivity", time.Now())
mustSelect(&res, orgTeamsActivityQuery, org, from, to) mustSelectN(&res, fmt.Sprintf(teamActivityQuery, p["item"], p["sort"]), p)
return
}
func StatOrgUsersTop(org string, from, to int64) (res []StatItem) {
defer measure("StatOrgUsersTop", time.Now())
mustSelect(&res, orgUsersTopQuery, org, from, to)
return return
} }

View File

@ -14,12 +14,12 @@ func apiOrgsHandler(w http.ResponseWriter, r *http.Request) {
func apiTeamsHandler(w http.ResponseWriter, r *http.Request) { func apiTeamsHandler(w http.ResponseWriter, r *http.Request) {
req, stat := parseRequest(w, r) req, stat := parseRequest(w, r)
teams := db.OrgTeams(stat.org) teams := db.OrgTeams(stat.Org)
req.respondWith(teams) req.respondWith(teams)
} }
func apiReposHandler(w http.ResponseWriter, r *http.Request) { func apiReposHandler(w http.ResponseWriter, r *http.Request) {
req, stat := parseRequest(w, r) req, stat := parseRequest(w, r)
repos := db.OrgRepos(stat.org) repos := db.OrgRepos(stat.Org)
req.respondWith(repos) req.respondWith(repos)
} }

View File

@ -19,11 +19,13 @@ type (
login string login string
} }
statRequest struct { statRequest struct {
org string Org string `structs:"org"`
team string Team string `structs:"team"`
user string User string `structs:"user"`
from int64 From int64 `structs:"from"`
to int64 To int64 `structs:"to"`
Item string `structs:"item"`
Sort string `structs:"sort"`
} }
) )
@ -71,12 +73,33 @@ func parseStatRequest(r *http.Request) *statRequest {
} else { } else {
to = time.Now().Unix() to = time.Now().Unix()
} }
var item string
switch val := r.FormValue("item"); val {
case "author":
item = "c.author"
case "team":
item = "t.name"
default:
item = "c.repo"
}
var sort string
switch val := r.FormValue("sort"); val {
case "commits", "delta":
sort = val
default:
sort = "commits"
}
return &statRequest{ return &statRequest{
org: r.FormValue("org"), Org: r.FormValue("org"),
team: r.FormValue("team"), Team: r.FormValue("team"),
user: r.FormValue("user"), User: r.FormValue("user"),
from: from, From: from,
to: to, To: to,
Item: item,
Sort: sort,
} }
} }

View File

@ -18,15 +18,16 @@ var (
func init() { func init() {
http.HandleFunc("/auth/signin", authSigninHandler) http.HandleFunc("/auth/signin", authSigninHandler)
http.HandleFunc("/auth/callback", authCallbackHandler) http.HandleFunc("/auth/callback", authCallbackHandler)
http.HandleFunc("/api/", authHandler) http.HandleFunc("/api/", authHandler)
http.HandleFunc("/api/orgs", apiOrgsHandler) http.HandleFunc("/api/orgs", apiOrgsHandler)
http.HandleFunc("/api/teams", apiTeamsHandler) http.HandleFunc("/api/teams", apiTeamsHandler)
http.HandleFunc("/api/repos", apiReposHandler) http.HandleFunc("/api/repos", apiReposHandler)
http.HandleFunc("/api/stat/repos/top", statOrgReposTop)
http.HandleFunc("/api/stat/repos/activity", statOrgReposActivity) http.HandleFunc("/api/stat/orgs/top", statOrgTopHandler)
http.HandleFunc("/api/stat/teams/top", statOrgTeamsTop) http.HandleFunc("/api/stat/orgs/activity", statOrgActivityHandler)
http.HandleFunc("/api/stat/teams/activity", statOrgTeamsActivity) http.HandleFunc("/api/stat/teams/top", statTeamTopHandler)
http.HandleFunc("/api/stat/users/top", statOrgUsersTop) http.HandleFunc("/api/stat/teams/activity", statTeamActivityHandler)
} }
func Start() { func Start() {

View File

@ -3,35 +3,30 @@ package server
import ( import (
"net/http" "net/http"
"github.com/fatih/structs"
"github.com/localhots/empact/db" "github.com/localhots/empact/db"
) )
func statOrgReposTop(w http.ResponseWriter, r *http.Request) { func statOrgTopHandler(w http.ResponseWriter, r *http.Request) {
req, stat := parseRequest(w, r) req, stat := parseRequest(w, r)
top := db.StatOrgReposTop(stat.org, stat.from, stat.to) top := db.StatOrgTop(structs.Map(stat))
req.respondWith(top) req.respondWith(top)
} }
func statOrgReposActivity(w http.ResponseWriter, r *http.Request) { func statOrgActivityHandler(w http.ResponseWriter, r *http.Request) {
req, stat := parseRequest(w, r) req, stat := parseRequest(w, r)
activity := db.StatOrgReposActivity(stat.org, stat.from, stat.to) activity := db.StatOrgActivity(structs.Map(stat))
req.respondWith(activity) req.respondWith(activity)
} }
func statOrgTeamsTop(w http.ResponseWriter, r *http.Request) { func statTeamTopHandler(w http.ResponseWriter, r *http.Request) {
req, stat := parseRequest(w, r) req, stat := parseRequest(w, r)
top := db.StatOrgTeamsTop(stat.org, stat.from, stat.to) top := db.StatTeamTop(structs.Map(stat))
req.respondWith(top) req.respondWith(top)
} }
func statOrgTeamsActivity(w http.ResponseWriter, r *http.Request) { func statTeamActivityHandler(w http.ResponseWriter, r *http.Request) {
req, stat := parseRequest(w, r) req, stat := parseRequest(w, r)
activity := db.StatOrgTeamsActivity(stat.org, stat.from, stat.to) activity := db.StatTeamActivity(structs.Map(stat))
req.respondWith(activity) req.respondWith(activity)
} }
func statOrgUsersTop(w http.ResponseWriter, r *http.Request) {
req, stat := parseRequest(w, r)
top := db.StatOrgUsersTop(stat.org, stat.from, stat.to)
req.respondWith(top)
}