Skip to content

Instantly share code, notes, and snippets.

@vossim
Last active September 28, 2017 20:22
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vossim/1d2c1794fba362fd091e to your computer and use it in GitHub Desktop.
Save vossim/1d2c1794fba362fd091e to your computer and use it in GitHub Desktop.
Jira burndown plugin for dashing - GIST

dashing-jira-burndown

Jira burndown plugin for dashing

Description

GitHub location: https://github.com/vossim/dashing-jira-burndown

Dashing widget to display a Jira (greenhopper) burn-down, rotating the last X sprints for a specific rapidView (where X is configurable)

Example of a burndown:

Image

Installation

Put the files jira_burndown.coffee, jira_burndown.html and jira_burndown.scss in the /widget/jira_burndown directory and the files jira_burndown.rb in the /jobs directory and (optionally) jira_burndown.yaml in the /conf directory

This first part can also be done by using the gist: https://gist.github.com/vossim/1d2c1794fba362fd091e

dashing install 1d2c1794fba362fd091e

You also need the c3.min.js and d3.min.js files in the /assets/javascripts directory and the c3.min.css file in the /assets/stylesheets directory.

Job configuration

Required configuration:

  • jira_url: Url to your jira server, excluding the trailing slash (/)
  • username: Username for a user with sufficient rights on your jira server
  • password: Password for the user
  • numberOfSprintsToShow: The number of sprints to show (it'll show the last sprints it finds)
  • sprint_mapping: Mapping of the sprints, can be used to use this job to retrieve multiple sprint burndowns (name => rapidViewId).

Example of sprint_mapping:

sprint_mapping: 
    burndownProject1: 1
    burndownProject2: 23

Option 1: jira_burndown.yaml

Create a jira_burndown.yaml file in the /conf directory and configure it (example file in this repo).

Option 2: jira_burndown.rb

Configure the JIRA_CONFIG block in the ruby code.

Dashboard configuration

Put the following in your dashboard.erb file to show the status:

<li data-row="1" data-col="1" data-sizex="1" data-sizey="2">
  <div data-id="burndownProject1" data-view="JiraBurdown"></div>
</li>

Multiple burndowns can be added to a dashboard by repeating the snippet and changing the data-id.

License

Distributed under the MIT license

class Dashing.JiraBurndown extends Dashing.Widget
@accessor 'more-info', Dashing.AnimatedValue
ready: ->
for childNode in @node.childNodes
if (childNode.className == "graphContainer")
@targetNode = childNode
if !@chart && @data
@chart = constructChart(@data, @targetNode)
drawChart(@chart, @data)
onData: (data) ->
@data = data
if !@chart && @targetNode
@chart = constructChart(data, @targetNode)
if @chart?
drawChart(@chart, data)
constructChart = (data, targetNode) ->
numberOfSeries = data.series.length - 1
xs = {}
colors = {}
for i in [0..numberOfSeries] by 1
xs["y_"+i] = "x_"+i
colors["y_"+i] = data.series[i].color
container = $(@node).parent()
width = (Dashing.widget_base_dimensions[0] * container.data("sizex")) + Dashing.widget_margins[0] * 2 * (container.data("sizex") - 1)
height = (Dashing.widget_base_dimensions[1] * container.data("sizey"))
@chart = c3.generate({
bindto: targetNode,
data: {
xs: xs,
columns: [],
colors: colors
},
axis: {
x: {
show: false,
},
y: {
show: true,
min: 0,
padding: {
top: 0,
bottom: 0
},
tick: {
format: (e) ->
Math.round( e / 3600 )
}
}
},
size: {
width: width,
height: height
},
legend: {
show: false
},
tooltip: {
show: false
},
});
@chart
drawChart = (chart, data) ->
numberOfSeries = data.series.length - 1
columns = []
for i in [0..numberOfSeries] by 1
x = ["x_"+i]
y = ["y_"+i]
for point in data.series[i].data
x.push point.x
y.push point.y
columns.push x
columns.push y
chart.load({
columns: columns
})
<span class="graphContainer" style="position:absolute;top 0px;left 0px"></span>
<p class="more-info" data-bind="more-info"></p>
<p class="updated-at" data-bind="updatedAtMessage"></p>
require 'net/http'
require 'open-uri'
require 'cgi'
require 'json'
require 'time'
yamlFile = "./conf/jira_burndown.yaml"
if File.exist?(yamlFile)
JIRA_CONFIG = YAML.load(File.new(yamlFile, "r").read)
else
JIRA_CONFIG = {
jira_url: "",
username: "",
password: "",
numberOfSprintsToShow: 1,
sprint_mapping: {
'myBurndown' => 0
}
}
end
class SprintJsonDownloader
def initialize(urlPrefix, username, password)
@urlPrefix = urlPrefix
@username = username
@password = password
end
private def downloadJson(url)
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
request = Net::HTTP::Get.new(uri.request_uri)
if !@username.nil? && !@username.empty?
request.basic_auth(@username, @password)
end
JSON.parse(http.request(request).body)
end
def sprintOverview(rapidViewId)
downloadJson("#{@urlPrefix}/rest/greenhopper/1.0/sprintquery/#{rapidViewId}?includeFutureSprints=false")
end
def sprintBurnDown(rapidViewId, sprintId)
downloadJson("#{@urlPrefix}/rest/greenhopper/1.0/rapid/charts/scopechangeburndownchart.json?rapidViewId=#{rapidViewId}&sprintId=#{sprintId}")
end
end
class SprintOverviewJsonReader
def initialize(json, numberOfSprintsToShow)
@json = json
@numberOfSprintsToShow = numberOfSprintsToShow
end
def getSprintOverview(sprintIndex)
sprints = @json["sprints"][ -1 * @numberOfSprintsToShow, @numberOfSprintsToShow]
sprints[sprintIndex]
end
end
class SprintJsonReader
def initialize(json)
@json = json
end
def sprintStart
Time.at(@json["startTime"] / 1000)
end
def sprintEnd
Time.at(@json["endTime"] / 1000)
end
def jsonChangesByDateTime
@json["changes"].find_all {|key, value|
value.find_all {|subEntry|
! subEntry["timeC"].nil?
}.length > 0
}.map{ |key, value|
timeChanges = value.find_all { |valueEntry|
! valueEntry["timeC"].nil?
}
[Time.at(key.to_i / 1000), timeChanges]
}
end
def startEstimation
jsonChangesByDateTime.find_all { |key, value|
key < sprintStart
}.flat_map { |key, value|
value.map { |singleStory|
[key, singleStory]
}
}.reverse_each.reduce([]) {|hash, entry|
containsItem = hash.find{|tempEntry|
tempEntry[1]["key"] == entry[1]["key"]
}
if containsItem.nil?
hash.push([entry[0], entry[1]])
else
hash
end
}.reduce(0) {|estimation, entry|
estimation + entry[1]["timeC"]["newEstimate"].to_i
}
end
def changesDuringSprint
jsonChangesByDateTime.find_all { |key, value|
key > sprintStart && key < sprintEnd
}.map { |key, value|
durationChange = value.reduce(0) {|res, story|
res - (story["timeC"]["oldEstimate"].to_i - story["timeC"]["newEstimate"].to_i)
}
[key, durationChange]
}.find_all { |key, value|
value != 0
}
end
def loggedTimeInSprint
jsonChangesByDateTime.find_all { |key, value|
key > sprintStart && key < sprintEnd
}.map { |key, value|
timeSpent = value.reduce(0) {|res, story|
res + story["timeC"]["timeSpent"].to_i
}
[key, timeSpent]
}
end
end
class BurnDownBuilder
def initialize(sprintJsonReader)
@rdr = sprintJsonReader
end
def buildBurnDown
targetLine = [
{x: @rdr.sprintStart.to_i, y: @rdr.startEstimation},
{x: @rdr.sprintEnd.to_i, y: 0}
]
lastEntry = Time.new.to_i
lastEntry = lastEntry > @rdr.sprintEnd.to_i ? @rdr.sprintEnd.to_i : lastEntry
realLine = [{x: @rdr.sprintStart.to_i, y: @rdr.startEstimation}]
realLine = @rdr.changesDuringSprint.reduce(realLine) { |res, entry|
beforeChange = res.last[:y]
afterChange = beforeChange + entry[1]
res << {x: entry[0].to_i, y: beforeChange} << {x: entry[0].to_i+1, y: afterChange}
} << {x: lastEntry, y: realLine[-1][:y]}
loggedLine = [{x: @rdr.sprintStart.to_i, y: 0}]
loggedLine = @rdr.loggedTimeInSprint.reduce(loggedLine) { |res, entry|
beforeChange = res.last[:y]
afterChange = beforeChange + entry[1]
res << {x: entry[0].to_i, y: beforeChange} << {x: entry[0].to_i+1, y: afterChange}
} << {x: lastEntry, y: loggedLine[-1][:y]}
lines = [
{name: "Target", color:"#959595", data: targetLine},
{name: "Logged", color: "#10cd10", data: loggedLine},
{name: "Real", color: "#cd1010", data: realLine}
]
end
end
JIRA_BURNDOWNS = Hash.new()
JIRA_CONFIG[:sprint_mapping].each do |mappingName, rapidViewId|
sprintIndex = 0
SCHEDULER.every '10s', :first_in => 0 do
burndowns = JIRA_BURNDOWNS[mappingName]
if !burndowns.nil? && !burndowns.empty?
tempSprintIndex = sprintIndex
sprintIndex = (sprintIndex >= JIRA_CONFIG[:numberOfSprintsToShow]-1) ? 0 : sprintIndex + 1
send_event(mappingName, burndowns[tempSprintIndex])
end
end
end
JIRA_CONFIG[:sprint_mapping].each do |mappingName, rapidViewId|
SCHEDULER.every '15m', :first_in => 0 do
endNbr = JIRA_CONFIG[:numberOfSprintsToShow].to_i - 1
burndowns = [*0..endNbr].map do |sprintIndex|
downloader = SprintJsonDownloader.new(JIRA_CONFIG[:jira_url], JIRA_CONFIG[:username], JIRA_CONFIG[:password])
sprintOverview = SprintOverviewJsonReader.new(downloader.sprintOverview(rapidViewId), JIRA_CONFIG[:numberOfSprintsToShow]).getSprintOverview(sprintIndex)
sprintName = sprintOverview["name"]
sprintId = sprintOverview["id"]
reader = SprintJsonReader.new(downloader.sprintBurnDown(rapidViewId, sprintId))
lines = BurnDownBuilder.new(reader).buildBurnDown
{"more-info" => sprintName, series: lines}
end
JIRA_BURNDOWNS[mappingName] = burndowns
end
end
// ----------------------------------------------------------------------------
// Widget-jira-burndown styles
// ----------------------------------------------------------------------------
.widget-jira-burndown {
position: relative;
svg {
position: absolute;
left: 0px;
top: 0px;
}
.c3-line {
stroke-width: 3px;
}
.graphContainer {
position: absolute !important;
left: 0px;
top: 0px;
}
.more-info {
color: rgba(0, 0, 0, 0.8);
font-weight: 600;
font-size: 20px;
margin-top: 0;
}
.updated-at {
color: rgba(0, 0, 0, 0.3);
}
}
---
:jira_url: http://my.jira.server
:username: myUserName
:password: myPassword
:numberOfSprintsToShow: 3
:sprint_mapping:
myBurndown: 123
The MIT License (MIT)
Copyright (c) 2015 Simon Vos
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@Akshaysimha
Copy link

I was able to get the widget how ever the graph is not plotting correctly
Y axis has just zero and it shows a red line at the bottom.

can anyone help on this?

@sijakubo
Copy link

I was able to get the widget how ever the graph is not plotting correctly. Y axis has just zero and it shows a red line at the bottom.

I have the exact same Problem. Did you find any solution to this?

@vossim
Copy link
Author

vossim commented Aug 5, 2015

Hi,

Sorry for not responding sooner.

This graph does use the estimated time left in a sprint rather than the number of points left in a sprint. Do you have the "estimated time left" field filled in?

It is probably quite trivial to adjust this widget to use story points instead of estimated time remaining, but currently I don't have access to my PC.

@janvandijk
Copy link

Same issue here :-) Did you find access to your pc yet ;)

@Feuer-sturm
Copy link

I am also interested in tracking the remaining story points in an active sprint. I would be very thankful if you can modify your script.

Kind regards SpookyFishes

@arandre
Copy link

arandre commented Sep 28, 2017

Hi,

Getting a error when running the widget -
jira_burndown.rb:32:in private': nil is not a symbol (TypeError) from jira_burndown.rb:32:in class:SprintJsonDownloader'
from jira_burndown.rb:19:in `

'

Thoughts?

Sorry - just new to this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment