Skip to content

Instantly share code, notes, and snippets.

@kerbyfc
Last active November 30, 2022 10:27
Show Gist options
  • Save kerbyfc/ca9567df94fb6e4df4c1d70eed7221d1 to your computer and use it in GitHub Desktop.
Save kerbyfc/ca9567df94fb6e4df4c1d70eed7221d1 to your computer and use it in GitHub Desktop.
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