commit
1d803a9bf4
|
@ -1 +1,5 @@
|
|||
node_modules/
|
||||
node_modules/
|
||||
.idea/
|
||||
*.log
|
||||
src/components/
|
||||
config.json
|
|
@ -0,0 +1,4 @@
|
|||
test:
|
||||
./node_modules/.bin/mocha --compilers coffee:coffee-script --reporter spec --ui exports --bail
|
||||
|
||||
.PHONY: test
|
164
README.md
164
README.md
|
@ -1,78 +1,118 @@
|
|||
# GitHub Burndown App
|
||||
#GitHub Burndown Chart
|
||||
|
||||
An app that displays a burndown chart for your GitHub Issues.
|
||||
**The original app got completely rewritten, see [notes](#rewrite), thank you.**
|
||||
|
||||
Displays a burndown chart from a set of GitHub issues in the current milestone.
|
||||
|
||||
[ ![Codeship Status for radekstepan/github-burndown-chart](https://www.codeship.io/projects/d69f4420-e5b0-0130-bbae-1632ddfb80f8/status?branch=rework)](https://www.codeship.io/projects/5855)
|
||||
|
||||
##Features
|
||||
|
||||
1. Client side; easily hosted on GitHub Pages.
|
||||
1. Private repos; use your GitHub API Token hiding it from public view if need be.
|
||||
1. Off days; specify which days of the week to leave out from ideal burndown progression line.
|
||||
1. Trend line; to see if you can make it to the deadline at this pace.
|
||||
|
||||
![image](https://raw.github.com/radekstepan/github-burndown-chart/original/example.png)
|
||||
|
||||
## Requirements:
|
||||
##Quickstart
|
||||
|
||||
You can install all the following dependencies by running:
|
||||
1. Choose a **repo** that you want to display burndown chart for.
|
||||
1. Make sure this repo has some **issues** assigned to a **milestone**.
|
||||
1. Put some **labels** on the issues looking like this: `size 1`, `size 3` etc.
|
||||
1. **Close** some of them labeled issues.
|
||||
1. Visit [http://radekstepan.com/github-burndown-chart/](http://radekstepan.com/github-burndown-chart/) following the instructions there.
|
||||
|
||||
##Configuration
|
||||
|
||||
There are three modes of operation balancing between usability & security:
|
||||
|
||||
1. **Static Mode**: you can just serve the `public` directory using a static file server or GitHub Pages. No config needed, just serve the app and point to your repo in the browser, e.g.: `http://127.0.0.1:8000/#!/radekstepan/disposable`. You are rate limited to the tune of [60 requests per hour](http://developer.github.com/v3/#rate-limiting).
|
||||
1. **Static Mode (Public Token)**: as before but now you want to use your [GitHub OAuth2 API Token](http://developer.github.com/v3/#authentication) in the config. This will require you to specify the token in the `config.json` file as outlined below.
|
||||
1. **Proxy Mode (Private Token)**: you find it preposterous to share your token with the world. In this case you will need to serve the app using the [Proxy Mode](#proxy-mode). Your token will be scrubbed from the config file and all requests be routed through a proxy.
|
||||
|
||||
All of the following fields are defined in `config.json` and none of them, including the file itself, are required:
|
||||
|
||||
###Size Label
|
||||
|
||||
The way we are getting a size of an issue from GitHub is by putting a label on it. The following regex (string) specifies which part of the label represents the number.
|
||||
|
||||
```json
|
||||
{
|
||||
"size_label": "^size (\\d+)$"
|
||||
}
|
||||
```
|
||||
|
||||
This is also the default label if no other is specified.
|
||||
|
||||
###Token
|
||||
|
||||
Your OAuth2 token from GitHub. Get it [here](https://github.com/settings/applications). Bear in mind that if you just statically serve the app, everybody will be able to see the token in transmission. If you would like to avoid that, use the [Proxy Mode](#proxy-mode).
|
||||
|
||||
Using the token increases your limit of requests per hour from [60 to 5000](http://developer.github.com/v3/#rate-limiting).
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "API_TOKEN"
|
||||
}
|
||||
```
|
||||
|
||||
###Off Days/Weekends
|
||||
|
||||
An array of day integers (Monday = 1) representing days of the week when you are not working. This will make the expected burndown line be more accurate.
|
||||
|
||||
```json
|
||||
{
|
||||
"off_days": [ 6, 7 ]
|
||||
}
|
||||
```
|
||||
|
||||
##Proxy Mode
|
||||
|
||||
Use this strategy if you do not wish for your token to be publicly visible. Proxy mode routes all requests from the client side app through it, scrubbing the token from the `config.json` file. It is *slightly* slower than requesting data straight from GitHub of course.
|
||||
|
||||
Make sure you have [CoffeeScript](http://coffeescript.org/) installed:
|
||||
|
||||
```bash
|
||||
$ npm install coffeescript -g
|
||||
```
|
||||
|
||||
Then start the proxy passing port number as an argument:
|
||||
|
||||
```bash
|
||||
$ PORT=1234 coffee proxy.coffee
|
||||
```
|
||||
|
||||
Visit the port in question in the browser and continue as before.
|
||||
|
||||
##Build It
|
||||
|
||||
If you would like to run your own build for a custom version of the app, use the [Apps/B Builder](https://github.com/intermine/apps-b-builder).
|
||||
|
||||
In short:
|
||||
|
||||
```bash
|
||||
$ npm install apps-b-builder -g
|
||||
$ apps-b build ./src/ ./build/
|
||||
```
|
||||
|
||||
##Test It
|
||||
|
||||
```bash
|
||||
$ npm install -d
|
||||
$ make test
|
||||
```
|
||||
|
||||
- [CoffeeScript](http://coffeescript.org/)
|
||||
- [express](http://expressjs.com/)
|
||||
- [eco](https://github.com/sstephenson/eco)
|
||||
- [js-yaml](https://github.com/visionmedia/js-yaml)
|
||||
Each bugfix receives an accompanying test case.
|
||||
|
||||
## Configure:
|
||||
##Rewrite
|
||||
|
||||
The app is configured by pointing to a GitHub user/project. Do so in `config.yml`:
|
||||
The original app got rewritten from a clunky server side to a (better) client side app. Some tests are also provided and more will be going into the future.
|
||||
|
||||
```yaml
|
||||
github_user: 'intermine'
|
||||
github_project: 'InterMine'
|
||||
project_name: 'Core InterMine Project'
|
||||
```
|
||||
If you are upgrading from the previous app, then please bear in mind that `config.yaml` is replaced with `config.json`.
|
||||
|
||||
The `project_name` key-value pair represents the title of the burndown chart that you will see in the top right corner of the page.
|
||||
If you would like to use the original app, please refer to the `original` branch.
|
||||
|
||||
To configure the app for a private GitHub project, you must additionally set the `api_token` key-value pair in `config.yml`:
|
||||
##Thanks
|
||||
|
||||
```yaml
|
||||
api_token: '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8'
|
||||
```
|
||||
|
||||
To generate an access token, see your [GitHub Application Settings](https://github.com/settings/applications).
|
||||
|
||||
### Milestones
|
||||
|
||||
Then visit your GitHub project's Issues page and create a new milestone with a date due in the future. This will represent your iteration. This app will pick the Milestone with the **closest due date in the future** as the *current* one.
|
||||
|
||||
### Sizes
|
||||
|
||||
Then assign a few labels to tickets in this Milestone. These labels will represent your perceived size of the task. The label takes a form of *size [number]* so to say that an Issue is as big as *5* points I would create and assign this label (don't worry about the colors...):
|
||||
|
||||
```
|
||||
size 5
|
||||
```
|
||||
|
||||
### Weekends
|
||||
|
||||
If you have days when you do not work on a project, edit the `config.yml` file with a list of days of the week when you are off. The numbers are 1 indexed and follow the international standard of starting a week on Monday, so for a Saturday and Sunday weekend do this:
|
||||
|
||||
```yaml
|
||||
weekend: [ 6, 7 ]
|
||||
```
|
||||
|
||||
### Base URL to app
|
||||
|
||||
If the app does not live in the root path of your server, edit the `base_url` property in the config file.
|
||||
|
||||
## Use:
|
||||
|
||||
```bash
|
||||
$ node start.js
|
||||
```
|
||||
|
||||
Then visit [http://127.0.0.1:3000/](http://127.0.0.1:3000/) or whichever port was configured in `process.env.PORT`.
|
||||
|
||||
The **orange line** - this represents you closing the Issues as you go through them. When you hover over it you will see, for each day, what the closed Issues were and how many points are left.
|
||||
|
||||
The **blue line** - this represents the dropping size of the outstanding Issues planned for the iteration/Milestone.
|
||||
|
||||
There is nothing to save in a database so each refresh of the page fetches all of the latest information from GitHub.
|
||||
|
||||
Enjoy!
|
||||
Thank you for using the app and your feedback/comments are very much welcome. Radek
|
||||
|
|
231
app.coffee
231
app.coffee
|
@ -1,231 +0,0 @@
|
|||
flatiron = require 'flatiron'
|
||||
connect = require 'connect'
|
||||
https = require 'https'
|
||||
fs = require "fs"
|
||||
yaml = require "js-yaml"
|
||||
eco = require 'eco'
|
||||
|
||||
# Helper object for GitHub Issues.
|
||||
Issues =
|
||||
|
||||
# Make HTTPS GET to GitHub API v3.
|
||||
get: (path, type, callback) ->
|
||||
options =
|
||||
host: "api.github.com"
|
||||
method: "GET"
|
||||
path: path
|
||||
headers:
|
||||
'User-Agent': 'Scrum Burndown (1)'
|
||||
|
||||
if Issues.config.api_token
|
||||
options.headers.Authorization = 'token '+Issues.config.api_token
|
||||
|
||||
https.request(options, (response) ->
|
||||
if response.statusCode is 200
|
||||
json = ""
|
||||
response.on "data", (chunk) -> json += chunk
|
||||
|
||||
response.on "end", -> callback JSON.parse(json), type
|
||||
else
|
||||
throw response.statusCode
|
||||
).end()
|
||||
|
||||
# URLs to API.
|
||||
getOpenIssues: (callback) -> Issues.get "/repos/#{Issues.config.github_user}/#{Issues.config.github_project}/issues?state=open", 'issues', callback
|
||||
getClosedIssues: (callback) -> Issues.get "/repos/#{Issues.config.github_user}/#{Issues.config.github_project}/issues?state=closed", 'issues', callback
|
||||
getMilestones: (callback) -> Issues.get "/repos/#{Issues.config.github_user}/#{Issues.config.github_project}/milestones", 'milestones', callback
|
||||
|
||||
# Convert GitHub ISO 8601 to JS timestamp at the beginning of UTC day!'
|
||||
dateToTime: (date) ->
|
||||
# Some milestones do not have due dates.
|
||||
return 0 unless date?
|
||||
# Add miliseconds and create `Date`.
|
||||
date = new Date(date[0...date.length - 1] + '.000' + date.charAt date.length-1)
|
||||
# Move to the beginning of the day (at 9am BST, 8am GMT, so we do not worry about time shifts).
|
||||
date = new Date date.getFullYear(), date.getMonth(), date.getDate(), 9
|
||||
# Return timestamp.
|
||||
date.getTime()
|
||||
|
||||
# Format issues for display in a listing.
|
||||
format: (issue) ->
|
||||
# Format the timestamps.
|
||||
if issue.created_at? then issue.created_at = new Date(Issues.dateToTime(issue.created_at)).toUTCString()
|
||||
if issue.updated_at? then issue.updated_at = new Date(Issues.dateToTime(issue.updated_at)).toUTCString()
|
||||
|
||||
issue
|
||||
|
||||
# Config filters.
|
||||
app = flatiron.app
|
||||
app.use flatiron.plugins.http,
|
||||
'before': [
|
||||
connect.favicon()
|
||||
connect.static __dirname + '/public'
|
||||
]
|
||||
|
||||
# Eco templating.
|
||||
app.use
|
||||
name: "eco-templating"
|
||||
attach: (options) ->
|
||||
app.eco = (path, data, cb) ->
|
||||
fs.readFile "./templates/#{path}.eco", "utf8", (err, template) ->
|
||||
if err then cb err, null
|
||||
else
|
||||
try
|
||||
cb null, eco.render template, data
|
||||
catch e
|
||||
cb e, null
|
||||
|
||||
# Show burndown chart.
|
||||
getBurndown = ->
|
||||
console.log 'Get burndown chart'
|
||||
|
||||
resources = 3 ; store = { 'issues': [], 'milestones': [] }
|
||||
done = (data, type) =>
|
||||
# One less to do.
|
||||
resources--
|
||||
|
||||
switch type
|
||||
when 'issues' then store.issues = store.issues.concat data
|
||||
when 'milestones' then store.milestones = store.milestones.concat data
|
||||
|
||||
# Are we done?
|
||||
if resources is 0
|
||||
# Do we actually have an open milestone?
|
||||
if store.milestones.length > 0
|
||||
# Store the current milestone and its size.
|
||||
current = { 'milestone': {}, 'diff': +Infinity, 'size': 0 }
|
||||
|
||||
# Determine the 'current' milestone
|
||||
now = new Date().getTime()
|
||||
for milestone in store.milestones
|
||||
# JS expects more accuracy.
|
||||
due = Issues.dateToTime milestone['due_on']
|
||||
# Is this the 'current' one?
|
||||
diff = due - now
|
||||
if diff > 0 and diff < current.diff
|
||||
current.milestone = milestone ; current.diff = diff ; current.due = due
|
||||
|
||||
# Create n dict with all dates in the milestone span.
|
||||
days = {} ; totalDays = 0 ; totalNonWorkingDays = 0
|
||||
day = Issues.dateToTime current.milestone.created_at
|
||||
while day < current.due
|
||||
# Do we have weekends configured?
|
||||
if Issues.config.weekend? and Issues.config.weekend instanceof Array
|
||||
dayOfWeek = new Date(day).getDay()
|
||||
# Fix stupid Abrahamic tradition.
|
||||
if dayOfWeek is 0 then dayOfWeek = 7
|
||||
|
||||
# Does this day fall on a weekend?
|
||||
if dayOfWeek in Issues.config.weekend
|
||||
totalNonWorkingDays += 1
|
||||
# Save the day.
|
||||
days[day] = { 'issues': [], 'actual': 0, 'ideal': 0, 'weekend': true }
|
||||
else
|
||||
# Save the day.
|
||||
days[day] = { 'issues': [], 'actual': 0, 'ideal': 0, 'weekend': false }
|
||||
else
|
||||
# Save the day.
|
||||
days[day] = { 'issues': [], 'actual': 0, 'ideal': 0, 'weekend': false }
|
||||
|
||||
# Shift by a day.
|
||||
day += 1000 * 60 * 60 * 24
|
||||
# Increase the total count.
|
||||
totalDays += 1
|
||||
|
||||
# Now go through the issues and place them to the appropriate days.
|
||||
for issue in store.issues
|
||||
# This milestone?
|
||||
if issue.milestone?.number is current.milestone.number
|
||||
# Has a size label?
|
||||
if issue.labels?
|
||||
issue.size = do (issue) ->
|
||||
for label in issue.labels
|
||||
if label.name.indexOf("size ") is 0
|
||||
return parseInt label.name[5...]
|
||||
|
||||
if issue.size?
|
||||
# Increase the total size of the milestone.
|
||||
current.size += issue.size
|
||||
# Is it closed?
|
||||
if issue.closed_at?
|
||||
# Save it.
|
||||
days[Issues.dateToTime issue.closed_at]['issues'].push issue
|
||||
|
||||
# Calculate the predicted daily velocity.
|
||||
dailyIdeal = current['size'] / (totalDays - totalNonWorkingDays) ; ideal = current['size']
|
||||
|
||||
# Go through the days and save the number of outstanding issues size.
|
||||
for day, d of days
|
||||
# Does this day have any closed issues? Reduce the total for this milestone.
|
||||
for issue in d['issues']
|
||||
current['size'] -= issue.size
|
||||
# Save the oustanding count for that day.
|
||||
days[day].actual = current['size']
|
||||
|
||||
# Save the predicted velocity for that day if it is not a non-working day.
|
||||
ideal -= dailyIdeal unless days[day].weekend
|
||||
days[day].ideal = ideal
|
||||
|
||||
# Finally send to client.
|
||||
app.eco 'burndown',
|
||||
'days': days
|
||||
'project': Issues.config.project_name
|
||||
'base_url': Issues.config.base_url
|
||||
, (err, html) =>
|
||||
throw err if err
|
||||
@res.writeHead 200, "content-type": "text/html"
|
||||
@res.write html
|
||||
@res.end()
|
||||
|
||||
else
|
||||
# No current milestone.
|
||||
app.eco 'empty',
|
||||
'project': Issues.config.project_name
|
||||
'base_url': Issues.config.base_url
|
||||
, (err, html) =>
|
||||
throw err if err
|
||||
@res.writeHead 200, "content-type": "text/html"
|
||||
@res.write html
|
||||
@res.end()
|
||||
|
||||
# Get Milestones, Opened and Closed Tickets.
|
||||
Issues.getMilestones done
|
||||
Issues.getOpenIssues done
|
||||
Issues.getClosedIssues done
|
||||
|
||||
# Show open issues.
|
||||
getIssues = ->
|
||||
console.log 'Get open issues'
|
||||
|
||||
Issues.getOpenIssues (issues) =>
|
||||
|
||||
# Replace the dates in issues with nice dates.
|
||||
issues = ( Issues.format(issue) for issue in issues )
|
||||
|
||||
app.eco 'issues',
|
||||
'issues': issues
|
||||
'project': Issues.config.project_name
|
||||
'base_url': Issues.config.base_url
|
||||
, (err, html) =>
|
||||
throw err if err
|
||||
@res.writeHead 200, "content-type": "text/html"
|
||||
@res.write html
|
||||
@res.end()
|
||||
|
||||
# Routes
|
||||
app.router.path '/', ->
|
||||
@get getBurndown
|
||||
|
||||
app.router.path '/burndown', ->
|
||||
@get getBurndown
|
||||
|
||||
app.router.path '/issues', ->
|
||||
@get getIssues
|
||||
|
||||
# Fetch config and start server.
|
||||
fs.readFile "config.yml", "utf8", (err, data) ->
|
||||
Issues.config = yaml.load data
|
||||
|
||||
app.start process.env.PORT, (err) ->
|
||||
throw err if err
|
||||
console.log "Listening on port #{app.server.address().port}"
|
|
@ -0,0 +1,684 @@
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
/*! normalize.css v2.1.2 | MIT License | git.io/normalize */
|
||||
|
||||
/* ==========================================================================
|
||||
HTML5 display definitions
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Correct `block` display not defined in IE 8/9.
|
||||
*/
|
||||
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
main,
|
||||
nav,
|
||||
section,
|
||||
summary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct `inline-block` display not defined in IE 8/9.
|
||||
*/
|
||||
|
||||
audio,
|
||||
canvas,
|
||||
video {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent modern browsers from displaying `audio` without controls.
|
||||
* Remove excess height in iOS 5 devices.
|
||||
*/
|
||||
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `[hidden]` styling not present in IE 8/9.
|
||||
* Hide the `template` element in IE, Safari, and Firefox < 22.
|
||||
*/
|
||||
|
||||
[hidden],
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Base
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Set default font family to sans-serif.
|
||||
* 2. Prevent iOS text size adjust after orientation change, without disabling
|
||||
* user zoom.
|
||||
*/
|
||||
|
||||
html {
|
||||
font-family: sans-serif; /* 1 */
|
||||
-ms-text-size-adjust: 100%; /* 2 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default margin.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Links
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background color from active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `outline` inconsistency between Chrome and other browsers.
|
||||
*/
|
||||
|
||||
a:focus {
|
||||
outline: thin dotted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improve readability when focused and also mouse hovered in all browsers.
|
||||
*/
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Typography
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address variable `h1` font-size and margin within `section` and `article`
|
||||
* contexts in Firefox 4+, Safari 5, and Chrome.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9, Safari 5, and Chrome.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: 1px dotted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in Safari 5 and Chrome.
|
||||
*/
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address differences between Firefox and other browsers.
|
||||
*/
|
||||
|
||||
hr {
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9.
|
||||
*/
|
||||
|
||||
mark {
|
||||
background: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct font family set oddly in Safari 5 and Chrome.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-family: monospace, serif;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improve readability of pre-formatted text in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set consistent quote types.
|
||||
*/
|
||||
|
||||
q {
|
||||
quotes: "\201C" "\201D" "\2018" "\2019";
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent and variable font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove border when inside `a` element in IE 8/9.
|
||||
*/
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct overflow displayed oddly in IE 9.
|
||||
*/
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Figures
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address margin not present in IE 8/9 and Safari 5.
|
||||
*/
|
||||
|
||||
figure {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Define consistent border, margin, and padding.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
border: 1px solid #c0c0c0;
|
||||
margin: 0 2px;
|
||||
padding: 0.35em 0.625em 0.75em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `color` not being inherited in IE 8/9.
|
||||
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
|
||||
*/
|
||||
|
||||
legend {
|
||||
border: 0; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct font family not being inherited in all browsers.
|
||||
* 2. Correct font size not being inherited in all browsers.
|
||||
* 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 2 */
|
||||
margin: 0; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
|
||||
* the UA stylesheet.
|
||||
*/
|
||||
|
||||
button,
|
||||
input {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||
* All other form control elements do not inherit `text-transform` values.
|
||||
* Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+.
|
||||
* Correct `select` style inheritance in Firefox 4+ and Opera.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||
* and `video` controls.
|
||||
* 2. Correct inability to style clickable `input` types in iOS.
|
||||
* 3. Improve usability and consistency of cursor style between image-type
|
||||
* `input` and others.
|
||||
*/
|
||||
|
||||
button,
|
||||
html input[type="button"], /* 1 */
|
||||
input[type="reset"],
|
||||
input[type="submit"] {
|
||||
-webkit-appearance: button; /* 2 */
|
||||
cursor: pointer; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-set default cursor for disabled elements.
|
||||
*/
|
||||
|
||||
button[disabled],
|
||||
html input[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Address box sizing set to `content-box` in IE 8/9.
|
||||
* 2. Remove excess padding in IE 8/9.
|
||||
*/
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
|
||||
* 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome
|
||||
* (include `-moz` to future-proof).
|
||||
*/
|
||||
|
||||
input[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
-moz-box-sizing: content-box;
|
||||
-webkit-box-sizing: content-box; /* 2 */
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and search cancel button in Safari 5 and Chrome
|
||||
* on OS X.
|
||||
*/
|
||||
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and border in Firefox 4+.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove default vertical scrollbar in IE 8/9.
|
||||
* 2. Improve readability and alignment in all browsers.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto; /* 1 */
|
||||
vertical-align: top; /* 2 */
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Tables
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove most spacing between table cells.
|
||||
*/
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
|
||||
.tip {
|
||||
position: absolute;
|
||||
padding: 5px;
|
||||
z-index: 1000;
|
||||
/* default offset for edge-cases: https://github.com/component/tip/pull/12 */
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* effects */
|
||||
|
||||
.tip.fade {
|
||||
transition: opacity 100ms;
|
||||
-moz-transition: opacity 100ms;
|
||||
-webkit-transition: opacity 100ms;
|
||||
}
|
||||
|
||||
.tip-hide {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tip {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.tip-inner {
|
||||
background-color: rgba(0,0,0,.75);
|
||||
color: #fff;
|
||||
padding: 8px 10px 7px 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tip-inner {
|
||||
border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
-webkit-border-radius: 3px;
|
||||
}
|
||||
|
||||
.tip-arrow {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
line-height: 0;
|
||||
border: 5px dashed rgba(0,0,0,.75);
|
||||
}
|
||||
|
||||
.tip-arrow-north { border-bottom-color: rgba(0,0,0,.75) }
|
||||
.tip-arrow-south { border-top-color: rgba(0,0,0,.75) }
|
||||
.tip-arrow-east { border-left-color: rgba(0,0,0,.75) }
|
||||
.tip-arrow-west { border-right-color: rgba(0,0,0,.75) }
|
||||
|
||||
.tip-north .tip-arrow,
|
||||
.tip-north-east .tip-arrow,
|
||||
.tip-north-west .tip-arrow {
|
||||
top: 0px;
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
border-bottom-style: solid;
|
||||
border-top: none;
|
||||
border-left-color: transparent;
|
||||
border-right-color: transparent
|
||||
}
|
||||
|
||||
.tip-south .tip-arrow,
|
||||
.tip-south-east .tip-arrow,
|
||||
.tip-south-west .tip-arrow {
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
border-top-style: solid;
|
||||
border-bottom: none;
|
||||
border-left-color: transparent;
|
||||
border-right-color: transparent
|
||||
}
|
||||
|
||||
.tip-east .tip-arrow {
|
||||
right: 0;
|
||||
top: 50%;
|
||||
margin-top: -5px;
|
||||
border-left-style: solid;
|
||||
border-right: none;
|
||||
border-top-color: transparent;
|
||||
border-bottom-color: transparent
|
||||
}
|
||||
|
||||
.tip-west .tip-arrow {
|
||||
left: 0;
|
||||
top: 50%;
|
||||
margin-top: -5px;
|
||||
border-right-style: solid;
|
||||
border-left: none;
|
||||
border-top-color: transparent;
|
||||
border-bottom-color: transparent
|
||||
}
|
||||
|
||||
.tip-north-west .tip-arrow,
|
||||
.tip-south-west .tip-arrow {
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
.tip-north-east .tip-arrow,
|
||||
.tip-south-east .tip-arrow {
|
||||
left: 85%;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Source Sans Pro'), local('SourceSansPro-Regular'),
|
||||
url('/fonts/SourceSansPro-Regular.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Source Sans Pro Semibold'), local('SourceSansPro-Semibold'),
|
||||
url('/fonts/SourceSansPro-Semibold.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'),
|
||||
url('/fonts/SourceSansPro-Bold.woff') format('woff');
|
||||
}
|
||||
body {
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
font-family: 'Source Sans Pro', sans-serif;
|
||||
padding: 100px;
|
||||
color: #64584c;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
ul li {
|
||||
padding: 0;
|
||||
}
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.box {
|
||||
background: #fff;
|
||||
box-shadow: 2px 4px 6px rgba(0,0,0,0.2);
|
||||
}
|
||||
.box.generic,
|
||||
.box.info,
|
||||
.box.error,
|
||||
.box.success {
|
||||
border-top: 4px solid #eac85d;
|
||||
padding: 20px;
|
||||
width: 50%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.box.info {
|
||||
border-top-color: #5f90b0;
|
||||
}
|
||||
.box.error {
|
||||
border-top-color: #e45e39;
|
||||
}
|
||||
.box.success {
|
||||
border-top-color: #4db07a;
|
||||
}
|
||||
.box a {
|
||||
color: #64584c;
|
||||
}
|
||||
.box h1 {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
color: #64584c;
|
||||
font-size: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
#graph {
|
||||
height: 200px;
|
||||
position: relative;
|
||||
}
|
||||
#graph #tooltip {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
#graph svg path.line {
|
||||
fill: none;
|
||||
stroke-width: 1px;
|
||||
clip-path: url("app/styles/#clip");
|
||||
}
|
||||
#graph svg path.line.actual {
|
||||
stroke: #64584c;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
#graph svg path.line.ideal {
|
||||
stroke: #cacaca;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
#graph svg path.line.trendline {
|
||||
stroke: #64584c;
|
||||
stroke-width: 1.5px;
|
||||
stroke-dasharray: 5, 5;
|
||||
}
|
||||
#graph svg line.today {
|
||||
stroke: #cacaca;
|
||||
stroke-width: 1px;
|
||||
shape-rendering: crispEdges;
|
||||
stroke-dasharray: 5, 5;
|
||||
}
|
||||
#graph svg circle {
|
||||
fill: #64584c;
|
||||
stroke: transparent;
|
||||
stroke-width: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#graph svg .axis {
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
#graph svg .axis line {
|
||||
stroke: rgba(202,202,202,0.25);
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
#graph svg .axis text {
|
||||
font-weight: bold;
|
||||
fill: #cacaca;
|
||||
}
|
||||
#graph svg .axis path {
|
||||
display: none;
|
||||
}
|
||||
#progress {
|
||||
padding: 20px;
|
||||
border-radius: 0 0 6px 6px;
|
||||
}
|
||||
#progress:after {
|
||||
clear: both;
|
||||
display: block;
|
||||
content: "";
|
||||
}
|
||||
#progress .bars {
|
||||
position: relative;
|
||||
}
|
||||
#progress .bars div {
|
||||
border-radius: 6px;
|
||||
height: 12px;
|
||||
}
|
||||
#progress .bars div.closed {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: #4daf7c;
|
||||
}
|
||||
#progress .bars div.closed:not(.done) {
|
||||
border-radius: 6px 0 0 6px;
|
||||
}
|
||||
#progress .bars div.opened {
|
||||
width: 100%;
|
||||
background: #e55f3a;
|
||||
}
|
||||
#progress h2 {
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
#progress h2.closed {
|
||||
float: left;
|
||||
color: #4daf7c;
|
||||
}
|
||||
#progress h2.opened {
|
||||
float: right;
|
||||
color: #e55f3a;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +0,0 @@
|
|||
github_user: 'intermine'
|
||||
github_project: 'intermine'
|
||||
project_name: 'Core InterMine Project'
|
||||
weekend: [ 6, 7 ]
|
||||
base_url: 'github-burndown-chart'
|
||||
api_token: false
|
BIN
example.png
BIN
example.png
Binary file not shown.
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 31 KiB |
37
package.json
37
package.json
|
@ -1,15 +1,38 @@
|
|||
{
|
||||
"name": "github-burndown-chart",
|
||||
"version": "0.2.1",
|
||||
"version": "1.0.0-alpha",
|
||||
"description": "Shows a burndown chart for GitHub Issues",
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"dependencies": {
|
||||
"coffee-script": "~1.6.3",
|
||||
"flatiron": "~0.3.8",
|
||||
"union": "~0.3.7",
|
||||
"connect": "~2.8.4",
|
||||
"eco": "~1.1.0-rc-3",
|
||||
"js-yaml": "~2.1.0"
|
||||
"async": "~0.2.9",
|
||||
"proxyquire": "~0.5.1",
|
||||
"lodash": "~1.3.1",
|
||||
"connect": "~2.8.5",
|
||||
"request": "~2.27.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "~1.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "start.js"
|
||||
"test": "make test"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/radekstepan/github-burndown-chart.git"
|
||||
},
|
||||
"keywords": [
|
||||
"github",
|
||||
"issues",
|
||||
"burndown",
|
||||
"chart",
|
||||
"scrum"
|
||||
],
|
||||
"author": "Radek <dev@radekstepan.com>",
|
||||
"license": "BSD",
|
||||
"bugs": {
|
||||
"url": "https://github.com/radekstepan/github-burndown-chart/issues"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env coffee
|
||||
{ _ } = require 'lodash'
|
||||
http = require 'http'
|
||||
fs = require 'fs'
|
||||
connect = require 'connect'
|
||||
request = require 'request'
|
||||
|
||||
# Read the original config.
|
||||
config = JSON.parse fs.readFileSync './config.json', 'utf-8'
|
||||
# Some defaults.
|
||||
config.host ?= 'api.github.com'
|
||||
# This is the scrubbed version.
|
||||
_.extend scrubbed = {}, config, { 'protocol': 'http', 'token': null }
|
||||
|
||||
proxy = (req, res, next) ->
|
||||
write = (code, body) ->
|
||||
res.writeHead code, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
res.end body
|
||||
|
||||
# Config?
|
||||
if req.url is '/config.json'
|
||||
# Refer to us like so.
|
||||
scrubbed.host = req.headers.host
|
||||
return write 200, JSON.stringify scrubbed, null, 4
|
||||
|
||||
# API request?
|
||||
if req.url.match /^\/repos/
|
||||
# The new headers.
|
||||
headers = 'Accept': 'application/vnd.github.raw'
|
||||
# Add a token?
|
||||
headers.Authorization = 'token ' + config.token if config.token
|
||||
# Make the HTTPS request.
|
||||
return request {
|
||||
'uri': 'https://' + config.host + req.url
|
||||
headers
|
||||
}, (_err, _res, body) ->
|
||||
return write(500) if _err
|
||||
write _res.statusCode, body
|
||||
|
||||
# Get handled by Connect.
|
||||
next()
|
||||
|
||||
app = connect()
|
||||
.use(proxy)
|
||||
.use(connect.static(__dirname + '/public'))
|
||||
.listen process.env.PORT
|
|
@ -0,0 +1 @@
|
|||
../build/build.css
|
|
@ -0,0 +1 @@
|
|||
../build/build.js
|
File diff suppressed because one or more lines are too long
|
@ -1,231 +0,0 @@
|
|||
.rickshaw_graph .detail {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
transition: opacity 0.25s linear;
|
||||
-moz-transition: opacity 0.25s linear;
|
||||
-o-transition: opacity 0.25s linear;
|
||||
-webkit-transition: opacity 0.25s linear;
|
||||
}
|
||||
.rickshaw_graph .detail.inactive {
|
||||
opacity: 0;
|
||||
}
|
||||
.rickshaw_graph .detail .item.active {
|
||||
opacity: 1;
|
||||
}
|
||||
.rickshaw_graph .detail .x_label {
|
||||
font-family: Arial, sans-serif;
|
||||
border-radius: 3px;
|
||||
padding: 6px;
|
||||
opacity: 0.5;
|
||||
border: 1px solid #e0e0e0;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
background: white;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.rickshaw_graph .detail .item {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
border-radius: 3px;
|
||||
padding: 0.25em;
|
||||
font-size: 12px;
|
||||
font-family: Arial, sans-serif;
|
||||
opacity: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
color: white;
|
||||
border: 1px solid rgba(0, 0, 0, 0.4);
|
||||
margin-left: 1em;
|
||||
margin-top: -1em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.rickshaw_graph .detail .item.active {
|
||||
opacity: 1;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
.rickshaw_graph .detail .item:before {
|
||||
content: "\25c2";
|
||||
position: absolute;
|
||||
left: -0.5em;
|
||||
color: rgba(0, 0, 0, 0.7);
|
||||
width: 0;
|
||||
}
|
||||
.rickshaw_graph .detail .dot {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
margin-left: -4px;
|
||||
margin-top: -3px;
|
||||
border-radius: 5px;
|
||||
position: absolute;
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
|
||||
background: white;
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
display: none;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
.rickshaw_graph .detail .dot.active {
|
||||
display: block;
|
||||
}
|
||||
.rickshaw_graph .detail ul {
|
||||
list-style-type: none;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
/* graph */
|
||||
|
||||
.rickshaw_graph {
|
||||
position: relative;
|
||||
}
|
||||
.rickshaw_graph svg {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ticks */
|
||||
|
||||
.rickshaw_graph .x_tick {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 0px;
|
||||
border-left: 1px dotted rgba(0, 0, 0, 0.2);
|
||||
pointer-events: none;
|
||||
}
|
||||
.rickshaw_graph .x_tick .title {
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
font-family: Arial, sans-serif;
|
||||
opacity: 0.5;
|
||||
white-space: nowrap;
|
||||
margin-left: 3px;
|
||||
bottom: 1px;
|
||||
}
|
||||
|
||||
/* annotations */
|
||||
|
||||
.rickshaw_annotation_timeline {
|
||||
height: 1px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
margin-top: 10px;
|
||||
position: relative;
|
||||
}
|
||||
.rickshaw_annotation_timeline .annotation {
|
||||
position: absolute;
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
margin-left: -2px;
|
||||
top: -3px;
|
||||
border-radius: 5px;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.rickshaw_graph .annotation_line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: -6px;
|
||||
width: 0px;
|
||||
border-left: 2px solid rgba(0, 0, 0, 0.3);
|
||||
display: none;
|
||||
}
|
||||
.rickshaw_graph .annotation_line.active {
|
||||
display: block;
|
||||
}
|
||||
.rickshaw_annotation_timeline .annotation .content {
|
||||
background: white;
|
||||
color: black;
|
||||
opacity: 0.9;
|
||||
padding: 5px 5px;
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.8);
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
font-size: 12px;
|
||||
padding: 6px 8px 8px;
|
||||
top: 18px;
|
||||
left: -11px;
|
||||
width: 160px;
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.rickshaw_annotation_timeline .annotation .content:before {
|
||||
content: "\25b2";
|
||||
position: absolute;
|
||||
top: -11px;
|
||||
color: white;
|
||||
text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
.rickshaw_annotation_timeline .annotation.active,
|
||||
.rickshaw_annotation_timeline .annotation:hover {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
cursor: none;
|
||||
}
|
||||
.rickshaw_annotation_timeline .annotation .content:hover {
|
||||
z-index: 50;
|
||||
}
|
||||
.rickshaw_annotation_timeline .annotation.active .content {
|
||||
display: block;
|
||||
}
|
||||
.rickshaw_annotation_timeline .annotation:hover .content {
|
||||
display: block;
|
||||
z-index: 50;
|
||||
}
|
||||
.rickshaw_graph .y_axis {
|
||||
fill: none;
|
||||
}
|
||||
.rickshaw_graph .y_ticks .tick {
|
||||
stroke: rgba(0, 0, 0, 0.16);
|
||||
stroke-width: 2px;
|
||||
shape-rendering: crisp-edges;
|
||||
pointer-events: none;
|
||||
}
|
||||
.rickshaw_graph .y_grid .tick {
|
||||
z-index: -1;
|
||||
stroke: rgba(0, 0, 0, 0.20);
|
||||
stroke-width: 1px;
|
||||
stroke-dasharray: 1 1;
|
||||
}
|
||||
.rickshaw_graph .y_grid path {
|
||||
fill: none;
|
||||
stroke: none;
|
||||
}
|
||||
.rickshaw_graph .y_ticks path {
|
||||
fill: none;
|
||||
stroke: #808080;
|
||||
}
|
||||
.rickshaw_graph .y_ticks text {
|
||||
opacity: 0.5;
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.rickshaw_graph .x_tick.glow .title,
|
||||
.rickshaw_graph .y_ticks.glow text {
|
||||
fill: black;
|
||||
color: black;
|
||||
text-shadow:
|
||||
-1px 1px 0 rgba(255, 255, 255, 0.1),
|
||||
1px -1px 0 rgba(255, 255, 255, 0.1),
|
||||
1px 1px 0 rgba(255, 255, 255, 0.1),
|
||||
0px 1px 0 rgba(255, 255, 255, 0.1),
|
||||
0px -1px 0 rgba(255, 255, 255, 0.1),
|
||||
1px 0px 0 rgba(255, 255, 255, 0.1),
|
||||
-1px 0px 0 rgba(255, 255, 255, 0.1),
|
||||
-1px -1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.rickshaw_graph .x_tick.inverse .title,
|
||||
.rickshaw_graph .y_ticks.inverse text {
|
||||
fill: white;
|
||||
color: white;
|
||||
text-shadow:
|
||||
-1px 1px 0 rgba(0, 0, 0, 0.8),
|
||||
1px -1px 0 rgba(0, 0, 0, 0.8),
|
||||
1px 1px 0 rgba(0, 0, 0, 0.8),
|
||||
0px 1px 0 rgba(0, 0, 0, 0.8),
|
||||
0px -1px 0 rgba(0, 0, 0, 0.8),
|
||||
1px 0px 0 rgba(0, 0, 0, 0.8),
|
||||
-1px 0px 0 rgba(0, 0, 0, 0.8),
|
||||
-1px -1px 0 rgba(0, 0, 0, 0.8);
|
||||
}
|
|
@ -1,209 +0,0 @@
|
|||
@font-face {
|
||||
font-family: 'OpenSansLight';
|
||||
src: url('../font/OpenSans-Light-webfont.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'OpenSansRegular';
|
||||
src: url('../font/OpenSans-Regular-webfont.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #fff;
|
||||
background-image: url("../img/colbg.jpg");
|
||||
background-repeat: repeat-x;
|
||||
background-position: 0px 40px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
body#landingBody {
|
||||
background-position: 0px 0px;
|
||||
}
|
||||
|
||||
.navbar .brand i {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.navbar .brand {
|
||||
color: #fff;
|
||||
-webkit-transition: color .2s ease-in-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.navbar .brand:hover {
|
||||
color: rgba(0,215,249,1);
|
||||
}
|
||||
|
||||
.navbar .nav a:hover {
|
||||
-webkit-transition: color .3s ease-in-out;
|
||||
}
|
||||
|
||||
.navbar .navbar-inner {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.navbar.blue .navbar-inner {
|
||||
background: #0956ae;
|
||||
background: -moz-linear-gradient(top, #0956ae 0%, #024a9e 100%);
|
||||
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#0956ae), color-stop(100%,#024a9e));
|
||||
background: -webkit-linear-gradient(top, #0956ae 0%,#024a9e 100%);
|
||||
background: -o-linear-gradient(top, #0956ae 0%,#024a9e 100%);
|
||||
background: -ms-linear-gradient(top, #0956ae 0%,#024a9e 100%);
|
||||
background: linear-gradient(top, #0956ae 0%,#024a9e 100%);
|
||||
}
|
||||
|
||||
.navbar.blue .nav > li > a {
|
||||
color: #fff;
|
||||
opacity: 0.8;
|
||||
-webkit-transition: opacity .3s ease-in-out;
|
||||
}
|
||||
|
||||
.navbar.blue .nav > li > a:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.navbar.blue .nav > .active > a {
|
||||
opacity: 1;
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.navbar .nav i {
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.navbar .nav:last-child i {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.badge-nav {
|
||||
top: 4px;
|
||||
background-color: #FFF;
|
||||
color: #444;
|
||||
-webkit-border-radius: 1px;
|
||||
-moz-border-radius: 1px;
|
||||
border-radius: 1px;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.sideBar {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.sideBar ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sideBar ul > li {
|
||||
font-family: "OpenSansLight";
|
||||
color: #222;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.sideBar ul > li figure {
|
||||
margin: 0;
|
||||
padding: 6px 14px 6px 14px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.sideBar ul > li figure:hover {
|
||||
background-color: #F9F9F9;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sideBar ul > li.active figure:first-child {
|
||||
color: #00AFDB;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sideBar ul.subSide {
|
||||
padding: 5px 0px 5px 0px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sideBar ul.subSide li {
|
||||
font-size: 15px;
|
||||
line-height: 17px;
|
||||
margin-bottom: 5px;
|
||||
padding: 4px 10px 5px 14px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.sideBar ul.subSide li:hover {
|
||||
background-color: rgba(0,175,219,0.1);
|
||||
}
|
||||
|
||||
.sideBar ul li i {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.sideBar ul > li > figure .badge {
|
||||
top: -2px;
|
||||
position: relative;
|
||||
font-family: "OpenSansRegular";
|
||||
-webkit-border-radius: 1px;
|
||||
-moz-border-radius: 1px;
|
||||
border-radius: 1px;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.borBox {
|
||||
-webkit-box-sizing: border-box;
|
||||
-ms-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-o-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pad40 {
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 7px 0px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.btn-sharp {
|
||||
-webkit-border-radius: 0px;
|
||||
-moz-border-radius: 0px;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.sharp .btn:first-child {
|
||||
-webkit-border-radius: 1px;
|
||||
-moz-border-radius: 1px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.sharp .btn:last-child {
|
||||
-webkit-border-radius: 1px;
|
||||
-moz-border-radius: 1px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.btn-dashboard {
|
||||
padding: 20px;
|
||||
height: 100px;
|
||||
-webkit-border-radius: 2px;
|
||||
-moz-border-radius: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.tLeft {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tRight {
|
||||
text-align: right;
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 38 KiB |
Binary file not shown.
Before Width: | Height: | Size: 8.6 KiB |
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,19 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>GitHub Burndown Chart</title>
|
||||
|
||||
<link href="build.css" media="all" rel="stylesheet" type="text/css" />
|
||||
<script src="build.js"></script>
|
||||
<script>
|
||||
document.onreadystatechange = function() {
|
||||
if (document.readyState == "complete") {
|
||||
require('app').call(null);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/env coffee
|
||||
async = require 'async'
|
||||
{ _ } = require 'lodash'
|
||||
|
||||
config = require './modules/config'
|
||||
regex = require './modules/regex'
|
||||
render = require './modules/render'
|
||||
repo = require './modules/repo'
|
||||
|
||||
# Check for a route.
|
||||
route = ->
|
||||
# Do we have a location match?
|
||||
if match = window.location.hash.match regex.location
|
||||
# Get the user/repo pair then.
|
||||
path = match[1..3].join('/')
|
||||
|
||||
# Say we are loading this repo then.
|
||||
render 'body', 'loading', { path }
|
||||
|
||||
# Get config/cache.
|
||||
return async.waterfall [ config
|
||||
# Render this repo.
|
||||
, (conf, cb) ->
|
||||
repo _.extend({ path }, conf), cb
|
||||
], (err) ->
|
||||
render 'body', 'error', { 'text': err.toString() } if err
|
||||
|
||||
# Info notice for you.
|
||||
render 'body', 'info'
|
||||
|
||||
module.exports = ->
|
||||
# Do we have browser support?
|
||||
if 'onhashchange' of window and 'hash' of window.location
|
||||
# Detect route changes.
|
||||
window.addEventListener 'hashchange', route, no
|
||||
# And route now.
|
||||
return do route
|
||||
|
||||
render 'body', 'error', { 'text': 'URL fragment identifier not supported' }
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "app",
|
||||
"main": "app.js",
|
||||
"version": "1.0.0-alpha",
|
||||
"dependencies": {
|
||||
"bestiejs/lodash": "*",
|
||||
"caolan/async": "*",
|
||||
"mbostock/d3": "*",
|
||||
"visionmedia/superagent": "*",
|
||||
"necolas/normalize.css": "*",
|
||||
"component/tip": "*",
|
||||
"component/aurora-tip": "*"
|
||||
},
|
||||
"scripts": [
|
||||
"app.coffee",
|
||||
|
||||
"modules/config.coffee",
|
||||
"modules/graph.coffee",
|
||||
"modules/issues.coffee",
|
||||
"modules/milestones.coffee",
|
||||
"modules/regex.coffee",
|
||||
"modules/request.coffee",
|
||||
"modules/render.coffee",
|
||||
"modules/repo.coffee",
|
||||
|
||||
"templates/error.eco",
|
||||
"templates/graph.eco",
|
||||
"templates/info.eco",
|
||||
"templates/label.eco",
|
||||
"templates/loading.eco",
|
||||
"templates/progress.eco"
|
||||
],
|
||||
"styles": [
|
||||
"styles/fonts.css",
|
||||
"styles/app.styl"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
#!/usr/bin/env coffee
|
||||
{ _ } = require 'lodash'
|
||||
|
||||
request = require './request'
|
||||
regex = require './regex'
|
||||
|
||||
# Have it?
|
||||
config = null
|
||||
# We are cold.
|
||||
wait = no
|
||||
# Callbacks go here.
|
||||
queue = []
|
||||
|
||||
# Defaults.
|
||||
defaults =
|
||||
# You do know we work with GitHub right?
|
||||
'host': 'api.github.com'
|
||||
# Making NSA (err taxpayer) work for it.
|
||||
'protocol': 'https'
|
||||
|
||||
# Validators of config fields.
|
||||
validators =
|
||||
'host': (value) ->
|
||||
_.isString value
|
||||
'protocol': (value) ->
|
||||
_.isString(value) and value.match /^http(s?)$/
|
||||
'token': (value) ->
|
||||
_.isString value
|
||||
'off_days': (value) ->
|
||||
return no unless _.isArray value
|
||||
( return no for day in value when day not in [ 1..7 ] )
|
||||
yes
|
||||
|
||||
# Get (& cache) configuration from the server.
|
||||
module.exports = (cb) ->
|
||||
# Have config?
|
||||
return cb null, config if config
|
||||
# Enqueue.
|
||||
queue.push cb
|
||||
# Load it?
|
||||
unless wait
|
||||
# Everyone else wait now.
|
||||
wait = yes
|
||||
# Make the request.
|
||||
request.config (err, result) ->
|
||||
# We do not strictly require config files.
|
||||
config = ( if err then { } else result )
|
||||
|
||||
# Tack on defaults?
|
||||
( config[k] ?= v for k, v of defaults )
|
||||
|
||||
# RegExpify the size label?
|
||||
if config.size_label
|
||||
config.size_label = new RegExp config.size_label
|
||||
else
|
||||
config.size_label = regex.size_label
|
||||
|
||||
# Validate it.
|
||||
for field, validator of validators when config[field]
|
||||
unless validator config[field]
|
||||
return cb "Config field `#{field}` misconfigured"
|
||||
|
||||
# Call back for each enqueued.
|
||||
_.each queue, (cb) ->
|
||||
cb null, config
|
|
@ -0,0 +1,247 @@
|
|||
#!/usr/bin/env coffee
|
||||
{ _ } = require 'lodash'
|
||||
d3 = require 'd3'
|
||||
Tip = require 'tip'
|
||||
|
||||
reg = require './regex'
|
||||
|
||||
module.exports =
|
||||
|
||||
# A graph of closed issues.
|
||||
'actual': (collection, created_at, total, cb) ->
|
||||
head = [ {
|
||||
date: new Date(created_at)
|
||||
points: total
|
||||
} ]
|
||||
|
||||
min = +Infinity ; max = -Infinity
|
||||
|
||||
# Generate the actual closes.
|
||||
rest = _.map collection, (issue) ->
|
||||
{ size, closed_at } = issue
|
||||
# Determine the range.
|
||||
min = size if size < min
|
||||
max = size if size > max
|
||||
|
||||
# Dropping points remaining.
|
||||
_.extend {}, issue,
|
||||
date: new Date(closed_at)
|
||||
points: total -= size
|
||||
|
||||
# Now add a radius in a range (will be used for a circle).
|
||||
range = d3.scale.linear().domain([ min, max ]).range([ 5, 8 ])
|
||||
|
||||
rest = _.map rest, (issue) ->
|
||||
issue.radius = range issue.size
|
||||
issue
|
||||
|
||||
cb null, [].concat head, rest
|
||||
|
||||
# A graph of an ideal progression..
|
||||
'ideal': (a, b, off_days, total, cb) ->
|
||||
# Swap?
|
||||
[ b, a ] = [ a, b ] if b < a
|
||||
|
||||
# We start here adding days to `d`.
|
||||
[ y, m, d ] = _.map a.match(reg.datetime)[1].split('-'), (v) -> parseInt v
|
||||
# We want to end here.
|
||||
cutoff = new Date(b)
|
||||
|
||||
# Go through the beginning to the end skipping off days.
|
||||
days = [] ; length = 0
|
||||
do once = (inc = 0) ->
|
||||
# A new day.
|
||||
day = new Date y, m - 1, d + inc
|
||||
|
||||
# Does this day count?
|
||||
day_of = 7 if !day_of = day.getDay()
|
||||
if day_of in off_days
|
||||
days.push { date: day, off_day: yes }
|
||||
else
|
||||
length += 1
|
||||
days.push { date: day }
|
||||
|
||||
# Go again?
|
||||
once(inc + 1) unless day > cutoff
|
||||
|
||||
# Map points on the array of days now.
|
||||
velocity = total / (length - 1)
|
||||
|
||||
days = _.map days, (day, i) ->
|
||||
day.points = total
|
||||
total -= velocity if days[i] and not days[i].off_day
|
||||
day
|
||||
|
||||
# Do we need to make a link to right now?
|
||||
days.push { date: now, points: 0 } if (now = new Date()) > cutoff
|
||||
|
||||
cb null, days
|
||||
|
||||
# Graph representing a trendling of actual issues.
|
||||
'trendline': (actual, created_at, due_on) ->
|
||||
start = +actual[0].date
|
||||
|
||||
# Values is a list of time from the start and points remaining.
|
||||
values = _.map actual, ({ date, points }) ->
|
||||
[ +date - start, points ]
|
||||
|
||||
# Now is an actual point too.
|
||||
last = actual[actual.length - 1]
|
||||
values.push [ + new Date() - start, last.points ]
|
||||
|
||||
# http://classroom.synonym.com/calculate-trendline-2709.html
|
||||
b1 = 0 ; e = 0 ; c1 = 0
|
||||
a = (l = values.length) * _.reduce(values, (sum, [ a, b ]) ->
|
||||
b1 += a ; e += b
|
||||
c1 += Math.pow(a, 2)
|
||||
sum + (a * b)
|
||||
, 0)
|
||||
|
||||
slope = (a - (b1 * e)) / ((l * c1) - (Math.pow(b1, 2)))
|
||||
intercept = (e - (slope * b1)) / l
|
||||
fn = (x) -> slope * x + intercept
|
||||
|
||||
a = +new Date(created_at) - start
|
||||
b = +new Date(due_on) - start
|
||||
|
||||
[
|
||||
{
|
||||
date: new Date(created_at)
|
||||
points: fn(a)
|
||||
}, {
|
||||
date: new Date(due_on)
|
||||
points: fn(b)
|
||||
}
|
||||
]
|
||||
|
||||
# The graph as a whole.
|
||||
'render': ([ actual, ideal, trendline ], cb) ->
|
||||
document.querySelector('#svg').innerHTML = ''
|
||||
|
||||
# Get available space.
|
||||
{ height, width } = document.querySelector('#graph').getBoundingClientRect()
|
||||
|
||||
margin = { top: 30, right: 30, bottom: 40, left: 50 }
|
||||
width -= margin.left + margin.right
|
||||
height -= margin.top + margin.bottom
|
||||
|
||||
# Scales.
|
||||
x = d3.time.scale().range([ 0, width ])
|
||||
y = d3.scale.linear().range([ height, 0 ])
|
||||
|
||||
# Axes.
|
||||
xAxis = d3.svg.axis().scale(x)
|
||||
.orient("bottom")
|
||||
# Show vertical lines...
|
||||
.tickSize(-height)
|
||||
# ...with day of the month...
|
||||
.tickFormat( (d) -> d.getDate() )
|
||||
# ...and give us a spacer.
|
||||
.tickPadding(10)
|
||||
|
||||
yAxis = d3.svg.axis().scale(y)
|
||||
.orient("left")
|
||||
.tickSize(-width)
|
||||
.ticks(5)
|
||||
.tickPadding(10)
|
||||
|
||||
# Line generator.
|
||||
line = d3.svg.line()
|
||||
.interpolate("linear")
|
||||
.x( (d) -> x(d.date) )
|
||||
.y( (d) -> y(d.points) )
|
||||
|
||||
# Get the minimum and maximum date, and initial points.
|
||||
x.domain([ ideal[0].date, ideal[ideal.length - 1].date ])
|
||||
y.domain([ 0, ideal[0].points ]).nice()
|
||||
|
||||
# Add an SVG element with the desired dimensions and margin.
|
||||
svg = d3.select("#svg").append("svg")
|
||||
.attr("width", width + margin.left + margin.right)
|
||||
.attr("height", height + margin.top + margin.bottom)
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
|
||||
|
||||
# Add the days x-axis.
|
||||
svg.append("g")
|
||||
.attr("class", "x axis day")
|
||||
.attr("transform", "translate(0,#{height})")
|
||||
.call(xAxis)
|
||||
|
||||
# Add the months x-axis.
|
||||
m = [
|
||||
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
||||
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
|
||||
]
|
||||
|
||||
mAxis = xAxis
|
||||
.orient("top")
|
||||
.tickSize(height)
|
||||
.tickFormat( (d) -> m[d.getMonth()] )
|
||||
.ticks(2)
|
||||
|
||||
svg.append("g")
|
||||
.attr("class", "x axis month")
|
||||
.attr("transform", "translate(0,#{height})")
|
||||
.call(mAxis)
|
||||
|
||||
# Add the y-axis.
|
||||
svg.append("g")
|
||||
.attr("class", "y axis")
|
||||
.call(yAxis)
|
||||
|
||||
# Add a line showing where we are now.
|
||||
svg.append("svg:line")
|
||||
.attr("class", "today")
|
||||
.attr("x1", x(new Date()))
|
||||
.attr("y1", 0)
|
||||
.attr("x2", x(new Date()))
|
||||
.attr("y2", height)
|
||||
|
||||
# Add the ideal line path.
|
||||
svg.append("path")
|
||||
.attr("class", "ideal line")
|
||||
.attr("d", line.interpolate("basis")(ideal))
|
||||
|
||||
# Add the trendline path.
|
||||
svg.append("path")
|
||||
.attr("class", "trendline line")
|
||||
.attr("d", line.interpolate("linear")(trendline))
|
||||
|
||||
# Add the actual line path.
|
||||
svg.append("path")
|
||||
.attr("class", "actual line")
|
||||
.attr("d", line.interpolate("linear").y( (d) -> y(d.points) )(actual))
|
||||
|
||||
# Collect the tooltip here.
|
||||
tooltip = null
|
||||
|
||||
# Show when we closed an issue.
|
||||
svg.selectAll("a.issue")
|
||||
.data(actual[1...]) # skip the starting point
|
||||
.enter()
|
||||
|
||||
# A wrapping link.
|
||||
.append('svg:a')
|
||||
.attr("xlink:href", ({ html_url }) -> html_url )
|
||||
.attr("xlink:show", 'new')
|
||||
.append('svg:circle')
|
||||
.attr("cx", ({ date }) -> x date )
|
||||
.attr("cy", ({ points }) -> y points )
|
||||
.attr("r", ({ radius }) -> 5 ) # fixed for now
|
||||
.on('mouseover', ({ date, points, title, number }) ->
|
||||
# Pass a title string.
|
||||
tooltip = new Tip "##{number}: #{title}"
|
||||
# Absolutely position the div.
|
||||
div = document.querySelector '#tooltip'
|
||||
div.style.left = x(date) + margin.left + 'px'
|
||||
div.style.top = -10 + y(points) + margin.top + 'px'
|
||||
# And now show us on the div.
|
||||
tooltip.show '#tooltip'
|
||||
)
|
||||
.on('mouseout', (d) ->
|
||||
# Hide after a time has passed if exists.
|
||||
tooltip?.hide(200)
|
||||
)
|
||||
|
||||
cb null
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env coffee
|
||||
{ _ } = require 'lodash'
|
||||
async = require 'async'
|
||||
|
||||
req = require './request'
|
||||
reg = require './regex'
|
||||
|
||||
module.exports =
|
||||
|
||||
# Used on an initial fetch of issues for a repo.
|
||||
'get_all': (repo, cb) ->
|
||||
# For each state...
|
||||
one_status = (state, cb) ->
|
||||
# Concat them here.
|
||||
results = []
|
||||
# One pageful fetch (next pages in series).
|
||||
do fetch_page = (page = 1) ->
|
||||
req.all_issues repo, {
|
||||
milestone: repo.milestone.number
|
||||
state
|
||||
page
|
||||
}, (err, data) ->
|
||||
# Request errors.
|
||||
return cb err if err
|
||||
# GitHub errors.
|
||||
return cb data.message if data.message
|
||||
# Empty?
|
||||
return cb null, results unless data.length
|
||||
# Concat sorted (API does not sort on closed_at!).
|
||||
results = results.concat _.sortBy data, 'closed_at'
|
||||
# < 100 results?
|
||||
return cb null, results if data.length < 100
|
||||
# Fetch the next page then.
|
||||
fetch_page page + 1
|
||||
|
||||
# For each `open` and `closed` issues in parallel.
|
||||
async.parallel [
|
||||
_.partial one_status, 'open'
|
||||
_.partial one_status, 'closed'
|
||||
], cb
|
||||
|
||||
# Filter an array of incoming issues based on a regex & save size on them.
|
||||
'filter': (collection, regex, cb) ->
|
||||
warnings = null ; total = 0
|
||||
try
|
||||
filtered = _.filter collection, (issue) ->
|
||||
{ labels, number } = issue
|
||||
number ?= '?'
|
||||
return false unless labels
|
||||
switch ( {} for { name } in labels when name and regex.test(name) ).length
|
||||
when 0 then false
|
||||
when 1
|
||||
# Provide the size attribute on the issue.
|
||||
total += issue.size = parseInt name.match(regex)[1]
|
||||
true
|
||||
else
|
||||
warnings ?= []
|
||||
warnings.push "Issue ##{number} has multiple matching size labels"
|
||||
true
|
||||
|
||||
cb null, warnings, filtered, total
|
||||
|
||||
catch err
|
||||
return cb err, warnings
|
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env coffee
|
||||
request = require './request'
|
||||
|
||||
module.exports =
|
||||
|
||||
# Get current milestones for a repo..
|
||||
'get_current': (repo, cb) ->
|
||||
request.all_milestones repo, (err, data) ->
|
||||
# Request errors?
|
||||
return cb err if err
|
||||
# GitHub errors?
|
||||
return cb data.message if data.message
|
||||
# Empty warning?
|
||||
return cb null, 'No open milestones for repo' unless data.length
|
||||
# The first milestone should be ending soonest.
|
||||
m = data[0]
|
||||
# Empty milestone?
|
||||
return cb null, 'No issues for milestone' if m.open_issues + m.closed_issues is 0
|
||||
|
||||
cb null, null, m
|
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/env coffee
|
||||
module.exports =
|
||||
# How do we parse GitHub dates?
|
||||
'datetime': /^(\d{4}-\d{2}-\d{2})T(.*)/
|
||||
# How does a size label look like?
|
||||
'size_label': /^size (\d+)$/
|
||||
# How do we specify which user/repo we want?
|
||||
'location': /^#!\/(.+)\/(.+)$/
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env coffee
|
||||
|
||||
# Render an eco template into a selector (innerHTML).
|
||||
module.exports = (selector, template, context = {}) ->
|
||||
tml = require "../templates/#{template}"
|
||||
document.querySelector(selector).innerHTML = tml context
|
|
@ -0,0 +1,80 @@
|
|||
#!/usr/bin/env coffee
|
||||
{ _ } = require 'lodash'
|
||||
async = require 'async'
|
||||
|
||||
milestones = require './milestones'
|
||||
issues = require './issues'
|
||||
graph = require './graph'
|
||||
regex = require './regex'
|
||||
render = require './render'
|
||||
|
||||
# Setup a repo and render it.
|
||||
module.exports = (opts, cb) ->
|
||||
|
||||
# Get the current milestone.
|
||||
async.waterfall [ (cb) ->
|
||||
milestones.get_current opts, (err, warn, milestone) ->
|
||||
return cb err if err
|
||||
return cb warn if warn
|
||||
opts.milestone = milestone
|
||||
cb null
|
||||
|
||||
# Get all issues.
|
||||
(cb) ->
|
||||
issues.get_all opts, cb
|
||||
|
||||
# Filter them to labeled ones.
|
||||
(all, cb) ->
|
||||
async.map all, (array, cb) ->
|
||||
issues.filter array, opts.size_label, (err, warn, filtered, total) ->
|
||||
cb err, [ filtered, total ]
|
||||
, (err, [ open, closed ]) ->
|
||||
return cb err if err
|
||||
# Empty?
|
||||
return cb 'No matching issues found' if open[1] + closed[1] is 0
|
||||
# Save the open/closed on us first.
|
||||
opts.issues =
|
||||
closed: { points: closed[1], data: closed[0] }
|
||||
open: { points: open[1], data: open[0] }
|
||||
cb null
|
||||
|
||||
# Create actual and ideal lines & render.
|
||||
(cb) ->
|
||||
progress = 100 * opts.issues.closed.points /
|
||||
(total = opts.issues.open.points + opts.issues.closed.points)
|
||||
|
||||
async.parallel [
|
||||
_.partial(
|
||||
graph.actual,
|
||||
opts.issues.closed.data,
|
||||
opts.milestone.created_at,
|
||||
total
|
||||
)
|
||||
_.partial(
|
||||
graph.ideal,
|
||||
opts.milestone.created_at,
|
||||
opts.milestone.due_on,
|
||||
opts.off_days or [],
|
||||
total
|
||||
)
|
||||
], (err, values) ->
|
||||
# Render the body.
|
||||
render 'body', 'graph', name: opts.repo
|
||||
|
||||
# Render the progress.
|
||||
render '#progress', 'progress', { progress }
|
||||
|
||||
# Generate a trendline?
|
||||
values.push(graph.trendline(
|
||||
values[0],
|
||||
opts.milestone.created_at,
|
||||
opts.milestone.due_on
|
||||
)) if values[0].length
|
||||
|
||||
# Render the chart.
|
||||
do doit = -> graph.render values, cb
|
||||
|
||||
# Watch window resize from now on?
|
||||
window.onresize = doit if 'onresize' of window
|
||||
|
||||
], cb
|
|
@ -0,0 +1,56 @@
|
|||
#!/usr/bin/env coffee
|
||||
sa = require 'superagent'
|
||||
{ _ } = require 'lodash'
|
||||
|
||||
# Custom JSON parser.
|
||||
sa.parse =
|
||||
'application/json': (res) ->
|
||||
try
|
||||
JSON.parse res
|
||||
catch e
|
||||
{} # it was not to be...
|
||||
|
||||
module.exports =
|
||||
|
||||
# Get all milestones.
|
||||
'all_milestones': (repo, cb) ->
|
||||
query = { 'state': 'open', 'sort': 'due_date', 'direction': 'asc' }
|
||||
request repo, query, 'milestones', cb
|
||||
|
||||
# Get all issues for a state.
|
||||
'all_issues': (repo, query, cb) ->
|
||||
_.extend query, { 'per_page': '100' }
|
||||
request repo, query, 'issues', cb
|
||||
|
||||
# Get config from our host always.
|
||||
'config': (cb) ->
|
||||
sa
|
||||
.get("http://#{window.location.host}/config.json")
|
||||
.set('Content-Type', 'application/json')
|
||||
.end _.partialRight respond, cb
|
||||
|
||||
# Make a request using SuperAgent to GitHub.
|
||||
request = ({ protocol, host, token, path }, query, noun, cb) ->
|
||||
# Make the query params.
|
||||
q = ( "#{k}=#{v}" for k, v of query ).join('&')
|
||||
|
||||
req = sa
|
||||
# The URI.
|
||||
.get("#{protocol}://#{host}/repos/#{path}/#{noun}?#{q}")
|
||||
# The content type.
|
||||
.set('Content-Type', 'application/json')
|
||||
# The media type.
|
||||
.set('Accept', 'application/vnd.github.raw')
|
||||
|
||||
# Auth token?
|
||||
req = req.set('Authorization', "token #{token}") if token
|
||||
|
||||
# Send.
|
||||
req.end _.partialRight respond, cb
|
||||
|
||||
# How do we respond to a response?
|
||||
respond = (data, cb) ->
|
||||
# 2xx?
|
||||
return cb data.error.message if data.statusType isnt 2
|
||||
# All good.
|
||||
cb null, data?.body
|
|
@ -0,0 +1,166 @@
|
|||
// color definitions
|
||||
$closed = #4DAF7C
|
||||
$opened = #E55F3A
|
||||
$grey = #CACACA
|
||||
$brown = #64584C
|
||||
|
||||
// font and gradient bg
|
||||
body
|
||||
height: 100%
|
||||
background: linear-gradient(135deg, #d7bcab 0%, #cc9485 100%)
|
||||
background-repeat: no-repeat
|
||||
background-attachment: fixed
|
||||
font-family: 'Source Sans Pro', sans-serif
|
||||
padding: 100px
|
||||
color: $brown
|
||||
|
||||
ul
|
||||
list-style-type: none
|
||||
padding: 0
|
||||
|
||||
li
|
||||
padding: 0
|
||||
|
||||
h2
|
||||
font-size: 16px
|
||||
text-transform: uppercase
|
||||
|
||||
// the white content box
|
||||
.box
|
||||
background: #FFF
|
||||
box-shadow: 2px 4px 6px rgba(0,0,0,0.2)
|
||||
|
||||
// different classes thereof
|
||||
&.generic, &.info, &.error, &.success
|
||||
border-top: 4px solid #EAC85D
|
||||
padding: 20px
|
||||
width: 50%
|
||||
margin: 0 auto
|
||||
|
||||
&.info
|
||||
border-top-color: #5F90B0
|
||||
|
||||
&.error
|
||||
border-top-color: #E45E39
|
||||
|
||||
&.success
|
||||
border-top-color: #4DB07A
|
||||
|
||||
a
|
||||
color: $brown
|
||||
|
||||
h1
|
||||
margin: 0
|
||||
padding: 20px
|
||||
color: $brown
|
||||
font-size: 20px
|
||||
text-transform: uppercase
|
||||
|
||||
// where D3 renders to
|
||||
#graph
|
||||
height: 200px
|
||||
position: relative
|
||||
|
||||
// position will be adjusted dynamically
|
||||
#tooltip
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
svg
|
||||
path
|
||||
&.line
|
||||
fill: none
|
||||
stroke-width: 1px
|
||||
clip-path: url(#clip)
|
||||
|
||||
// actual progress
|
||||
&.actual
|
||||
stroke: $brown
|
||||
stroke-width: 3px
|
||||
|
||||
// ideal velocity throughout the sprint
|
||||
&.ideal
|
||||
stroke: $grey
|
||||
stroke-width: 3px
|
||||
|
||||
// trend of actual issue closures
|
||||
&.trendline
|
||||
stroke: $brown
|
||||
stroke-width: 1.5px
|
||||
stroke-dasharray: 5,5
|
||||
|
||||
// right now
|
||||
line
|
||||
&.today
|
||||
stroke: $grey
|
||||
stroke-width: 1px
|
||||
shape-rendering: crispEdges
|
||||
stroke-dasharray: 5,5
|
||||
|
||||
// represents one issue closed
|
||||
circle
|
||||
fill: $brown
|
||||
// make it easier to click
|
||||
stroke: transparent
|
||||
stroke-width: 15px
|
||||
cursor: pointer
|
||||
|
||||
// axes...
|
||||
.axis
|
||||
shape-rendering: crispEdges
|
||||
|
||||
line
|
||||
stroke: rgba($grey, 0.25)
|
||||
shape-rendering: crispEdges
|
||||
|
||||
text
|
||||
font-weight: bold
|
||||
fill: $grey
|
||||
|
||||
path
|
||||
display: none
|
||||
|
||||
// progression graph
|
||||
#progress
|
||||
padding: 20px
|
||||
border-radius: 0 0 6px 6px
|
||||
|
||||
// clear
|
||||
&:after
|
||||
clear: both
|
||||
display: block
|
||||
content: ""
|
||||
|
||||
.bars
|
||||
position: relative
|
||||
|
||||
// the two bars
|
||||
div
|
||||
border-radius: 6px
|
||||
height: 12px
|
||||
|
||||
&.closed
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
background: $closed
|
||||
|
||||
// when we have issues left
|
||||
&:not(.done)
|
||||
border-radius: 6px 0 0 6px
|
||||
|
||||
&.opened
|
||||
width: 100%
|
||||
background: $opened
|
||||
|
||||
h2
|
||||
margin: 10px 0 0 0
|
||||
|
||||
&.closed
|
||||
float: left
|
||||
color: $closed
|
||||
|
||||
&.opened
|
||||
float: right
|
||||
color: $opened
|
|
@ -0,0 +1,21 @@
|
|||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Source Sans Pro'), local('SourceSansPro-Regular'),
|
||||
url('/fonts/SourceSansPro-Regular.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Source Sans Pro Semibold'), local('SourceSansPro-Semibold'),
|
||||
url('/fonts/SourceSansPro-Semibold.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'),
|
||||
url('/fonts/SourceSansPro-Bold.woff') format('woff');
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<div class="box error">
|
||||
<h2>Trouble</h2>
|
||||
<p><%- @text %></p>
|
||||
</div>
|
|
@ -0,0 +1,8 @@
|
|||
<div class="box">
|
||||
<h1><%- @name %></h2>
|
||||
<div id="graph">
|
||||
<div id="tooltip"></div>
|
||||
<div id="svg"></div>
|
||||
</div>
|
||||
<div id="progress"></div>
|
||||
</div>
|
|
@ -0,0 +1,4 @@
|
|||
<div class="box info">
|
||||
<h2>GitHub Burndown Chart</h2>
|
||||
<p>Use your browser's location hash to specify a repo: <a href="#!/radekstepan/disposable">#!/radekstepan/disposable</a>.</p>
|
||||
</div>
|
|
@ -0,0 +1,10 @@
|
|||
<% points = Math.ceil @points %>
|
||||
<% if points > 1: %>
|
||||
<%- points %> points left
|
||||
<% else: %>
|
||||
<% if points is 1: %>
|
||||
1 point left
|
||||
<% else: %>
|
||||
Done
|
||||
<% end %>
|
||||
<% end %>
|
|
@ -0,0 +1,4 @@
|
|||
<div class="box generic">
|
||||
<h2>GitHub Burndown Chart</h2>
|
||||
<p>Loading <a href="#!/<%- @path %>">#!/<%- @path %></a>.</p>
|
||||
</div>
|
|
@ -0,0 +1,10 @@
|
|||
<div class="bars">
|
||||
<% if @progress is 100: %>
|
||||
<div class="closed done" style="width:100%"></div>
|
||||
<% else: %>
|
||||
<div class="closed" style="width:<%= @progress %>%"></div>
|
||||
<% end %>
|
||||
<div class="opened"></div>
|
||||
</div>
|
||||
<h2 class="closed">Closed / <%= Math.floor @progress %>%</h2>
|
||||
<h2 class="opened">Open / <%= 100 - Math.floor @progress %>%</h2>
|
3
start.js
3
start.js
|
@ -1,3 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
require('coffee-script');
|
||||
require('./app.coffee');
|
|
@ -1,162 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>Burndown App</title>
|
||||
|
||||
<base href="<%= @base_url %>"/>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="css/bootstrap.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/rickshaw.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/style.css">
|
||||
|
||||
<script src="js/d3.min.js"></script>
|
||||
<script src="js/d3.layout.min.js"></script>
|
||||
<script src="js/rickshaw.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="navbar blue navbar-fixed-top">
|
||||
<div class="navbar-inner">
|
||||
<div class="container-fluid">
|
||||
<ul class="nav pull-left">
|
||||
<li><a><i class="icon-white icon-fire"></i> Burndown App</a></li>
|
||||
</ul>
|
||||
<ul class="nav pull-right">
|
||||
<li><a><i class="icon-white icon-book"></i> <%= @project %></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="container-fluid pad40">
|
||||
<section class="row-fluid">
|
||||
<div class="span2 sideBar">
|
||||
<br>
|
||||
<ul>
|
||||
<li class="active">
|
||||
<figure>
|
||||
<a href="burndown"><i class="icon-signal"></i> Burndown Chart</a>
|
||||
</figure>
|
||||
</li>
|
||||
<li>
|
||||
<figure>
|
||||
<a href="issues"><i class="icon-tasks"></i> Issues</a>
|
||||
</figure>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<section class="span10 content borBox">
|
||||
<div class="row-fluid">
|
||||
<div class="page-header">
|
||||
<h1>Burndown chart <small>showing actual vs. ideal velocity on each day</small></h1>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<div class="row-fluid">
|
||||
<div class="span12">
|
||||
<div id="chart"></div>
|
||||
<div id="timeline"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var now = + new Date;
|
||||
|
||||
var ideal = [], actual = [], issues = {}, i = 0;
|
||||
<% for day, data of @days: %>
|
||||
ideal.push({ 'x': <%= day / 1000 %>, 'y': <%= data['ideal'] %> });
|
||||
actual.push({ 'x': <%= day / 1000 %>, 'y': <%= data['actual'] %> });
|
||||
|
||||
// Fill up issues object.
|
||||
<% if data['issues'].length > 0: %>
|
||||
issues[<%= day %>] = [];
|
||||
<% for issue in data['issues']: %>
|
||||
issues[<%= day %>].push('<%= escape issue['title'] %>');
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
i++;
|
||||
<% end %>
|
||||
|
||||
// Early exit to avoid Rickshaw error.
|
||||
if (actual.length == 0) return;
|
||||
|
||||
// The line graph.
|
||||
var graph = new Rickshaw.Graph({
|
||||
element: document.querySelector("#chart"),
|
||||
height: 600,
|
||||
renderer: 'line',
|
||||
series: [{
|
||||
data: ideal,
|
||||
color: '#75ABC5',
|
||||
name: 'Ideal'
|
||||
}, {
|
||||
data: actual,
|
||||
color: '#F89406',
|
||||
name: 'Actual'
|
||||
}]
|
||||
});
|
||||
|
||||
graph.render();
|
||||
|
||||
// Onhover.
|
||||
var hoverDetail = new Rickshaw.Graph.HoverDetail({
|
||||
graph: graph,
|
||||
xFormatter: function(d) { return new Date(d * 1000).toUTCString().substring(0, 11) },
|
||||
formatter: function(series, stamp, points) {
|
||||
if (series.name == 'Ideal') {
|
||||
return '<p>Ideally ' + Math.round(points) + ' points left</p>'
|
||||
} else {
|
||||
var left = '<p>' + Math.round(points) + ' points left</p>';
|
||||
// Have we closed any issues today?
|
||||
var iss = issues[stamp * 1000];
|
||||
if (iss) {
|
||||
var string = '<ul>';
|
||||
for (var i = 0; i < iss.length; i++) {
|
||||
string += '<li><span class="icon-ok icon-white"></span> ' + unescape(iss[i]) + '</li>';
|
||||
}
|
||||
string += '</ul>';
|
||||
return left + string;
|
||||
} else {
|
||||
return left;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Axes.
|
||||
var xAxis = new Rickshaw.Graph.Axis.Time({
|
||||
graph: graph,
|
||||
ticksTreatment: 'glow'
|
||||
});
|
||||
|
||||
var yAxis = new Rickshaw.Graph.Axis.Y({
|
||||
graph: graph,
|
||||
tickFormat: Rickshaw.Fixtures.Number.formatKMBT,
|
||||
ticksTreatment: 'glow'
|
||||
});
|
||||
|
||||
xAxis.render();
|
||||
yAxis.render();
|
||||
|
||||
// Annotations.
|
||||
var annotator = new Rickshaw.Graph.Annotate({
|
||||
graph: graph,
|
||||
element: document.getElementById('timeline')
|
||||
});
|
||||
|
||||
annotator.add(now / 1000, 'Now');
|
||||
annotator.update();
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -1,56 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>Burndown App</title>
|
||||
|
||||
<base href="<%= @base_url %>"/>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="css/bootstrap.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="navbar blue navbar-fixed-top">
|
||||
<div class="navbar-inner">
|
||||
<div class="container-fluid">
|
||||
<ul class="nav pull-left">
|
||||
<li><a><i class="icon-white icon-fire"></i> Burndown App</a></li>
|
||||
</ul>
|
||||
<ul class="nav pull-right">
|
||||
<li><a><i class="icon-white icon-book"></i> <%= @project %></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="container-fluid pad40">
|
||||
<section class="row-fluid">
|
||||
<div class="span2 sideBar">
|
||||
<br>
|
||||
<ul>
|
||||
<li class="active">
|
||||
<figure>
|
||||
<a href="burndown"><i class="icon-signal"></i> Burndown Chart</a>
|
||||
</figure>
|
||||
</li>
|
||||
<li>
|
||||
<figure>
|
||||
<a href="issues"><i class="icon-tasks"></i> Issues</a>
|
||||
</figure>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<section class="span10 content borBox">
|
||||
<div class="row-fluid">
|
||||
<div class="page-header">
|
||||
<h1>All issues are closed</h1>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
|
@ -1,100 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>Burndown App</title>
|
||||
|
||||
<base href="<%= @base_url %>"/>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="css/bootstrap.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="navbar blue navbar-fixed-top">
|
||||
<div class="navbar-inner">
|
||||
<div class="container-fluid">
|
||||
<ul class="nav pull-left">
|
||||
<li><a><i class="icon-white icon-fire"></i> Burndown App</a></li>
|
||||
</ul>
|
||||
<ul class="nav pull-right">
|
||||
<li><a><i class="icon-white icon-book"></i> <%= @project %></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="container-fluid pad40">
|
||||
<section class="row-fluid">
|
||||
<div class="span2 sideBar">
|
||||
<br>
|
||||
<ul>
|
||||
<li>
|
||||
<figure>
|
||||
<a href="burndown"><i class="icon-signal"></i> Burndown Chart</a>
|
||||
</figure>
|
||||
</li>
|
||||
<li class="active">
|
||||
<figure>
|
||||
<a href="issues"><i class="icon-tasks"></i> Issues</a>
|
||||
</figure>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<section class="span10 content borBox">
|
||||
<% if @issues.length > 0: %>
|
||||
<div class="row-fluid">
|
||||
<div class="page-header">
|
||||
<h1>List of outstanding issues</h1>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<div class="row-fluid">
|
||||
<div class="span12">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Title</th>
|
||||
<th>Opened</th>
|
||||
<th>Updated</th>
|
||||
<th>Opened By</th>
|
||||
<th>Assigned To</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% for issue in @issues: %>
|
||||
<tr>
|
||||
<td><%= issue.number %></td>
|
||||
<td><strong><%= issue.title %></strong></td>
|
||||
<td><%= issue.created_at %></td>
|
||||
<td><%= issue.updated_at %></td>
|
||||
<td><%= issue.user.login %></td>
|
||||
<td><%= issue.assignee?.login %></td>
|
||||
<td>
|
||||
<a target="_new" href="<%= issue.html_url %>">
|
||||
<button class="btn btn-mini btn-success"><i class="icon-white icon-eye-open"></i></button>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<% else: %>
|
||||
<div class="row-fluid">
|
||||
<div class="page-header">
|
||||
<h1>All issues are closed</h1>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,189 @@
|
|||
#!/usr/bin/env coffee
|
||||
proxy = do require('proxyquire').noCallThru
|
||||
assert = require 'assert'
|
||||
path = require 'path'
|
||||
|
||||
req = {}
|
||||
|
||||
regex = require path.resolve(__dirname, '../src/modules/regex.coffee')
|
||||
|
||||
issues = proxy path.resolve(__dirname, '../src/modules/issues.coffee'),
|
||||
'./request': req
|
||||
|
||||
repo = { 'milestone': { 'number': no } }
|
||||
|
||||
module.exports =
|
||||
|
||||
'issues - all empty': (done) ->
|
||||
called = 0
|
||||
req.all_issues = (repo, opts, cb) ->
|
||||
called += 1
|
||||
cb null, []
|
||||
|
||||
issues.get_all repo, (err, [ open, closed ]) ->
|
||||
assert.ifError err
|
||||
assert.equal called, 2
|
||||
assert.equal open.length, 0
|
||||
assert.equal closed.length, 0
|
||||
do done
|
||||
|
||||
'issues - open empty': (done) ->
|
||||
called = 0
|
||||
req.all_issues = (repo, opts, cb) ->
|
||||
called += 1
|
||||
cb null, if called is 1 then [] else [
|
||||
{ number: 1 }
|
||||
]
|
||||
|
||||
issues.get_all repo, (err, [ open, closed ]) ->
|
||||
assert.ifError err
|
||||
assert.equal called, 2
|
||||
assert.equal open.length, 0
|
||||
assert.equal closed.length, 1
|
||||
do done
|
||||
|
||||
'issues - closed empty': (done) ->
|
||||
called = 0
|
||||
req.all_issues = (repo, opts, cb) ->
|
||||
called += 1
|
||||
cb null, if called is 2 then [] else [
|
||||
{ number: 1 }
|
||||
]
|
||||
|
||||
issues.get_all repo, (err, [ open, closed ]) ->
|
||||
assert.ifError err
|
||||
assert.equal called, 2
|
||||
assert.equal open.length, 1
|
||||
assert.equal closed.length, 0
|
||||
do done
|
||||
|
||||
'issues - both not empty': (done) ->
|
||||
called = 0
|
||||
req.all_issues = (repo, opts, cb) ->
|
||||
called += 1
|
||||
cb null, [ { number: 1 } ]
|
||||
|
||||
issues.get_all repo, (err, [ open, closed ]) ->
|
||||
assert.ifError err
|
||||
assert.equal called, 2
|
||||
assert.equal open.length, 1
|
||||
assert.equal closed.length, 1
|
||||
do done
|
||||
|
||||
'issues - 99 results on a page': (done) ->
|
||||
called = 0
|
||||
req.all_issues = (repo, opts, cb) ->
|
||||
called += 1
|
||||
cb null, ( { number: i } for i in [ 0...99 ] )
|
||||
|
||||
issues.get_all repo, (err, [ open, closed ]) ->
|
||||
assert.ifError err
|
||||
assert.equal called, 2
|
||||
assert.equal open.length, 99
|
||||
assert.equal closed.length, 99
|
||||
do done
|
||||
|
||||
'issues - 100 results on a page': (done) ->
|
||||
called = 0
|
||||
req.all_issues = (repo, opts, cb) ->
|
||||
called += 1
|
||||
assert opts.page in [ 1, 2 ]
|
||||
cb null, if opts.page is 1 then ( { number: i } for i in [ 0...100 ] ) else []
|
||||
|
||||
issues.get_all repo, (err, [ open, closed ]) ->
|
||||
assert.ifError err
|
||||
assert.equal called, 4
|
||||
assert.equal open.length, 100
|
||||
assert.equal closed.length, 100
|
||||
do done
|
||||
|
||||
'issues - 101 total results': (done) ->
|
||||
called = 0
|
||||
req.all_issues = (repo, opts, cb) ->
|
||||
called += 1
|
||||
assert opts.page in [ 1, 2 ]
|
||||
cb null, if opts.page is 1
|
||||
( { number: i } for i in [ 0...100 ] )
|
||||
else
|
||||
[ { number: 100 } ]
|
||||
|
||||
issues.get_all repo, (err, [ open, closed ]) ->
|
||||
assert.ifError err
|
||||
assert.equal called, 4
|
||||
assert.equal open.length, 101
|
||||
assert.equal closed.length, 101
|
||||
assert.deepEqual open[100], { number: 100 }
|
||||
assert.deepEqual closed[100], { number: 100 }
|
||||
do done
|
||||
|
||||
'issues - 201 total results': (done) ->
|
||||
called = 0
|
||||
req.all_issues = (repo, opts, cb) ->
|
||||
called += 1
|
||||
assert opts.page in [ 1, 2, 3 ]
|
||||
cb null, if opts.page in [ 1, 2 ]
|
||||
( { number: i } for i in [ (h = 100 * (opts.page - 1))...h + 100 ] )
|
||||
else
|
||||
[ { number: 200 } ]
|
||||
|
||||
issues.get_all repo, (err, [ open, closed ]) ->
|
||||
assert.ifError err
|
||||
assert.equal called, 6
|
||||
assert.equal open.length, 201
|
||||
assert.equal closed.length, 201
|
||||
for i in [ open, closed ]
|
||||
for j in [ 100, 200 ]
|
||||
assert.deepEqual i[j], { number: j }
|
||||
do done
|
||||
|
||||
'issues - get all when not found': (done) ->
|
||||
called = 0
|
||||
req.all_issues = (repo, opts, cb) ->
|
||||
called += 1
|
||||
cb null, { 'message': 'Not Found' }
|
||||
|
||||
issues.get_all repo, (err, [ open, closed ]) ->
|
||||
assert.equal err, 'Not Found'
|
||||
assert.equal called, 1
|
||||
do done
|
||||
|
||||
'issues - filter on existing label regex': (done) ->
|
||||
issues.filter [ { labels: [ { name: 'size 15' } ] } ]
|
||||
, regex.size_label, (err, warn, data) ->
|
||||
assert.ifError err
|
||||
assert.ifError warn
|
||||
assert.equal data.length, 1
|
||||
assert.equal data[0].size, 15
|
||||
do done
|
||||
|
||||
'issues - filter when no labels': (done) ->
|
||||
issues.filter [ { } ]
|
||||
, regex.size_label, (err, warn, data) ->
|
||||
assert.ifError err
|
||||
assert.ifError warn
|
||||
assert.equal data.length, 0
|
||||
do done
|
||||
|
||||
'issues - filter when empty labels': (done) ->
|
||||
issues.filter [ { labels: [] } ]
|
||||
, regex.size_label, (err, warn, data) ->
|
||||
assert.ifError err
|
||||
assert.ifError warn
|
||||
assert.equal data.length, 0
|
||||
do done
|
||||
|
||||
'issues - filter when not matching regex': (done) ->
|
||||
issues.filter [ { labels: [ { name: 'size 1A' } ] } ]
|
||||
, regex.size_label, (err, warn, data) ->
|
||||
assert.ifError err
|
||||
assert.ifError warn
|
||||
assert.equal data.length, 0
|
||||
do done
|
||||
|
||||
'issues - filter when multiple match the regex': (done) ->
|
||||
issues.filter [ { labels: [ { name: 'size 1' }, { name: 'size 6' } ] } ]
|
||||
, regex.size_label, (err, warn, data) ->
|
||||
assert.ifError err
|
||||
assert.equal warn.length, 1
|
||||
assert.equal data.length, 1
|
||||
do done
|
|
@ -0,0 +1,86 @@
|
|||
#!/usr/bin/env coffee
|
||||
proxy = do require('proxyquire').noCallThru
|
||||
assert = require 'assert'
|
||||
path = require 'path'
|
||||
|
||||
req = {}
|
||||
|
||||
milestones = proxy path.resolve(__dirname, '../src/modules/milestones.coffee'),
|
||||
'./request': req
|
||||
|
||||
module.exports =
|
||||
|
||||
'milestones - get current from 1': (done) ->
|
||||
req.all_milestones = (opts, cb) ->
|
||||
cb null, [
|
||||
{
|
||||
'number': 1
|
||||
'created_at': '2013-01-01T00:00:00Z'
|
||||
'due_on': '2013-02-01T00:00:00Z'
|
||||
}
|
||||
]
|
||||
|
||||
milestones.get_current {}, (err, warn, milestone) ->
|
||||
assert.ifError err
|
||||
assert.equal milestone.number, 1
|
||||
do done
|
||||
|
||||
# We always take from head because of request params.
|
||||
'milestones - get current from > 1': (done) ->
|
||||
req.all_milestones = (opts, cb) ->
|
||||
cb null, [
|
||||
{
|
||||
'number': 2
|
||||
'created_at': '2013-01-01T00:00:00Z'
|
||||
'due_on': '2013-01-15T00:00:00Z'
|
||||
}
|
||||
{
|
||||
'number': 1
|
||||
'created_at': '2013-01-01T00:00:00Z'
|
||||
'due_on': '2013-02-01T00:00:00Z'
|
||||
}
|
||||
{
|
||||
'number': 3
|
||||
'created_at': '2013-01-01T00:00:00Z'
|
||||
'due_on': '2013-02-15T00:00:00Z'
|
||||
}
|
||||
]
|
||||
|
||||
milestones.get_current {}, (err, warn, milestone) ->
|
||||
assert.ifError err
|
||||
assert.equal milestone.number, 2
|
||||
do done
|
||||
|
||||
'milestones - get current when empty': (done) ->
|
||||
req.all_milestones = (opts, cb) ->
|
||||
cb null, []
|
||||
|
||||
milestones.get_current {}, (err, warn, milestone) ->
|
||||
assert.ifError err
|
||||
assert.equal warn, 'No open milestones for repo'
|
||||
do done
|
||||
|
||||
'milestones - get current when not found': (done) ->
|
||||
req.all_milestones = (opts, cb) ->
|
||||
cb null, { 'message': 'Not Found' }
|
||||
|
||||
milestones.get_current {}, (err, warn, milestone) ->
|
||||
assert.equal err, 'Not Found'
|
||||
do done
|
||||
|
||||
'milestones - get current when no issues': (done) ->
|
||||
req.all_milestones = (opts, cb) ->
|
||||
cb null, [
|
||||
{
|
||||
'number': 1
|
||||
'created_at': '2013-01-01T00:00:00Z'
|
||||
'due_on': '2013-02-01T00:00:00Z',
|
||||
'open_issues': 0,
|
||||
'closed_issues': 0
|
||||
}
|
||||
]
|
||||
|
||||
milestones.get_current {}, (err, warn, milestone) ->
|
||||
assert.ifError err
|
||||
assert.equal warn, 'No issues for milestone'
|
||||
do done
|
Loading…
Reference in New Issue