ready for assembly
This commit is contained in:
parent
9703da48b2
commit
aaf7fc2a18
|
@ -1,7 +0,0 @@
|
|||
node_modules/
|
||||
.idea/
|
||||
*.log
|
||||
src/components/
|
||||
config.json
|
||||
vendor/
|
||||
.build_cache~
|
|
@ -1,74 +0,0 @@
|
|||
module.exports = (grunt) ->
|
||||
grunt.initConfig
|
||||
pkg: grunt.file.readJSON("package.json")
|
||||
|
||||
apps_c:
|
||||
commonjs:
|
||||
src: [ 'src/**/*.{coffee,js,eco}' ]
|
||||
dest: 'build/app.js'
|
||||
options:
|
||||
main: 'src/app.coffee'
|
||||
name: [ 'ghbc', 'ghb', 'github-burndown-chart' ]
|
||||
|
||||
stylus:
|
||||
compile:
|
||||
options:
|
||||
paths: [ 'src/styles/app.styl' ]
|
||||
files:
|
||||
'build/app.css': 'src/styles/app.styl'
|
||||
|
||||
concat:
|
||||
scripts:
|
||||
src: [
|
||||
# Vendor dependencies.
|
||||
'vendor/async/lib/async.js'
|
||||
'vendor/d3/d3.js'
|
||||
'vendor/d3-tip/index.js'
|
||||
'vendor/lodash/dist/lodash.js'
|
||||
'vendor/marked/lib/marked.js'
|
||||
'vendor/superagent/superagent.js'
|
||||
# Our app.
|
||||
'build/app.js'
|
||||
]
|
||||
dest: 'build/app.bundle.js'
|
||||
options:
|
||||
separator: ';' # for minification purposes
|
||||
|
||||
styles:
|
||||
src: [
|
||||
# Vendor dependencies.
|
||||
'vendor/normalize-css/normalize.css'
|
||||
# Our styles.
|
||||
'src/styles/fonts.css'
|
||||
'build/app.css'
|
||||
]
|
||||
dest: 'build/app.bundle.css'
|
||||
|
||||
uglify:
|
||||
scripts:
|
||||
files:
|
||||
'build/app.min.js': 'build/app.js'
|
||||
'build/app.bundle.min.js': 'build/app.bundle.js'
|
||||
|
||||
cssmin:
|
||||
combine:
|
||||
files:
|
||||
'build/app.min.css': 'build/app.css'
|
||||
'build/app.bundle.min.css': 'build/app.bundle.css'
|
||||
|
||||
grunt.loadNpmTasks('grunt-apps-c')
|
||||
grunt.loadNpmTasks('grunt-contrib-stylus')
|
||||
grunt.loadNpmTasks('grunt-contrib-concat')
|
||||
grunt.loadNpmTasks('grunt-contrib-uglify')
|
||||
grunt.loadNpmTasks('grunt-contrib-cssmin')
|
||||
|
||||
grunt.registerTask('default', [
|
||||
'apps_c'
|
||||
'stylus'
|
||||
'concat'
|
||||
])
|
||||
|
||||
grunt.registerTask('minify', [
|
||||
'uglify'
|
||||
'cssmin'
|
||||
])
|
32
Makefile
32
Makefile
|
@ -1,32 +0,0 @@
|
|||
install:
|
||||
npm install
|
||||
bower install
|
||||
|
||||
build:
|
||||
grunt
|
||||
|
||||
minify:
|
||||
grunt minify
|
||||
|
||||
watch:
|
||||
watch --color -n 1 grunt
|
||||
|
||||
publish: build minify
|
||||
git checkout gh-pages
|
||||
git show master:build/app.bundle.min.js > app.bundle.min.js
|
||||
git show master:build/app.bundle.min.css > app.bundle.min.css
|
||||
git add .
|
||||
@status=$$(git status --porcelain); \
|
||||
if ! test "x$${status}" = x; then \
|
||||
git commit -m 'publish latest build to gh-pages'; \
|
||||
git push -u origin gh-pages; \
|
||||
fi
|
||||
git checkout master
|
||||
|
||||
test:
|
||||
./node_modules/.bin/mocha --compilers coffee:coffee-script --reporter spec --ui exports --timeout 20000 --slow 15000 --bail
|
||||
|
||||
serve:
|
||||
cd public; python -m SimpleHTTPServer 1892
|
||||
|
||||
.PHONY: build test
|
145
README.md
145
README.md
|
@ -1,145 +0,0 @@
|
|||
#GitHub Burndown Chart [![Built with Grunt](https://cdn.gruntjs.com/builtwith.png)](http://gruntjs.com/)
|
||||
|
||||
Displays a burndown chart from a set of GitHub issues in a milestone.
|
||||
|
||||
[ ![Codeship Status for radekstepan/github-burndown-chart](https://www.codeship.io/projects/d69f4420-e5b0-0130-bbae-1632ddfb80f8/status)](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.
|
||||
1. Multiple milestones; watch multiple milestones per repo, e.g. when using them for tracking epics.
|
||||
|
||||
![image](https://raw.github.com/radekstepan/github-burndown-chart/master/example.png)
|
||||
|
||||
##Quickstart
|
||||
|
||||
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 Personal Access 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. Just make sure that if the file exists, it is served with a correct MIME media type which is [application/json](http://stackoverflow.com/questions/477816/what-is-the-correct-json-content-type).
|
||||
|
||||
###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.
|
||||
|
||||
When multiple matching size labels are present on an issue, their **sum** is taken as the size of the whole issue. This allows you to mix & match (if you want) without creating too many labels in the GitHub interface.
|
||||
|
||||
###Token
|
||||
|
||||
Your **personal access token** 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 ]
|
||||
}
|
||||
```
|
||||
|
||||
###Style
|
||||
|
||||
Edit the `src/styles/app.styl` file to change the look & feel of the app. The head of the file contains variables for easy editing.
|
||||
|
||||
##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 coffee-script -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 build a custom version of your app, edit the `Gruntfile.coffee` and run the following:
|
||||
|
||||
```bash
|
||||
$ make install
|
||||
$ make build
|
||||
```
|
||||
|
||||
When you are done developing, you may want to create a minified build:
|
||||
|
||||
```bash
|
||||
$ make minify
|
||||
```
|
||||
|
||||
We are using the [Bower](http://bower.io/) package manager and [Grunt](http://gruntjs.com/) task runner. To [install Bower](http://bower.io/#install-bower) and [install Grunt](http://gruntjs.com/installing-grunt) run the following commands:
|
||||
|
||||
```bash
|
||||
$ sudo npm install bower -g
|
||||
$ sudo npm install grunt -g
|
||||
$ sudo npm install grunt-cli -g
|
||||
```
|
||||
|
||||
##Publish It
|
||||
|
||||
If you would like to track changes to build files in `gh-pages` branch, execute the following command:
|
||||
|
||||
```bash
|
||||
$ make publish
|
||||
```
|
||||
|
||||
It will checkout the `gh-pages` branch, copy the two build files from master and provided we have changed them locally, make a commit a push them to remote.
|
||||
|
||||
##Test It
|
||||
|
||||
```bash
|
||||
$ npm install -d
|
||||
$ make test
|
||||
```
|
||||
|
||||
Each bugfix receives an accompanying test case.
|
||||
|
||||
##Rewrite
|
||||
|
||||
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.
|
||||
|
||||
If you are upgrading from the previous app, then please bear in mind that `config.yaml` is replaced with `config.json`.
|
||||
|
||||
If you would like to use the original app, please refer to the `original` branch.
|
||||
|
||||
##Thanks
|
||||
|
||||
Thank you for using the app and your feedback/comments are very much welcome. Radek
|
13
bower.json
13
bower.json
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"name": "github-burndown-chart",
|
||||
"version": "1.0.0-alpha",
|
||||
"dependencies": {
|
||||
"lodash": "2.4.1",
|
||||
"async": "0.9.0",
|
||||
"d3": "3.4.8",
|
||||
"d3-tip": "0.6.4",
|
||||
"superagent": "0.18.0",
|
||||
"normalize-css": "3.0.1",
|
||||
"marked": "0.3.2"
|
||||
}
|
||||
}
|
|
@ -1,483 +0,0 @@
|
|||
/*! normalize.css v3.0.1 | MIT License | git.io/normalize */
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/* HTML5 display definitions
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Correct `block` display not defined for any HTML5 element in IE 8/9.
|
||||
* Correct `block` display not defined for `details` or `summary` in IE 10/11 and Firefox.
|
||||
* Correct `block` display not defined for `main` in IE 11.
|
||||
*/
|
||||
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
main,
|
||||
nav,
|
||||
section,
|
||||
summary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `inline-block` display not defined in IE 8/9.
|
||||
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
audio,
|
||||
canvas,
|
||||
progress,
|
||||
video {
|
||||
display: inline-block; /* 1 */
|
||||
vertical-align: baseline; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 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/10.
|
||||
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
|
||||
*/
|
||||
|
||||
[hidden],
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Links
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background color from active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improve readability when focused and also mouse hovered in all browsers.
|
||||
*/
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: 1px dotted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in Safari and Chrome.
|
||||
*/
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address variable `h1` font-size and margin within `section` and `article`
|
||||
* contexts in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9.
|
||||
*/
|
||||
|
||||
mark {
|
||||
background: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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/10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct overflow not hidden in IE 9/10/11.
|
||||
*/
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address margin not present in IE 8/9 and Safari.
|
||||
*/
|
||||
|
||||
figure {
|
||||
margin: 1em 40px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address differences between Firefox and other browsers.
|
||||
*/
|
||||
|
||||
hr {
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contain overflow in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address odd `em`-unit font size rendering in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Known limitation: by default, Chrome and Safari on OS X allow very limited
|
||||
* styling of `select`, unless a `border` property is set.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 1. Correct color not being inherited.
|
||||
* Known issue: affects color of disabled elements.
|
||||
* 2. Correct font properties not being inherited.
|
||||
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
color: inherit; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
margin: 0; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `overflow` set to `hidden` in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
button {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Firefox, IE 8/9/10/11, and Opera.
|
||||
* Correct `select` style inheritance in Firefox.
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and border in Firefox 4+.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
|
||||
* the UA stylesheet.
|
||||
*/
|
||||
|
||||
input {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* It's recommended that you don't attempt to style these elements.
|
||||
* Firefox's implementation doesn't respect box-sizing, padding, or width.
|
||||
*
|
||||
* 1. Address box sizing set to `content-box` in IE 8/9/10.
|
||||
* 2. Remove excess padding in IE 8/9/10.
|
||||
*/
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
|
||||
* `font-size` values of the `input`, it causes the cursor style of the
|
||||
* decrement button to change from `default` to `text`.
|
||||
*/
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
|
||||
* 2. Address `box-sizing` set to `border-box` in Safari 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 and Chrome on OS X.
|
||||
* Safari (but not Chrome) clips the cancel button when the search input has
|
||||
* padding (and `textfield` appearance).
|
||||
*/
|
||||
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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/10/11.
|
||||
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
|
||||
*/
|
||||
|
||||
legend {
|
||||
border: 0; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default vertical scrollbar in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't inherit the `font-weight` (applied by a rule above).
|
||||
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
|
||||
*/
|
||||
|
||||
optgroup {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Tables
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove most spacing between table cells.
|
||||
*/
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Source Sans Pro'), local('SourceSansPro-Regular'), url(http://themes.googleusercontent.com/static/fonts/sourcesanspro/v6/ODelI1aHBYDBqgeIAH2zlBM0YzuT7MdOe03otPbuUS0.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(http://themes.googleusercontent.com/static/fonts/sourcesanspro/v6/toadOcfmlt9b38dHJxOBGJ6-ys_j0H4QL65VLqzI3wI.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(http://themes.googleusercontent.com/static/fonts/sourcesanspro/v6/toadOcfmlt9b38dHJxOBGFkQc6VGVFSmCnC_l7QZG60.woff) format('woff');
|
||||
}
|
||||
body{height:100%;background:#d7bcab;background:-moz--webkit-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-moz--moz-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-moz--o-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-moz--ms-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-moz-linear-gradient(-45deg, #d7bcab 0%, #cc9485 100%);background:-webkit-gradient(linear, left top, right bottom, color-stop(0%, $background), color-stop(100%, #cc9485));background:-webkit-gradient(linear, left top, right bottom, color-stop(0%, $background), color-stop(100%, #cc9485));background:-webkit-gradient(linear, left top, right bottom, color-stop(0%, $background), color-stop(100%, #cc9485));background:-webkit-gradient(linear, left top, right bottom, color-stop(0%, $background), color-stop(100%, #cc9485));background:-webkit-gradient(linear, left top, right bottom, color-stop(0%, $background), color-stop(100%, #cc9485));background:-webkit--webkit-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-webkit--moz-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-webkit--o-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-webkit--ms-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-webkit-linear-gradient(-45deg, #d7bcab 0%, #cc9485 100%);background:-o--webkit-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-o--moz-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-o--o-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-o--ms-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-o-linear-gradient(-45deg, #d7bcab 0%, #cc9485 100%);background:-ms--webkit-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-ms--moz-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-ms--o-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-ms--ms-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-ms-linear-gradient(-45deg, #d7bcab 0%, #cc9485 100%);background:-webkit-linear-gradient(315deg, #d7bcab 0%, #cc9485 100%);background:-moz-linear-gradient(315deg, #d7bcab 0%, #cc9485 100%);background:-o-linear-gradient(315deg, #d7bcab 0%, #cc9485 100%);background:-ms-linear-gradient(315deg, #d7bcab 0%, #cc9485 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;-webkit-box-shadow:2px 4px 6px rgba(0,0,0,0.2);box-shadow:2px 4px 6px rgba(0,0,0,0.2);}
|
||||
.box.generic,.box.info,.box.error,.box.success{padding:20px 0;border-top:4px solid #eac85d;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}
|
||||
.box h2{margin:0;padding:0 20px 20px}
|
||||
.box p{margin:5px 0;padding:0 20px;}
|
||||
.box p.description{margin:-10px 0 0 0}
|
||||
#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("#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}
|
||||
.d3-tip{margin-top:-10px;font-size:11px;padding:8px 10px 7px 10px;text-align:center;background:rgba(0,0,0,0.75);color:#fff;-webkit-border-radius:3px;border-radius:3px;}
|
||||
.d3-tip:after{width:100%;color:rgba(0,0,0,0.8);content:"\25BC";position:absolute}
|
||||
.d3-tip.n:after{margin:-3px 0 0 0;top:100%;left:0}
|
||||
#progress{padding:20px;-webkit-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;}
|
||||
#progress:after{clear:both;display:block;content:""}
|
||||
#progress .bars{position:relative;}
|
||||
#progress .bars div{-webkit-border-radius:6px;border-radius:6px;height:12px;}
|
||||
#progress .bars div.closed{position:absolute;top:0;left:0;background:#4daf7c;}
|
||||
#progress .bars div.closed:not(.done){-webkit-border-radius:6px 0 0 6px;border-radius:6px 0 0 6px}
|
||||
#progress .bars div.opened{width:100%;background:#e55f3a}
|
||||
#progress h2{margin:10px 0 0 0;padding:0;}
|
||||
#progress h2.closed{float:left;color:#4daf7c}
|
||||
#progress h2.opened{float:right;color:#e55f3a}
|
21560
build/app.bundle.js
21560
build/app.bundle.js
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,39 +0,0 @@
|
|||
body{height:100%;background:#d7bcab;background:-moz--webkit-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-moz--moz-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-moz--o-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-moz--ms-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-moz-linear-gradient(-45deg, #d7bcab 0%, #cc9485 100%);background:-webkit-gradient(linear, left top, right bottom, color-stop(0%, $background), color-stop(100%, #cc9485));background:-webkit-gradient(linear, left top, right bottom, color-stop(0%, $background), color-stop(100%, #cc9485));background:-webkit-gradient(linear, left top, right bottom, color-stop(0%, $background), color-stop(100%, #cc9485));background:-webkit-gradient(linear, left top, right bottom, color-stop(0%, $background), color-stop(100%, #cc9485));background:-webkit-gradient(linear, left top, right bottom, color-stop(0%, $background), color-stop(100%, #cc9485));background:-webkit--webkit-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-webkit--moz-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-webkit--o-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-webkit--ms-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-webkit-linear-gradient(-45deg, #d7bcab 0%, #cc9485 100%);background:-o--webkit-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-o--moz-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-o--o-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-o--ms-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-o-linear-gradient(-45deg, #d7bcab 0%, #cc9485 100%);background:-ms--webkit-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-ms--moz-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-ms--o-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-ms--ms-linear-gradient(135deg, #d7bcab 0%, #cc9485 100%);background:-ms-linear-gradient(-45deg, #d7bcab 0%, #cc9485 100%);background:-webkit-linear-gradient(315deg, #d7bcab 0%, #cc9485 100%);background:-moz-linear-gradient(315deg, #d7bcab 0%, #cc9485 100%);background:-o-linear-gradient(315deg, #d7bcab 0%, #cc9485 100%);background:-ms-linear-gradient(315deg, #d7bcab 0%, #cc9485 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;-webkit-box-shadow:2px 4px 6px rgba(0,0,0,0.2);box-shadow:2px 4px 6px rgba(0,0,0,0.2);}
|
||||
.box.generic,.box.info,.box.error,.box.success{padding:20px 0;border-top:4px solid #eac85d;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}
|
||||
.box h2{margin:0;padding:0 20px 20px}
|
||||
.box p{margin:5px 0;padding:0 20px;}
|
||||
.box p.description{margin:-10px 0 0 0}
|
||||
#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("#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}
|
||||
.d3-tip{margin-top:-10px;font-size:11px;padding:8px 10px 7px 10px;text-align:center;background:rgba(0,0,0,0.75);color:#fff;-webkit-border-radius:3px;border-radius:3px;}
|
||||
.d3-tip:after{width:100%;color:rgba(0,0,0,0.8);content:"\25BC";position:absolute}
|
||||
.d3-tip.n:after{margin:-3px 0 0 0;top:100%;left:0}
|
||||
#progress{padding:20px;-webkit-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;}
|
||||
#progress:after{clear:both;display:block;content:""}
|
||||
#progress .bars{position:relative;}
|
||||
#progress .bars div{-webkit-border-radius:6px;border-radius:6px;height:12px;}
|
||||
#progress .bars div.closed{position:absolute;top:0;left:0;background:#4daf7c;}
|
||||
#progress .bars div.closed:not(.done){-webkit-border-radius:6px 0 0 6px;border-radius:6px 0 0 6px}
|
||||
#progress .bars div.opened{width:100%;background:#e55f3a}
|
||||
#progress h2{margin:10px 0 0 0;padding:0;}
|
||||
#progress h2.closed{float:left;color:#4daf7c}
|
||||
#progress h2.opened{float:right;color:#e55f3a}
|
1377
build/app.js
1377
build/app.js
File diff suppressed because it is too large
Load Diff
|
@ -1 +0,0 @@
|
|||
body{height:100%;background:#d7bcab;background:-moz--webkit-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-moz--moz-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-moz--o-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-moz--ms-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-moz-linear-gradient(-45deg,#d7bcab 0,#cc9485 100%);background:-webkit-gradient(linear,left top,right bottom,color-stop(0%,$background),color-stop(100%,#cc9485));background:-webkit-gradient(linear,left top,right bottom,color-stop(0%,$background),color-stop(100%,#cc9485));background:-webkit-gradient(linear,left top,right bottom,color-stop(0%,$background),color-stop(100%,#cc9485));background:-webkit-gradient(linear,left top,right bottom,color-stop(0%,$background),color-stop(100%,#cc9485));background:-webkit-gradient(linear,left top,right bottom,color-stop(0%,$background),color-stop(100%,#cc9485));background:-webkit--webkit-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-webkit--moz-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-webkit--o-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-webkit--ms-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-webkit-linear-gradient(-45deg,#d7bcab 0,#cc9485 100%);background:-o--webkit-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-o--moz-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-o--o-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-o--ms-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-o-linear-gradient(-45deg,#d7bcab 0,#cc9485 100%);background:-ms--webkit-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-ms--moz-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-ms--o-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-ms--ms-linear-gradient(135deg,#d7bcab 0,#cc9485 100%);background:-ms-linear-gradient(-45deg,#d7bcab 0,#cc9485 100%);background:-webkit-linear-gradient(315deg,#d7bcab 0,#cc9485 100%);background:-moz-linear-gradient(315deg,#d7bcab 0,#cc9485 100%);background:-o-linear-gradient(315deg,#d7bcab 0,#cc9485 100%);background:-ms-linear-gradient(315deg,#d7bcab 0,#cc9485 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;-webkit-box-shadow:2px 4px 6px rgba(0,0,0,.2);box-shadow:2px 4px 6px rgba(0,0,0,.2)}.box.generic,.box.info,.box.error,.box.success{padding:20px 0;border-top:4px solid #eac85d;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}.box h2{margin:0;padding:0 20px 20px}.box p{margin:5px 0;padding:0 20px}.box p.description{margin:-10px 0 0 0}#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(#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,.25);shape-rendering:crispEdges}#graph svg .axis text{font-weight:700;fill:#cacaca}#graph svg .axis path{display:none}.d3-tip{margin-top:-10px;font-size:11px;padding:8px 10px 7px;text-align:center;background:rgba(0,0,0,.75);color:#fff;-webkit-border-radius:3px;border-radius:3px}.d3-tip:after{width:100%;color:rgba(0,0,0,.8);content:"\25BC";position:absolute}.d3-tip.n:after{margin:-3px 0 0 0;top:100%;left:0}#progress{padding:20px;-webkit-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}#progress:after{clear:both;display:block;content:""}#progress .bars{position:relative}#progress .bars div{-webkit-border-radius:6px;border-radius:6px;height:12px}#progress .bars div.closed{position:absolute;top:0;left:0;background:#4daf7c}#progress .bars div.closed:not(.done){-webkit-border-radius:6px 0 0 6px;border-radius:6px 0 0 6px}#progress .bars div.opened{width:100%;background:#e55f3a}#progress h2{margin:10px 0 0;padding:0}#progress h2.closed{float:left;color:#4daf7c}#progress h2.opened{float:right;color:#e55f3a}
|
File diff suppressed because one or more lines are too long
BIN
example.png
BIN
example.png
Binary file not shown.
Before Width: | Height: | Size: 31 KiB |
46
package.json
46
package.json
|
@ -1,46 +0,0 @@
|
|||
{
|
||||
"name": "github-burndown-chart",
|
||||
"version": "1.0.0",
|
||||
"description": "Shows a burndown chart for GitHub Issues",
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"dependencies": {
|
||||
"coffee-script": "~1.6.3",
|
||||
"async": "~0.2.9",
|
||||
"proxyquire": "~0.5.1",
|
||||
"lodash": "~1.3.1",
|
||||
"connect": "~2.12.0",
|
||||
"request": "~2.27.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "~1.12.0",
|
||||
"marked": "~0.3.2",
|
||||
|
||||
"grunt": "~0.4.1",
|
||||
"grunt-apps-c": "0.1.10",
|
||||
"grunt-contrib-stylus": "~0.9.0",
|
||||
"grunt-contrib-concat": "~0.3.0",
|
||||
"grunt-contrib-uglify": "~0.2.5",
|
||||
"grunt-contrib-cssmin": "~0.6.2"
|
||||
},
|
||||
"scripts": {
|
||||
"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"
|
||||
}
|
||||
}
|
59
proxy.coffee
59
proxy.coffee
|
@ -1,59 +0,0 @@
|
|||
#!/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) ->
|
||||
end = (code, body) ->
|
||||
res.writeHead code, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
res.end body
|
||||
|
||||
# Log it.
|
||||
console.log new Date(), req.url
|
||||
|
||||
# Config?
|
||||
if req.url is '/config.json'
|
||||
# Refer to us like so.
|
||||
# Prefer custom header x-forwarded-host if defined.
|
||||
scrubbed.host = req.headers['x-forwarded-host'] or req.headers.host
|
||||
return end 200, JSON.stringify scrubbed, null, 4
|
||||
|
||||
# GitHub API request?
|
||||
if req.url.match /^\/repos/
|
||||
# The default headers.
|
||||
headers =
|
||||
# See http://developer.github.com/v3/media/#beta-v3-and-the-future
|
||||
'Accept': 'application/vnd.github.v3'
|
||||
# See http://developer.github.com/v3/#user-agent-required
|
||||
'User-Agent': 'GitHub-Burndown-Chart'
|
||||
# Add a token?
|
||||
headers.Authorization = "token #{config.token}" if config.token?
|
||||
|
||||
# Make the HTTPS request.
|
||||
return request {
|
||||
'uri': "https://#{config.host}#{req.url}"
|
||||
headers
|
||||
# Handle the response.
|
||||
}, (err, _res, body) ->
|
||||
return end(500) if err
|
||||
end _res.statusCode, body
|
||||
|
||||
# Get handled by Connect.
|
||||
do next
|
||||
|
||||
app = connect()
|
||||
.use(proxy)
|
||||
# Serve the public directory with the app, no need to launch another service.
|
||||
.use(connect.static(__dirname + '/public'))
|
||||
# Connect on an env port or go random.
|
||||
.listen process.env.PORT, ->
|
||||
console.log 'Proxy listening on port', app.address().port
|
|
@ -1 +0,0 @@
|
|||
!config.json
|
|
@ -1 +0,0 @@
|
|||
../build/app.bundle.js
|
|
@ -1 +0,0 @@
|
|||
../build/app.bundle.min.css
|
|
@ -1 +0,0 @@
|
|||
../build/app.bundle.min.js
|
|
@ -1 +0,0 @@
|
|||
../config.json
|
|
@ -1,19 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>GitHub Burndown Chart</title>
|
||||
|
||||
<link href="app.bundle.min.css" media="all" rel="stylesheet" type="text/css" />
|
||||
<script src="app.bundle.min.js"></script>
|
||||
<script>
|
||||
document.onreadystatechange = function() {
|
||||
if (document.readyState == "complete") {
|
||||
require('ghbc').call(null);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
|
@ -1,40 +0,0 @@
|
|||
#!/usr/bin/env coffee
|
||||
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
|
||||
# User/repo/(milestone) path
|
||||
path = match[1][1...]
|
||||
|
||||
# Say we are loading this repo then.
|
||||
render 'body', 'loading', { path }
|
||||
|
||||
# Did we specify a milestone?
|
||||
[ u, r, m ] = path.split('/')
|
||||
opts = if m then { 'path': "#{u}/#{r}", 'milestone': m } else { path }
|
||||
|
||||
# Get config/cache.
|
||||
return async.waterfall [ config
|
||||
# Render this repo.
|
||||
, (conf, cb) ->
|
||||
repo _.extend(opts, conf), cb
|
||||
], (err) ->
|
||||
render 'body', 'error', { 'text': do 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' }
|
|
@ -1,66 +0,0 @@
|
|||
#!/usr/bin/env coffee
|
||||
{ _ } = require './require'
|
||||
|
||||
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) ->
|
||||
# Skip cache in node.
|
||||
config = null if typeof window is 'undefined'
|
||||
# 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) ->
|
||||
# The wait is over.
|
||||
wait = no
|
||||
|
||||
# We do not strictly require config files.
|
||||
config = _.defaults result or {}, 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.
|
||||
( queue.pop() null, config while queue.length )
|
|
@ -1,241 +0,0 @@
|
|||
#!/usr/bin/env coffee
|
||||
{ _, d3 } = require './require'
|
||||
|
||||
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
|
||||
|
||||
# Milestone always has a creation date.
|
||||
created_at = new Date created_at
|
||||
# Due date can be empty.
|
||||
due_on = if due_on then new Date(due_on) else new Date()
|
||||
|
||||
a = created_at - start
|
||||
b = due_on - start
|
||||
|
||||
[
|
||||
{
|
||||
date: created_at
|
||||
points: fn(a)
|
||||
}, {
|
||||
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 = d3.tip().attr('class', 'd3-tip').html ({ number, title }) ->
|
||||
"##{number}: #{title}"
|
||||
|
||||
svg.call(tooltip)
|
||||
|
||||
# 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', tooltip.show)
|
||||
.on('mouseout', tooltip.hide)
|
||||
|
||||
cb null
|
|
@ -1,62 +0,0 @@
|
|||
#!/usr/bin/env coffee
|
||||
{ _, async } = require './require'
|
||||
|
||||
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) ->
|
||||
# Errors?
|
||||
return cb err if err
|
||||
# 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) ->
|
||||
# The total size of all issues.
|
||||
total = 0
|
||||
|
||||
filtered = _.filter collection, (issue) ->
|
||||
# Skip if no labels exist.
|
||||
return no unless labels = issue.labels
|
||||
|
||||
# Determine the total issue size from all labels.
|
||||
issue.size = _.reduce labels, (sum, label) ->
|
||||
# Not matching.
|
||||
return sum unless matches = label.name.match(regex)
|
||||
# Increase sum.
|
||||
sum += parseInt matches[1]
|
||||
, 0
|
||||
|
||||
# Increase the total.
|
||||
total += issue.size
|
||||
|
||||
# Are we saving it?
|
||||
!!issue.size
|
||||
|
||||
cb null, filtered, total
|
|
@ -1,45 +0,0 @@
|
|||
#!/usr/bin/env coffee
|
||||
{ _, marked } = require './require'
|
||||
|
||||
request = require './request'
|
||||
|
||||
# Get current/specified milestone for a repo.
|
||||
module.exports = (repo, cb) ->
|
||||
# Has description? Parse GFM.
|
||||
parse = (data) ->
|
||||
data.description = marked(data.description)[3...-5] if data.description
|
||||
data
|
||||
|
||||
# Get a specific milestone.
|
||||
if repo.milestone
|
||||
request.one_milestone repo, repo.milestone, (err, m) ->
|
||||
# Errors?
|
||||
return cb err if err
|
||||
# Empty milestone?
|
||||
if m.open_issues + m.closed_issues is 0
|
||||
return cb null, "No issues for milestone `#{m.title}`"
|
||||
# Parse GFM.
|
||||
m = parse m
|
||||
|
||||
cb null, null, m
|
||||
|
||||
# Get the current milestone out of many.
|
||||
else
|
||||
request.all_milestones repo, (err, data) ->
|
||||
# Errors?
|
||||
return cb err if err
|
||||
# Empty warning?
|
||||
return cb null, "No open milestones for repo #{repo.path}" unless data.length
|
||||
# The first milestone should be ending soonest.
|
||||
m = data[0]
|
||||
# Filter milestones without due date.
|
||||
m = _.rest data, { 'due_on' : null }
|
||||
# The first milestone should be ending soonest. Prefer milestones with due dates.
|
||||
m = if m[0] then m[0] else data[0]
|
||||
# Empty milestone?
|
||||
if m.open_issues + m.closed_issues is 0
|
||||
return cb null, "No issues for milestone `#{m.title}`"
|
||||
# Parse GFM.
|
||||
m = parse m
|
||||
|
||||
cb null, null, m
|
|
@ -1,8 +0,0 @@
|
|||
#!/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/(milestone) we want?
|
||||
'location': /^#!((\/[^\/]+){2,3})$/
|
|
@ -1,6 +0,0 @@
|
|||
#!/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
|
|
@ -1,79 +0,0 @@
|
|||
#!/usr/bin/env coffee
|
||||
{ _, async } = require './require'
|
||||
|
||||
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/specified milestone.
|
||||
async.waterfall [ (cb) ->
|
||||
milestones 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, 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', { 'repo': opts.path, 'milestone': opts.milestone }
|
||||
|
||||
# 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
|
|
@ -1,119 +0,0 @@
|
|||
#!/usr/bin/env coffee
|
||||
{ superagent, _ } = require './require'
|
||||
|
||||
# Custom JSON parser.
|
||||
superagent.parse =
|
||||
'application/json': (res) ->
|
||||
try
|
||||
JSON.parse res
|
||||
catch e
|
||||
{} # it was not to be...
|
||||
|
||||
module.exports =
|
||||
|
||||
# Get all milestones.
|
||||
'all_milestones': (repo, cb) ->
|
||||
request
|
||||
'protocol': repo.protocol
|
||||
'host': repo.host
|
||||
'path': "/repos/#{repo.path}/milestones"
|
||||
'query': { 'state': 'open', 'sort': 'due_date', 'direction': 'asc' }
|
||||
'headers': headers repo.token
|
||||
, cb
|
||||
|
||||
# Get one milestone.
|
||||
'one_milestone': (repo, number, cb) ->
|
||||
request
|
||||
'protocol': repo.protocol
|
||||
'host': repo.host
|
||||
'path': "/repos/#{repo.path}/milestones/#{number}"
|
||||
'query': { 'state': 'open', 'sort': 'due_date', 'direction': 'asc' }
|
||||
'headers': headers repo.token
|
||||
, cb
|
||||
|
||||
# Get all issues for a state.
|
||||
'all_issues': (repo, query, cb) ->
|
||||
request
|
||||
'protocol': repo.protocol
|
||||
'host': repo.host
|
||||
'path': "/repos/#{repo.path}/issues"
|
||||
'query': _.extend query, { 'per_page': '100' }
|
||||
'headers': headers repo.token
|
||||
, cb
|
||||
|
||||
# Get config from our host always.
|
||||
'config': (cb) ->
|
||||
request
|
||||
'protocol': 'http'
|
||||
'host': window.location.host
|
||||
'path': "#{window.location.pathname}config.json"
|
||||
'headers': _.extend headers(), { 'Accept': 'application/json' }
|
||||
, cb
|
||||
|
||||
# Make a request using SuperAgent.
|
||||
request = ({ protocol, host, path, query, headers }, cb) ->
|
||||
exited = no
|
||||
|
||||
# Make the query params.
|
||||
q = if query then '?' + ( "#{k}=#{v}" for k, v of query ).join('&') else ''
|
||||
|
||||
# The URI.
|
||||
req = superagent.get("#{protocol}://#{host}#{path}#{q}")
|
||||
# Add headers.
|
||||
( req.set(k, v) for k, v of headers )
|
||||
|
||||
# Timeout for requests that do not finish... see #32.
|
||||
timeout = setTimeout ->
|
||||
exited = yes
|
||||
cb 'Request has timed out'
|
||||
, 1e4 # give us 10s
|
||||
|
||||
# Send.
|
||||
req.end (err, data) ->
|
||||
# Arrived too late.
|
||||
return if exited
|
||||
# All fine.
|
||||
exited = yes
|
||||
clearTimeout timeout
|
||||
# Actually process the response.
|
||||
response err, data, cb
|
||||
|
||||
# How do we respond to a response?
|
||||
response = (err, data, cb) ->
|
||||
return cb error err if err
|
||||
# 2xx?
|
||||
if data.statusType isnt 2
|
||||
# Do we have a message from GitHub?
|
||||
return cb data.body.message if data?.body?.message?
|
||||
# Use SA one.
|
||||
return cb data.error.message
|
||||
# All good.
|
||||
cb null, data.body
|
||||
|
||||
# Give us headers.
|
||||
headers = (token) ->
|
||||
# The defaults.
|
||||
h = _.extend {},
|
||||
'Content-Type': 'application/json'
|
||||
'Accept': 'application/vnd.github.v3'
|
||||
# Add token?
|
||||
h.Authorization = "token #{token}" if token?
|
||||
h
|
||||
|
||||
# Parse an error.
|
||||
error = (err) ->
|
||||
switch
|
||||
when _.isString err
|
||||
message = err
|
||||
when _.isArray err
|
||||
message = err[1]
|
||||
when _.isObject(err) and _.isString(err.message)
|
||||
message = err.message
|
||||
|
||||
unless message
|
||||
try
|
||||
message = JSON.stringify err
|
||||
catch
|
||||
message = do err.toString
|
||||
|
||||
message
|
|
@ -1,5 +0,0 @@
|
|||
#!/usr/bin/env coffee
|
||||
# So we can easily stub these.
|
||||
module.exports = {
|
||||
_, superagent, d3, async, marked
|
||||
}
|
|
@ -1,209 +0,0 @@
|
|||
@import 'nib'
|
||||
|
||||
// color definitions
|
||||
$closed = #4DAF7C
|
||||
$opened = #E55F3A
|
||||
$grey = #CACACA
|
||||
$brown = #64584C
|
||||
$background1 = #D7BCAB
|
||||
$background2 = #CC9485
|
||||
|
||||
// font and gradient bg
|
||||
body
|
||||
height: 100%
|
||||
background: $background1
|
||||
background: -moz-linear-gradient(-45deg, $background1 0%, $background2 100%)
|
||||
background: -webkit-gradient(linear, left top, right bottom, color-stop(0%,$background), color-stop(100%,$background2))
|
||||
background: -webkit-linear-gradient(-45deg, $background1 0%,$background2 100%)
|
||||
background: -o-linear-gradient(-45deg, $background1 0%,$background2 100%)
|
||||
background: -ms-linear-gradient(-45deg, $background1 0%,$background2 100%)
|
||||
background: linear-gradient(135deg, $background1 0%,$background2 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
|
||||
padding: 20px 0
|
||||
border-top: 4px solid #EAC85D
|
||||
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
|
||||
|
||||
h2
|
||||
margin: 0
|
||||
padding: 0 20px 20px
|
||||
|
||||
p
|
||||
margin: 5px 0
|
||||
padding: 0 20px
|
||||
|
||||
&.description
|
||||
margin: -10px 0 0 0
|
||||
|
||||
// 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
|
||||
|
||||
// tooltips
|
||||
.d3-tip
|
||||
margin-top: -10px
|
||||
font-size: 11px
|
||||
padding: 8px 10px 7px 10px
|
||||
text-align: center
|
||||
background: rgba(0,0,0,0.75)
|
||||
color: #fff
|
||||
border-radius: 3px
|
||||
|
||||
&:after
|
||||
width: 100%
|
||||
color: rgba(0,0,0,0.8)
|
||||
content: "\25BC"
|
||||
position: absolute
|
||||
|
||||
&.n:after
|
||||
margin: -3px 0 0 0
|
||||
top: 100%
|
||||
left: 0
|
||||
|
||||
// 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
|
||||
padding: 0
|
||||
|
||||
&.closed
|
||||
float: left
|
||||
color: $closed
|
||||
|
||||
&.opened
|
||||
float: right
|
||||
color: $opened
|
|
@ -1,18 +0,0 @@
|
|||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Source Sans Pro'), local('SourceSansPro-Regular'), url(http://themes.googleusercontent.com/static/fonts/sourcesanspro/v6/ODelI1aHBYDBqgeIAH2zlBM0YzuT7MdOe03otPbuUS0.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(http://themes.googleusercontent.com/static/fonts/sourcesanspro/v6/toadOcfmlt9b38dHJxOBGJ6-ys_j0H4QL65VLqzI3wI.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(http://themes.googleusercontent.com/static/fonts/sourcesanspro/v6/toadOcfmlt9b38dHJxOBGFkQc6VGVFSmCnC_l7QZG60.woff) format('woff');
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
<div class="box error">
|
||||
<h2>Trouble</h2>
|
||||
<p><%- @text %></p>
|
||||
</div>
|
|
@ -1,11 +0,0 @@
|
|||
<div class="box">
|
||||
<h1><%- @milestone.title %>@<%- @repo %></h1>
|
||||
<% if @milestone.description: %>
|
||||
<p class="description"><%- @milestone.description %></p>
|
||||
<% end %>
|
||||
<div id="graph">
|
||||
<div id="tooltip"></div>
|
||||
<div id="svg"></div>
|
||||
</div>
|
||||
<div id="progress"></div>
|
||||
</div>
|
|
@ -1,9 +0,0 @@
|
|||
<div class="box info">
|
||||
<h2>GitHub Burndown Chart</h2>
|
||||
|
||||
<p>Use your browser's location hash to specify a <strong>repo</strong>: <a href="#!/radekstepan/disposable">#!/radekstepan/disposable</a>.</p>
|
||||
|
||||
<p>You can choose a specific <strong>milestone</strong> by its <em>number</em>: <a href="#!/radekstepan/disposable/1">#!/radekstepan/disposable/1</a>.</p>
|
||||
|
||||
<p>To get the milestone <em>number</em>, fetch all your milestones using <a href="https://developer.github.com/v3/issues/milestones/#list-milestones-for-a-repository" title="GitHub API docs">GitHub API</a>.</p>
|
||||
</div>
|
|
@ -1,10 +0,0 @@
|
|||
<% points = Math.ceil @points %>
|
||||
<% if points > 1: %>
|
||||
<%- points %> points left
|
||||
<% else: %>
|
||||
<% if points is 1: %>
|
||||
1 point left
|
||||
<% else: %>
|
||||
Done
|
||||
<% end %>
|
||||
<% end %>
|
|
@ -1,4 +0,0 @@
|
|||
<div class="box generic">
|
||||
<h2>GitHub Burndown Chart</h2>
|
||||
<p>Loading <a href="#!/<%- @path %>">#!/<%- @path %></a>.</p>
|
||||
</div>
|
|
@ -1,10 +0,0 @@
|
|||
<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>
|
|
@ -1,87 +0,0 @@
|
|||
#!/usr/bin/env coffee
|
||||
proxy = do require('proxyquire').noCallThru
|
||||
assert = require 'assert'
|
||||
path = require 'path'
|
||||
|
||||
req = {}
|
||||
|
||||
config = proxy path.resolve(__dirname, '../src/modules/config.coffee'),
|
||||
'./request': req
|
||||
'./require':
|
||||
'_': require 'lodash'
|
||||
'superagent': null
|
||||
'd3': null
|
||||
'async': null
|
||||
'marked': null
|
||||
|
||||
{ size_label } = require path.resolve __dirname, '../src/modules/regex.coffee'
|
||||
|
||||
module.exports =
|
||||
|
||||
'config - is null': (done) ->
|
||||
req.config = (cb) ->
|
||||
cb null, null
|
||||
|
||||
config (err, cfg) ->
|
||||
assert.ifError err
|
||||
assert.deepEqual cfg,
|
||||
'host': 'api.github.com'
|
||||
'protocol': 'https'
|
||||
'size_label': new RegExp size_label
|
||||
do done
|
||||
|
||||
'config - is empty': (done) ->
|
||||
req.config = (cb) ->
|
||||
cb null, {}
|
||||
|
||||
config (err, cfg) ->
|
||||
assert.ifError err
|
||||
assert.deepEqual cfg,
|
||||
'host': 'api.github.com'
|
||||
'protocol': 'https'
|
||||
'size_label': new RegExp size_label
|
||||
do done
|
||||
|
||||
'config - custom size label': (done) ->
|
||||
size = '/^taille (\d+)$/'
|
||||
|
||||
req.config = (cb) ->
|
||||
cb null, { 'size_label': size }
|
||||
|
||||
config (err, cfg) ->
|
||||
assert.ifError err
|
||||
assert.deepEqual cfg,
|
||||
'host': 'api.github.com'
|
||||
'protocol': 'https'
|
||||
'size_label': new RegExp size
|
||||
do done
|
||||
|
||||
'config - custom valid protocol': (done) ->
|
||||
req.config = (cb) ->
|
||||
cb null, { 'protocol': 'http' }
|
||||
|
||||
config (err, cfg) ->
|
||||
assert.ifError err
|
||||
assert.deepEqual cfg,
|
||||
'host': 'api.github.com'
|
||||
'protocol': 'http'
|
||||
'size_label': new RegExp size_label
|
||||
do done
|
||||
|
||||
'config - custom invalid protocol': (done) ->
|
||||
req.config = (cb) ->
|
||||
cb null, { 'protocol': 'nntp' }
|
||||
|
||||
config (err, cfg) ->
|
||||
assert.equal err, 'Config field `protocol` misconfigured'
|
||||
assert.equal cfg, null
|
||||
do done
|
||||
|
||||
'config - custom invalid off days': (done) ->
|
||||
req.config = (cb) ->
|
||||
cb null, { 'off_days': [ 0 ] }
|
||||
|
||||
config (err, cfg) ->
|
||||
assert.equal err, 'Config field `off_days` misconfigured'
|
||||
assert.equal cfg, null
|
||||
do done
|
|
@ -1,196 +0,0 @@
|
|||
#!/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
|
||||
'./require':
|
||||
'_': require 'lodash'
|
||||
'superagent': null
|
||||
'd3': null
|
||||
'async': require 'async'
|
||||
'marked': null
|
||||
|
||||
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 '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, data) ->
|
||||
assert.ifError err
|
||||
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, data) ->
|
||||
assert.ifError err
|
||||
assert.equal data.length, 0
|
||||
do done
|
||||
|
||||
'issues - filter when empty labels': (done) ->
|
||||
issues.filter [ { labels: [] } ]
|
||||
, regex.size_label, (err, data) ->
|
||||
assert.ifError err
|
||||
assert.equal data.length, 0
|
||||
do done
|
||||
|
||||
'issues - filter when not matching regex': (done) ->
|
||||
issues.filter [ { labels: [ { name: 'size 1A' } ] } ]
|
||||
, regex.size_label, (err, data) ->
|
||||
assert.ifError err
|
||||
assert.equal data.length, 0
|
||||
do done
|
||||
|
||||
'issues - filter when multiple match the regex': (done) ->
|
||||
issues.filter [
|
||||
{ labels: [ { name: 'size 1' }, { name: 'size 6' } ] }
|
||||
{ labels: [ { name: 'size really big' }, { name: 'size 4' } ] }
|
||||
]
|
||||
, regex.size_label, (err, data) ->
|
||||
assert.ifError err
|
||||
assert.equal data.length, 2
|
||||
[ a, b ] = data
|
||||
assert.equal a.size, 7
|
||||
assert.equal b.size, 4
|
||||
do done
|
|
@ -1,188 +0,0 @@
|
|||
#!/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
|
||||
'./require':
|
||||
'_': require 'lodash'
|
||||
'superagent': null
|
||||
'd3': null
|
||||
'async': null
|
||||
'marked': require 'marked'
|
||||
|
||||
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 {}, (err, warn, milestone) ->
|
||||
assert.ifError err
|
||||
assert.equal milestone.number, 1
|
||||
do done
|
||||
|
||||
'milestones - get current from 1 when milestone has no due date': (done) ->
|
||||
req.all_milestones = (opts, cb) ->
|
||||
cb null, [
|
||||
{
|
||||
'number': 1
|
||||
'created_at': '2013-01-01T00:00:00Z'
|
||||
'due_on': null
|
||||
}
|
||||
]
|
||||
|
||||
milestones {}, (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 {}, (err, warn, milestone) ->
|
||||
assert.ifError err
|
||||
assert.equal milestone.number, 2
|
||||
do done
|
||||
|
||||
'milestones - get current from > 1 when there are milestones without due date': (done) ->
|
||||
req.all_milestones = (opts, cb) ->
|
||||
cb null, [
|
||||
{
|
||||
'number': 2
|
||||
'created_at': '2013-01-01T00:00:00Z'
|
||||
'due_on': null
|
||||
}
|
||||
{
|
||||
'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 {}, (err, warn, milestone) ->
|
||||
assert.ifError err
|
||||
assert.equal milestone.number, 1
|
||||
do done
|
||||
|
||||
'milestones - get current when empty': (done) ->
|
||||
req.all_milestones = (opts, cb) ->
|
||||
cb null, []
|
||||
|
||||
milestones { 'path': 'some/repo' }, (err, warn, milestone) ->
|
||||
assert.ifError err
|
||||
assert.equal warn, 'No open milestones for repo some/repo'
|
||||
do done
|
||||
|
||||
'milestones - get current when not found': (done) ->
|
||||
req.all_milestones = (opts, cb) ->
|
||||
cb 'Not Found'
|
||||
|
||||
milestones {}, (err, warn, milestone) ->
|
||||
assert.equal err, 'Not Found'
|
||||
do done
|
||||
|
||||
'milestones - get current when no issues': (done) ->
|
||||
req.all_milestones = (opts, cb) ->
|
||||
cb null, [
|
||||
{
|
||||
'title': 'No issues'
|
||||
'number': 1
|
||||
'created_at': '2013-01-01T00:00:00Z'
|
||||
'due_on': '2013-02-01T00:00:00Z',
|
||||
'open_issues': 0,
|
||||
'closed_issues': 0
|
||||
}
|
||||
]
|
||||
|
||||
milestones {}, (err, warn, milestone) ->
|
||||
assert.ifError err
|
||||
assert.equal warn, 'No issues for milestone `No issues`'
|
||||
do done
|
||||
|
||||
'milestones - get one': (done) ->
|
||||
m =
|
||||
'number': 1
|
||||
'created_at': '2013-01-01T00:00:00Z'
|
||||
'due_on': '2013-02-01T00:00:00Z'
|
||||
|
||||
req.one_milestone = (opts, number, cb) ->
|
||||
cb null, m
|
||||
|
||||
milestones { 'milestone': 1 }, (err, warn, milestone) ->
|
||||
assert.ifError err
|
||||
assert.equal warn, null
|
||||
assert.deepEqual milestone, m
|
||||
do done
|
||||
|
||||
'milestones - get one (404)': (done) ->
|
||||
req.one_milestone = (opts, number, cb) ->
|
||||
cb 'Not Found'
|
||||
|
||||
milestones { 'milestone': 9 }, (err, warn, milestone) ->
|
||||
assert.equal err, 'Not Found'
|
||||
do done
|
||||
|
||||
'milestones - get one when no issues': (done) ->
|
||||
req.one_milestone = (opts, number, cb) ->
|
||||
cb null, {
|
||||
'title': 'No issues'
|
||||
'number': 1
|
||||
'created_at': '2013-01-01T00:00:00Z'
|
||||
'due_on': '2013-02-01T00:00:00Z',
|
||||
'open_issues': 0,
|
||||
'closed_issues': 0
|
||||
}
|
||||
|
||||
milestones { 'milestone': 9 }, (err, warn, milestone) ->
|
||||
assert.ifError err
|
||||
assert.equal warn, 'No issues for milestone `No issues`'
|
||||
do done
|
||||
|
||||
'milestones - has description': (done) ->
|
||||
req.all_milestones = (opts, cb) ->
|
||||
cb null, [
|
||||
{
|
||||
'number': 1
|
||||
'created_at': '2013-01-01T00:00:00Z'
|
||||
'due_on': '2013-02-01T00:00:00Z'
|
||||
'description': 'A description of this <strong>milestone</strong> goes *here*'
|
||||
}
|
||||
]
|
||||
|
||||
milestones {}, (err, warn, milestone) ->
|
||||
assert.ifError err
|
||||
assert.equal milestone.description, 'A description of this <strong>milestone</strong> goes <em>here</em>'
|
||||
do done
|
|
@ -1,114 +0,0 @@
|
|||
#!/usr/bin/env coffee
|
||||
proxy = do require('proxyquire').noCallThru
|
||||
assert = require 'assert'
|
||||
path = require 'path'
|
||||
_ = require 'lodash'
|
||||
|
||||
class Superagent
|
||||
|
||||
# How soon do we call back?
|
||||
timeout: 1
|
||||
|
||||
# Save the uri.
|
||||
get: (uri) ->
|
||||
@params = { uri }
|
||||
@
|
||||
|
||||
# Save the key-value pair.
|
||||
set: (key, value) ->
|
||||
@params[key] = value
|
||||
@
|
||||
|
||||
# Call back with the response.
|
||||
end: (cb) ->
|
||||
setTimeout =>
|
||||
cb null, @response
|
||||
, @timeout
|
||||
|
||||
request = proxy path.resolve(__dirname, '../src/modules/request.coffee'),
|
||||
'./require':
|
||||
'_': require 'lodash'
|
||||
'superagent': sa = new Superagent()
|
||||
'd3': null
|
||||
'async': null
|
||||
'marked': null
|
||||
|
||||
module.exports =
|
||||
|
||||
'request - all milestones (ok)': (done) ->
|
||||
sa.response =
|
||||
'statusType': 2
|
||||
'error': no
|
||||
'body': [ null ]
|
||||
|
||||
request.all_milestones {}, (err, data) ->
|
||||
assert.ifError err
|
||||
assert.deepEqual sa.params,
|
||||
'uri': 'undefined://undefined/repos/undefined/milestones?state=open&sort=due_date&direction=asc'
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/vnd.github.v3'
|
||||
assert.deepEqual data, [ null ]
|
||||
do done
|
||||
|
||||
'request - one milestone (ok)': (done) ->
|
||||
sa.response =
|
||||
'statusType': 2
|
||||
'error': no
|
||||
'body': [ null ]
|
||||
|
||||
request.one_milestone {}, 1, (err, data) ->
|
||||
assert.ifError err
|
||||
assert.deepEqual sa.params,
|
||||
'uri': 'undefined://undefined/repos/undefined/milestones/1?state=open&sort=due_date&direction=asc'
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/vnd.github.v3'
|
||||
assert.deepEqual data, [ null ]
|
||||
do done
|
||||
|
||||
'request - one milestone (404)': (done) ->
|
||||
sa.response =
|
||||
'statusType': 4
|
||||
'error': Error "cannot GET undefined (404)"
|
||||
'body':
|
||||
'documentation_url': "http://developer.github.com/v3"
|
||||
'message': "Not Found"
|
||||
|
||||
request.one_milestone {}, 9, (err) ->
|
||||
assert.equal err, 'Not Found'
|
||||
do done
|
||||
|
||||
'request - one milestone (500)': (done) ->
|
||||
sa.response =
|
||||
'statusType': 5
|
||||
'error': Error "Error"
|
||||
'body': null
|
||||
|
||||
request.one_milestone {}, 9, (err) ->
|
||||
assert.equal err, 'Error'
|
||||
do done
|
||||
|
||||
'request - all issues (ok)': (done) ->
|
||||
sa.response =
|
||||
'statusType': 2
|
||||
'error': no
|
||||
'body': [ null ]
|
||||
|
||||
request.all_issues {}, {}, (err, data) ->
|
||||
assert.ifError err
|
||||
assert.deepEqual sa.params,
|
||||
'uri': 'undefined://undefined/repos/undefined/issues?per_page=100'
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/vnd.github.v3'
|
||||
assert.deepEqual data, [ null ]
|
||||
do done
|
||||
|
||||
'request - timeout': (done) ->
|
||||
sa.timeout = 10001
|
||||
sa.response =
|
||||
'statusType': 2
|
||||
'error': no
|
||||
'body': [ null ]
|
||||
|
||||
request.all_issues {}, {}, (err) ->
|
||||
assert.equal err, 'Request has timed out'
|
||||
do done
|
Loading…
Reference in New Issue