Last active
November 30, 2022 10:27
-
-
Save kerbyfc/ca9567df94fb6e4df4c1d70eed7221d1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
window.helicopterView = (function(){ | |
const CONTAINER_CLS = 'stats'; | |
const HEADER_CLS = 'ghx-heading'; | |
const SUBNAV_CLS = 'subnav-container'; | |
function getColIssues(col, status) { | |
return Array.from(col.children).reduce((acc, issue) => { | |
const [spEl, componentsEl, labelsEl] = $(issue).find('.ghx-extra-fields').children(); | |
const sp = Number(spEl.innerText.replace(/,/g, '.')); | |
const type = $(issue).find('.ghx-type').attr('title'); | |
const priority = $(issue).find('.ghx-priority').attr('title'); | |
const flagged = $(issue).find('.ghx-flag').length > 0; | |
return [...acc, { | |
components: componentsEl.innerText.split(/,\s*/), | |
labels: labelsEl.innerText.split(/,\s*/), | |
priority, | |
flagged, | |
status, | |
type, | |
sp: isNaN(sp) ? 0 : sp, | |
}]; | |
}, []); | |
}; | |
function sp(issues) { | |
return issues.reduce((acc, issue) => ({ | |
sp: acc.sp + issue.sp, | |
spRisks: acc.spRisks + (issue.flagged ? issue.sp : 0), | |
}), | |
{ | |
sp: 0, | |
spRisks: 0, | |
}); | |
} | |
function getDaysLeft() { | |
const days = parseInt($('.days-left').text().replace(/[^\d]/g, '')); | |
return isNaN(days) ? null : days; | |
} | |
function getSwimlineStats(swimline) { | |
const [todoCol, wipCol, reviewCol, doneCol] = $(swimline).find('.ghx-columns').children(); | |
const todoIssues = getColIssues(todoCol, 'todo'); | |
const wipIssues = getColIssues(wipCol, 'wip').concat(getColIssues(reviewCol, 'wip')); | |
const doneIssues = getColIssues(doneCol, 'done'); | |
const stats = { | |
todo: { | |
issues: todoIssues, | |
count: todoIssues.length, | |
...sp(todoIssues), | |
}, | |
wip: { | |
issues: wipIssues, | |
count: wipIssues.length, | |
...sp(wipIssues), | |
}, | |
done: { | |
issues: doneIssues, | |
count: doneIssues.length, | |
...sp(doneIssues), | |
}, | |
}; | |
stats.common = { | |
issues: todoIssues.concat(wipIssues).concat(doneIssues), | |
count: stats.todo.count + stats.wip.count + stats.done.count, | |
sp: stats.todo.sp + stats.wip.sp + stats.done.sp, | |
}; | |
stats.risks = { | |
sp: stats.todo.spRisks + stats.wip.spRisks, | |
}; | |
stats.progress = stats.common.sp | |
? Math.round(stats.done.sp / stats.common.sp * 100) | |
: Math.round(stats.done.count / stats.common.count * 100); | |
return stats; | |
} | |
function getSwimlineName(swimline) { | |
return $(swimline).find(`.${HEADER_CLS} > span`).text(); | |
} | |
function getStatsContainer(holder) { | |
let container = holder.find(`.${CONTAINER_CLS}`); | |
if (!container.length) { | |
holder.append(`<div class="${CONTAINER_CLS}" style="width: 100%"/>`); | |
} | |
return holder.find(`.${CONTAINER_CLS}`); | |
} | |
function getSwimlineStatsContainer(swimline) { | |
const holder = $(swimline).find(`.${HEADER_CLS}`); | |
return getStatsContainer(holder); | |
} | |
function getSubnavStatsContainer() { | |
const holder = $(`.${SUBNAV_CLS}`); | |
return getStatsContainer(holder); | |
} | |
function div(...args) { | |
const content = args.length > 1 ? args[1] : args[0]; | |
const style = args.length > 1 ? args[0] : []; | |
return `<div style="${style.join(';')}">${content}</div>` | |
} | |
function renderSwimlineIssue(issue) { | |
const isGoal = issue.type === 'Sprint Goal'; | |
let content = issue.sp > 0 || isGoal ? String(issue.sp).replace(/^0/, '') : ''; | |
let color = 'gray'; | |
if (issue.status === 'wip') { | |
color = '#6482b4'; | |
} | |
if (issue.status === 'done') { | |
color = '#97BA42'; | |
} | |
const style = [ | |
`margin-right: 2px`, | |
`box-sizing: border-box`, | |
`font-size: 9px`, | |
`color: white`, | |
`width: ${Math.max(isGoal ? 20 : 7, 25 * issue.sp)}px`, | |
`border: 2px solid ${issue.flagged ? 'orange' : color}`, | |
`background: ${color}`, | |
]; | |
if (isGoal) { | |
if (issue.labels.includes('green')) { | |
content += "🟢"; | |
} else if (issue.labels.includes('yellow')) { | |
content += "🟡"; | |
} else { | |
content += '🔴'; | |
} | |
} | |
if (sp > 0 && ['Основной', 'Критический'].includes(issue.priority)) { | |
content += "🔺"; | |
} | |
return ` | |
<div style="${style.join(';')}">${content}</div> | |
`; | |
} | |
function span(style, text) { | |
return `<span style="${style.join(';')}">${text}</span>`; | |
} | |
function risks(sp) { | |
if (sp) { | |
return span([`color: orange`], `(${sp} 🚩)`); | |
} | |
return ''; | |
} | |
function renderStats(stats, options = {}) { | |
const daysLeft = getDaysLeft(); | |
const risksPercent = stats.total ? Math.round(stats.risks / stats.total * 100) : 0; | |
const progressPercent = stats.total ? Math.round(stats.done / stats.total * 100) : 0; | |
const awaitedProgressPercent = daysLeft ? Math.round(100 - (100 / 9 * daysLeft)) : 100; | |
const minAwaitedProgressPercent = awaitedProgressPercent - (options.awailableProgressLag || 20); | |
const fontSize = options.fontSize || 16; | |
const itemStyle = [ | |
`margin-left: 15px`, | |
`height: 16px`, | |
`line-height: 16px`, | |
`font-size: ${fontSize}px` | |
]; | |
let progressColor = '#97BA42'; | |
if (progressPercent < awaitedProgressPercent) { | |
if (minAwaitedProgressPercent - progressPercent < 0) { | |
progressColor = 'black'; | |
} else { | |
progressColor = 'orange' | |
} | |
} | |
const progressStyle = [ | |
...itemStyle, | |
`color: ${progressColor}`, | |
]; | |
const risksStyle = [ | |
...itemStyle, | |
`color: ${risksPercent > (options.awailableRisksPercent || 0) ? 'orange' : '#97BA42'}`, | |
]; | |
const awaitedProgress = ( | |
options.awaitedProgress === true && | |
awaitedProgressPercent > progressPercent | |
) | |
? ` vs awaited as least ${minAwaitedProgressPercent}%` | |
: ''; | |
const small = (text) => span([`font-size: ${fontSize * 0.7}px`], text); | |
const done = `${stats.done}/${stats.total}sp`; | |
const donePercent = `${progressPercent}%${awaitedProgress ? ` ${awaitedProgress}` : ''}`; | |
return ` | |
${div(progressStyle, `✅ ${done} ${small(`(${donePercent + risks(stats.doneRisks)})`)}`)} | |
${div(itemStyle, `⏳ ${stats.wip}sp ${small(risks(stats.wipRisks))}`)} | |
${div(itemStyle, `📤 ${stats.todo}sp ${small(risks(stats.todoRisks))}`)} | |
${div(risksStyle, `🚩 ${stats.risks}sp ${small(`(${risksPercent}%)`)}`)} | |
`; | |
} | |
function addStatsToSubnav(swimlineStats) { | |
const container = getSubnavStatsContainer(); | |
const stats = swimlineStats.reduce((acc, swimline) => ({ | |
done: acc.done + swimline.done.sp, | |
todo: acc.todo + swimline.todo.sp, | |
total: acc.total + swimline.common.sp, | |
doneRisks: acc.doneRisks + swimline.done.spRisks, | |
todoRisks: acc.todoRisks + swimline.todo.spRisks, | |
wipRisks: acc.wipRisks + swimline.wip.spRisks, | |
risks: acc.risks + swimline.risks.sp, | |
wip: acc.wip + swimline.wip.sp, | |
}), { | |
done: 0, | |
todo: 0, | |
total: 0, | |
todoRisks: 0, | |
wipRisks: 0, | |
doneRisks: 0, | |
risks: 0, | |
wip: 0, | |
}); | |
const style = [ | |
`display: flex`, | |
`font-size: 16px`, | |
`padding-top: 8px`, | |
]; | |
container.html(div(style, renderStats(stats, { | |
awaitedProgress: true, | |
awailableRisksPercent: 15 | |
}))); | |
} | |
function addStatsToSwimline(swimline, stats) { | |
const container = getSwimlineStatsContainer(swimline); | |
const style = [ | |
`display: flex`, | |
`height: 16px`, | |
`text-align: center`, | |
`font-size: 11px` | |
]; | |
container.html(div(style,` | |
${stats.common.issues.map(renderSwimlineIssue).join('')} | |
${renderStats({ | |
done: stats.done.sp, | |
todo: stats.todo.sp, | |
total: stats.common.sp, | |
doneRisks: stats.done.spRisks, | |
todoRisks: stats.todo.spRisks, | |
wipRisks: stats.wip.spRisks, | |
risks: stats.risks.sp, | |
wip: stats.wip.sp, | |
}, { | |
fontSize: 14, | |
})} | |
`)); | |
} | |
const swimlineStats = []; | |
$('.ghx-swimlane').each((index, swimline) => { | |
const name = getSwimlineName(swimline); | |
const stats = getSwimlineStats(swimline); | |
swimlineStats.push(stats); | |
addStatsToSwimline(swimline, stats); | |
}); | |
if ($('.js-quickfilter-button.ghx-active').length === 0) { | |
addStatsToSubnav(swimlineStats); | |
} | |
}); | |
if (window.helicopterViewTimer) { | |
clearInterval(window.helicopterViewTimer); | |
window.helicopterViewTimer = null; | |
} | |
window.shouldRenderHelicopterView = true; | |
setInterval(function() { | |
if ($('.ghx-loading-pool').length > 0) { | |
window.shouldRenderHelicopterView = true; | |
} else if (window.shouldRenderHelicopterView) { | |
window.shouldRenderHelicopterView = false; | |
window.helicopterView(); | |
} | |
}, 500); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment