Skip to content

Instantly share code, notes, and snippets.

@slam-it
Last active April 16, 2016 08:12
Show Gist options
  • Save slam-it/a973a4974b6a57a4dbc4cb712d74b900 to your computer and use it in GitHub Desktop.
Save slam-it/a973a4974b6a57a4dbc4cb712d74b900 to your computer and use it in GitHub Desktop.
Dashing Widget - Jira Health

dashing-jira-health

Jira health widget for dashing

Description

GitHub location: https://github.com/slam-it/dashing-jira-health

Dashing widget to display the Jira (greenhopper) health status of the active sprint

Installation

1. Import Canvasjs library

In dashboards/layout.erb, add this script tag:

<script type="text/javascript" src="/assets/canvasjs.min.js"></script>

before this script tag:

<script type="text/javascript" src="/assets/application.js"></script>

2. Import Dashing.Canvasjs Widget

Put the files canvasjs.min.js and dashing-canvasjs.coffee files in the /assets/javascripts directory. Then in assets/javascripts/application.coffee, add #= require dashing-canvasjs right after #= require dashing.js so it looks like this:

# dashing.js is located in the dashing framework
# It includes jquery & batman for you.
#= require dashing.js
#= require dashing-canvasjs
3. Import Health widget

Put the files health.coffee, health.html and health.scss in the /widget/health directory and the file health.rb in the /jobs directory

Job configuration

Required configuration:

  • HOST: 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.
  • RAPID_VIEW_ID: The rapid board view id.

Dashboard configuration

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

<li data-row="1" data-col="1" data-sizex="2" data-sizey="1">
  <div data-id="health" data-view="Health" data-title="Sprint Health"></div>
