diff --git a/app/scripts/app.jsx b/app/scripts/app.jsx index c52e09f..5ca4a93 100644 --- a/app/scripts/app.jsx +++ b/app/scripts/app.jsx @@ -67,11 +67,11 @@ var Dashboard = React.createClass({ var OrgStats = React.createClass({ mixins: [Router.Navigation, Router.State], render: function(){ - var topTeams = "/api/stat/teams/top?org="+ this.getParams().org, - teamURL = "/app/"+ this.getParams().org +"/teams/"; + var topRepos = "/api/stat/orgs/top?org="+ this.getParams().org +"&item=repo", + repoURL = "/app/"+ this.getParams().org +"/repos/"; return (
- +
); } @@ -80,8 +80,12 @@ var OrgStats = React.createClass({ var TeamStats = React.createClass({ mixins: [Router.Navigation, Router.State], 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 ( -
Team stats!
+
+ +
); } }); diff --git a/app/scripts/charts.js b/app/scripts/charts.js deleted file mode 100644 index d5a0467..0000000 --- a/app/scripts/charts.js +++ /dev/null @@ -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) - ) - ); - } -}); diff --git a/app/scripts/charts.jsx b/app/scripts/charts.jsx index b05428c..3e6330b 100644 --- a/app/scripts/charts.jsx +++ b/app/scripts/charts.jsx @@ -10,10 +10,11 @@ var BarChart = React.createClass({ componentDidMount: function() { $.get(this.props.api, function(res){ + res = res.slice(0, 15); var max = 1; res.map(function(el) { - if (el.value > max) { - max = el.value + if (el.commits > max) { + max = el.commits } }); this.setState({points: res, max: max}); @@ -44,7 +45,7 @@ var BarChart = React.createClass({ return ( ); } @@ -59,9 +60,10 @@ var Bar = React.createClass({ render: function() { var p = this.props.point w = this.props.width*500, - label = p.item + ": " + p.value, + label = p.item + ": " + p.commits, + lw = label.length*10 + 5, tx = 10; - if (label.length*15 > w) { + if (lw > w) { tx = w + tx; } return ( @@ -70,7 +72,7 @@ var Bar = React.createClass({ width={this.props.width*500} height={this.props.height} x="0" y={this.props.y} rx="2" ry="2" /> - + {label} ); diff --git a/db/db.go b/db/db.go index 2b3623c..b4d3383 100644 --- a/db/db.go +++ b/db/db.go @@ -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) { duration := time.Since(start).Nanoseconds() outcome := "succeeded" diff --git a/db/stat.go b/db/stat.go index 31c4e26..64c3fa4 100644 --- a/db/stat.go +++ b/db/stat.go @@ -1,57 +1,27 @@ package db import ( + "fmt" "time" ) type ( StatItem struct { - Item string `json:"item"` - Value int `json:"value"` + Item string `json:"item"` + Commits int `json:"commits"` + Delta int `json:"delta"` } StatPoint struct { StatItem - Timestamp uint64 `json:"ts"` + Week uint64 `json:"week"` } ) -const orgReposTopQuery = ` +const orgTopQuery = ` select - 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 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 + %s as item, + sum(c.commits) as commits, + sum(c.additions) - sum(c.deletions) as delta from contribs c join members m on c.author = m.user and @@ -60,17 +30,18 @@ join teams t on m.team_id = t.id where m.id is not null and - c.owner = ? and - c.week >= ? and - c.week <= ? + c.owner = :org and + c.week >= :from and + c.week <= :to group by item -order by value desc` +order by %s desc` -const orgTeamsActivityQuery = ` +const orgActivityQuery = ` select - c.week as ts, - t.name as item, - sum(c.commits) as value + %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 @@ -79,54 +50,73 @@ join teams t on m.team_id = t.id where m.id is not null and - c.owner = ? and - c.week >= ? and - c.week <= ? -group by ts, item -order by ts, item` + c.owner = :org and + c.week >= :from and + c.week <= :to +group by item, week +order by week, %s desc` -const orgUsersTopQuery = ` +const teamTopQuery = ` select - c.author as item, - sum(c.commits) value + %s as item, + sum(c.commits) as commits, + sum(c.additions) - sum(c.deletions) as delta 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 = ? and - c.week >= ? and - c.week <= ? + c.owner = :org and + c.week >= :from and + c.week <= :to group by item -order by value desc` +order by %s desc` -func StatOrgReposTop(org string, from, to int64) (res []StatItem) { - defer measure("StatOrgReposTop", time.Now()) - mustSelect(&res, orgReposTopQuery, org, from, to) +const teamActivityQuery = ` +select + %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 } -func StatOrgReposActivity(org string, from, to int64) (res []StatPoint) { - defer measure("StatOrgReposActivity", time.Now()) - mustSelect(&res, orgReposActivityQuery, org, from, to) +func StatOrgActivity(p map[string]interface{}) (res []StatPoint) { + defer measure("StatOrgActivity", time.Now()) + mustSelectN(&res, fmt.Sprintf(orgActivityQuery, p["item"], p["sort"]), p) return } -func StatOrgTeamsTop(org string, from, to int64) (res []StatItem) { - defer measure("StatOrgTeamsTop", time.Now()) - mustSelect(&res, orgTeamsTopQuery, org, from, to) +func StatTeamTop(p map[string]interface{}) (res []StatItem) { + defer measure("StatTeamTop", time.Now()) + mustSelectN(&res, fmt.Sprintf(teamTopQuery, p["item"], p["sort"]), p) return } -func StatOrgTeamsActivity(org string, from, to int64) (res []StatPoint) { - defer measure("StatOrgTeamsActivity", time.Now()) - mustSelect(&res, orgTeamsActivityQuery, org, from, to) - return -} - -func StatOrgUsersTop(org string, from, to int64) (res []StatItem) { - defer measure("StatOrgUsersTop", time.Now()) - mustSelect(&res, orgUsersTopQuery, org, from, to) +func StatTeamActivity(p map[string]interface{}) (res []StatPoint) { + defer measure("StatTeamActivity", time.Now()) + mustSelectN(&res, fmt.Sprintf(teamActivityQuery, p["item"], p["sort"]), p) return } diff --git a/server/api.go b/server/api.go index 28a2a80..5ace053 100644 --- a/server/api.go +++ b/server/api.go @@ -14,12 +14,12 @@ func apiOrgsHandler(w http.ResponseWriter, r *http.Request) { func apiTeamsHandler(w http.ResponseWriter, r *http.Request) { req, stat := parseRequest(w, r) - teams := db.OrgTeams(stat.org) + teams := db.OrgTeams(stat.Org) req.respondWith(teams) } func apiReposHandler(w http.ResponseWriter, r *http.Request) { req, stat := parseRequest(w, r) - repos := db.OrgRepos(stat.org) + repos := db.OrgRepos(stat.Org) req.respondWith(repos) } diff --git a/server/request.go b/server/request.go index 056971d..a09bb7b 100644 --- a/server/request.go +++ b/server/request.go @@ -19,11 +19,13 @@ type ( login string } statRequest struct { - org string - team string - user string - from int64 - to int64 + Org string `structs:"org"` + Team string `structs:"team"` + User string `structs:"user"` + From int64 `structs:"from"` + To int64 `structs:"to"` + Item string `structs:"item"` + Sort string `structs:"sort"` } ) @@ -71,12 +73,33 @@ func parseStatRequest(r *http.Request) *statRequest { } else { 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{ - org: r.FormValue("org"), - team: r.FormValue("team"), - user: r.FormValue("user"), - from: from, - to: to, + Org: r.FormValue("org"), + Team: r.FormValue("team"), + User: r.FormValue("user"), + From: from, + To: to, + Item: item, + Sort: sort, } } diff --git a/server/server.go b/server/server.go index 677aa94..48736ea 100644 --- a/server/server.go +++ b/server/server.go @@ -18,15 +18,16 @@ var ( func init() { http.HandleFunc("/auth/signin", authSigninHandler) http.HandleFunc("/auth/callback", authCallbackHandler) + http.HandleFunc("/api/", authHandler) http.HandleFunc("/api/orgs", apiOrgsHandler) http.HandleFunc("/api/teams", apiTeamsHandler) http.HandleFunc("/api/repos", apiReposHandler) - http.HandleFunc("/api/stat/repos/top", statOrgReposTop) - http.HandleFunc("/api/stat/repos/activity", statOrgReposActivity) - http.HandleFunc("/api/stat/teams/top", statOrgTeamsTop) - http.HandleFunc("/api/stat/teams/activity", statOrgTeamsActivity) - http.HandleFunc("/api/stat/users/top", statOrgUsersTop) + + http.HandleFunc("/api/stat/orgs/top", statOrgTopHandler) + http.HandleFunc("/api/stat/orgs/activity", statOrgActivityHandler) + http.HandleFunc("/api/stat/teams/top", statTeamTopHandler) + http.HandleFunc("/api/stat/teams/activity", statTeamActivityHandler) } func Start() { diff --git a/server/stat.go b/server/stat.go index 177e93d..5c6512c 100644 --- a/server/stat.go +++ b/server/stat.go @@ -3,35 +3,30 @@ package server import ( "net/http" + "github.com/fatih/structs" "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) - top := db.StatOrgReposTop(stat.org, stat.from, stat.to) + top := db.StatOrgTop(structs.Map(stat)) req.respondWith(top) } -func statOrgReposActivity(w http.ResponseWriter, r *http.Request) { +func statOrgActivityHandler(w http.ResponseWriter, r *http.Request) { req, stat := parseRequest(w, r) - activity := db.StatOrgReposActivity(stat.org, stat.from, stat.to) + activity := db.StatOrgActivity(structs.Map(stat)) req.respondWith(activity) } -func statOrgTeamsTop(w http.ResponseWriter, r *http.Request) { +func statTeamTopHandler(w http.ResponseWriter, r *http.Request) { req, stat := parseRequest(w, r) - top := db.StatOrgTeamsTop(stat.org, stat.from, stat.to) + top := db.StatTeamTop(structs.Map(stat)) req.respondWith(top) } -func statOrgTeamsActivity(w http.ResponseWriter, r *http.Request) { +func statTeamActivityHandler(w http.ResponseWriter, r *http.Request) { req, stat := parseRequest(w, r) - activity := db.StatOrgTeamsActivity(stat.org, stat.from, stat.to) + activity := db.StatTeamActivity(structs.Map(stat)) 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) -}