</li>
class Dashing.Health extends Dashing.Canvasjs
ready: ->
@chart @get('container'), @get('chartData'), @get('title')
@addClass()
onData: (data) ->
@chart @get('container'), @get('chartData'), @get('title')
@addClass()
addClass: =>
healthData = @get('healthData')
scope = $(@node).find(".scope")
blocker = $(@node).find(".blocker")
flagged = $(@node).find(".flagged")
scope.addClass("change") if (healthData['scopeChange'] != "0%")
blocker.addClass("fade") if (healthData['blocker'] == 0)
flagged.addClass("fade") if (healthData['flagged'] == 0)
<div style="width:540px">
<div id="healthChart" class="left"></div>
<div class="right">
<div class="column">
<p>Days left</p>
<p data-bind="healthData['daysLeft']"></p>
</div>
<div class="column">
<p>Time elapsed</p>
<p data-bind="healthData['timeElapsed']"></p>
</div>
<div class="column">
<p>Work complete</p>
<p data-bind="healthData['workComplete']"></p>
</div>
<div class="column scope">
<p>Scope change</p>
<p data-bind="healthData['scopeChange']"></p>
</div>
<div class="column blocker">
<p>Blocker</p>
<p data-bind="healthData['blocker']"></p>
</div>
<div class="column flagged">
<p>Flagged</p>
<p data-bind="healthData['flagged']"></p>
</div>
</div>
</div>
require 'json'
require 'net/http'
require 'date'
require 'time'
$HOST = "JIRA-HOST"
$USERNAME = "JIRA-USERNAME"
$PASSWORD = "JIRA-PASSWORD"
$RAPID_VIEW_ID = "RAPID-VIEW-ID"
sprintQuery = "#{$HOST}/rest/greenhopper/1.0/sprintquery/#{$RAPID_VIEW_ID}?includeFutureSprints=false"
sprintReport = "#{$HOST}/rest/greenhopper/1.0/rapid/charts/sprintreport?rapidViewId=#{$RAPID_VIEW_ID}&sprintId=%s"
def fetch(uri)
request = Net::HTTP::Get.new(uri)
request.basic_auth $USERNAME, $PASSWORD
Net::HTTP.start(uri.hostname, uri.port, :use_ssl => uri.scheme == 'https') do |http|
JSON.parse(http.request(request).body)
end
end
def scopeChange(contents)
totalPoints = contents['completedIssuesEstimateSum']['value'] + contents['issuesNotCompletedEstimateSum']['value']
addedStories = contents['issueKeysAddedDuringSprint'].map { |issue| issue[0] }
startingPoints = contents['completedIssues'].select { |issue| !issue['estimateStatistic'].nil? && issue['estimateStatistic']['statFieldId'].eql?('customfield_10008') }.reject { |issue| issue['estimateStatistic']['statFieldValue']['value'].nil? || addedStories.include?(issue['key']) }.map { |issue| issue['estimateStatistic']['statFieldValue']['value'] }.reduce(:+)
startingPoints += contents['issuesNotCompletedInCurrentSprint'].select { |issue| !issue['estimateStatistic'].nil? && issue['estimateStatistic']['statFieldId'].eql?('customfield_10008') }.reject { |issue| issue['estimateStatistic']['statFieldValue']['value'].nil? || addedStories.include?(issue['key']) }.map { |issue| issue['estimateStatistic']['statFieldValue']['value'] }.reduce(:+)
(((totalPoints - startingPoints) / startingPoints) * 100).round
end
def countWeekendDays(from, to)
from.upto(to).count { |date| date.saturday? || date.sunday? }
end
def points(contents, statusIds)
contents.select { |issue| !issue['estimateStatistic'].nil? && issue['estimateStatistic']['statFieldId'].eql?('customfield_10008') && statusIds.include?(issue['statusId']) }.map! { |issue| issue['estimateStatistic']['statFieldValue']['value'] }.compact.reduce(0) { |sum, num| sum + num }
end
SCHEDULER.every '30m', first_in: 0 do |_job|
# Fetch active sprint
activeSprint = fetch(URI(sprintQuery))['sprints'].find { |sprint| sprint['state'].eql? 'ACTIVE' }
# Fetch sprint report for active sprint
report = fetch(URI(sprintReport % activeSprint['id']))
startTime = Time.parse(report['sprint']['startDate'])
startDate = startTime.to_date
endTime = Time.parse(report['sprint']['endDate'])
endDate = endTime.to_date
time = Time.now
date = time.to_date
# 1 => 'Open', 10000 => 'To Do', 10003 => 'Analyzing', 10014 => 'Reviewing', 10016 => 'Ready For Sprint', 10027 => 'Ready For Poker', 10031 => 'On hold/blocked'
toDo = points(report['contents']['issuesNotCompletedInCurrentSprint'], %w(1 10000 10003 10014 10016 10027 10031))
# 3 => 'In Progress', 10005 => 'Implementing', 10007 => 'Accepting', 10024 => 'Ready for Implementation', 10028 => 'Ready For Verification', 10029 => 'Verifying', 10030 => 'Ready For Acceptance'
inProgress = points(report['contents']['issuesNotCompletedInCurrentSprint'], %w(3 10005 10007 10024 10028 10029 10030))
# 6 => 'Closed', 10001 => 'Done'
done = points(report['contents']['completedIssues'], %w(6 10001))
totalPoints = (toDo + inProgress + done)
toDoPercentage = ((100 / totalPoints) * toDo).round
inProgressPercentage = ((100 / totalPoints) * inProgress).round
donePercentage = ((100 / totalPoints) * done).round
daysLeft = date.upto(endDate).reject { |date| date.saturday? || date.sunday? }.count - 1
timeElapsed = ((((time.to_i - startTime.to_i) - (countWeekendDays(startDate, date) * 86_400)).to_f / ((endTime.to_i - startTime.to_i) - (countWeekendDays(startDate, endDate) * 86_400)).to_f) * 100).round
workComplete = ((done / totalPoints) * 100).round
flagged = report['contents']['issuesNotCompletedInCurrentSprint'].count { |issue| issue['flagged'] }
blocker = report['contents']['issuesNotCompletedInCurrentSprint'].count { |issue| issue['priorityName'].eql?('Blocker') }
scopeChange = scopeChange(report['contents'])
chartData = [{type: 'pie', showInLegend: true, dataPoints: [{color: '#426082', y: toDo, legendText: "To Do (#{toDoPercentage}%)"}, {color: '#F3B834', y: inProgress, legendText: "In Progress (#{inProgressPercentage}%)"}, {color: '#13882B', y: done, legendText: "Done (#{donePercentage}%)"}]}]
healthData = {daysLeft: daysLeft, timeElapsed: "#{timeElapsed}%", workComplete: "#{workComplete}%", flagged: flagged, blocker: blocker, scopeChange: "#{scopeChange}%"}
send_event('health', container: 'healthChart', chartData: chartData, healthData: healthData, title: 'Overall sprint progress')
end
$background-color: #2d303a;
.widget-health {
background-color: $background-color;
.change{
color: #D04438;
}
.fade {
opacity: 0.2
}
.left {
height:310px;
width:50%;
}
.right {
position:absolute;
left:310px;
top:25px;
}
.column {
width:50%;
height:33%;
float:left;
text-align:center;
}
p:nth-child(1) {
margin-top:10px;
}
p:nth-child(2) {
font-size:40px
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment