Add initial line chart to show story point changes over time
Needs to be updated to consolidate by date and show the last point total only.
This commit is contained in:
parent
bd34d3bd96
commit
a87c8bae86
|
@ -2,7 +2,7 @@
|
|||
"manifestVersion": 1,
|
||||
"id": "enhanced-sprint-history",
|
||||
"publisher": "JamesSkemp",
|
||||
"version": "0.0.144",
|
||||
"version": "0.0.151",
|
||||
"name": "Enhanced Sprint History",
|
||||
"description": "Azure DevOps Extension",
|
||||
"categories": [
|
||||
|
|
|
@ -12,7 +12,9 @@
|
|||
"azure-devops-extension-api": "^2.226.0",
|
||||
"azure-devops-extension-sdk": "^3.1.3",
|
||||
"azure-devops-ui": "^2.236.0",
|
||||
"chart.js": "^4.4.2",
|
||||
"react": "~16.13.1",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-dom": "~16.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -109,6 +111,11 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@kurkle/color": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
|
||||
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
@ -1033,6 +1040,17 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.2.tgz",
|
||||
"integrity": "sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==",
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"pnpm": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
|
@ -3935,6 +3953,15 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-chartjs-2": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz",
|
||||
"integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==",
|
||||
"peerDependencies": {
|
||||
"chart.js": "^4.1.1",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz",
|
||||
|
|
|
@ -26,7 +26,9 @@
|
|||
"azure-devops-extension-api": "^2.226.0",
|
||||
"azure-devops-extension-sdk": "^3.1.3",
|
||||
"azure-devops-ui": "^2.236.0",
|
||||
"chart.js": "^4.4.2",
|
||||
"react": "~16.13.1",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-dom": "~16.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -33,3 +33,9 @@ td {
|
|||
margin-bottom: 1em;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.iteration-history-display {
|
||||
.bolt-card-content {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,10 @@ import { TeamSettingsIteration } from "azure-devops-extension-api/Work";
|
|||
import { IHubWorkItemHistory, IHubWorkItemIterationRevisions, ITypedWorkItem, ITypedWorkItemWithRevision } from "./HubInterfaces";
|
||||
import { getFlattenedRelevantRevisions, getIterationRelevantWorkItems, getTypedWorkItem } from "./HubUtils";
|
||||
import { Card } from "azure-devops-ui/Card";
|
||||
import { CategoryScale, Chart as ChartJs, LineElement, LinearScale, PointElement, Tooltip } from "chart.js";
|
||||
import { Line } from "react-chartjs-2";
|
||||
|
||||
ChartJs.register(CategoryScale, LinearScale, PointElement, LineElement, Tooltip);
|
||||
|
||||
export interface IterationHistoryDisplayProps {
|
||||
iteration: TeamSettingsIteration | undefined;
|
||||
|
@ -108,10 +112,103 @@ export class IterationHistoryDisplay extends React.Component<IterationHistoryDis
|
|||
|
||||
let totalStoryPoints = 0;
|
||||
|
||||
let changedWorkItems = getChangedWorkItems(getFlattenedRelevantRevisions(iterationWorkItemRevisions));
|
||||
|
||||
const storyPointChanges = changedWorkItems.map((wi, i, a) => {
|
||||
const workItemChange = getWorkItemChange(wi, i, a);
|
||||
const storyClosed = isWorkItemClosed(wi);
|
||||
const storyPointsChanged = workItemChange.change.indexOf('Story Points Changed') >= 0;
|
||||
let addedStoryPoints = 0;
|
||||
let subtractedStoryPoints = 0;
|
||||
let showAddedPoints = false;
|
||||
let showSubtractedPoints = false;
|
||||
if (workItemChange.change.indexOf('Removed') >= 0) {
|
||||
subtractedStoryPoints = wi.storyPoints;
|
||||
showSubtractedPoints = true;
|
||||
}
|
||||
if (workItemChange.change.indexOf('Added') >= 0) {
|
||||
addedStoryPoints = wi.storyPoints;
|
||||
showAddedPoints = true;
|
||||
}
|
||||
if (workItemChange.change.indexOf('Reopened') >= 0) {
|
||||
addedStoryPoints = wi.storyPoints;
|
||||
showAddedPoints = true;
|
||||
}
|
||||
if (storyClosed) {
|
||||
subtractedStoryPoints = wi.storyPoints;
|
||||
showSubtractedPoints = true;
|
||||
}
|
||||
if (storyPointsChanged) {
|
||||
if (!showAddedPoints && !showSubtractedPoints) {
|
||||
addedStoryPoints = wi.storyPoints;
|
||||
subtractedStoryPoints = workItemChange.lastRevision?.storyPoints ?? 0;
|
||||
showAddedPoints = true;
|
||||
showSubtractedPoints = true;
|
||||
} else if (storyClosed) {
|
||||
addedStoryPoints = wi.storyPoints;
|
||||
subtractedStoryPoints += workItemChange.lastRevision?.storyPoints ?? 0;
|
||||
showAddedPoints = true;
|
||||
showSubtractedPoints = true;
|
||||
} else {
|
||||
// TODO potentially?
|
||||
/*console.groupCollapsed(wi.id);
|
||||
console.table(wi);
|
||||
console.log(workItemChange);
|
||||
console.groupEnd();*/
|
||||
}
|
||||
}
|
||||
|
||||
let changeCharacterCode = 160;
|
||||
if (addedStoryPoints > subtractedStoryPoints) {
|
||||
changeCharacterCode = 8593; //8599;
|
||||
} else if (addedStoryPoints < subtractedStoryPoints) {
|
||||
changeCharacterCode = 8595; //8600
|
||||
}
|
||||
|
||||
let updatedTotalStoryPoints = addedStoryPoints - subtractedStoryPoints;
|
||||
totalStoryPoints += updatedTotalStoryPoints;
|
||||
|
||||
const totalStoryPointsClass = 'story-points total' + (updatedTotalStoryPoints > 0 ? ' increase' : updatedTotalStoryPoints < 0 ? ' decrease' : '');
|
||||
|
||||
return {
|
||||
changedDateFull: wi.changedDateFull,
|
||||
url: wi.url,
|
||||
title: wi.title,
|
||||
id: wi.id,
|
||||
workItemChange: workItemChange,
|
||||
addedStoryPoints: addedStoryPoints,
|
||||
showAddedPoints: showAddedPoints,
|
||||
subtractedStoryPoints: subtractedStoryPoints,
|
||||
showSubtractedPoints: showSubtractedPoints,
|
||||
totalStoryPointsClass: totalStoryPointsClass,
|
||||
totalStoryPoints: totalStoryPoints,
|
||||
changeCharacterCode: changeCharacterCode
|
||||
};
|
||||
})
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true
|
||||
};
|
||||
|
||||
const chartData = {
|
||||
labels: storyPointChanges.map(cwi => cwi.changedDateFull.toLocaleString()),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Story Points',
|
||||
data: storyPointChanges.map(cwi => cwi.totalStoryPoints),
|
||||
borderColor: 'rgb(53, 162, 235)',
|
||||
//backgroundColor: 'rgba(53, 162, 235, 0.5)'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="iteration-history-display"
|
||||
titleProps={{ text: "Iteration User Story History", ariaLevel: 3 }}>
|
||||
<table>
|
||||
<div className="display-child">
|
||||
<Line options={chartOptions} data={chartData} />
|
||||
</div>
|
||||
<table className="display-child">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="date">Date</th>
|
||||
|
@ -123,74 +220,18 @@ export class IterationHistoryDisplay extends React.Component<IterationHistoryDis
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
getChangedWorkItems(getFlattenedRelevantRevisions(iterationWorkItemRevisions)).map((wi, i, a) => {
|
||||
const workItemChange = getWorkItemChange(wi, i, a);
|
||||
const storyClosed = isWorkItemClosed(wi);
|
||||
const storyPointsChanged = workItemChange.change.indexOf('Story Points Changed') >= 0;
|
||||
let addedStoryPoints = 0;
|
||||
let subtractedStoryPoints = 0;
|
||||
let showAddedPoints = false;
|
||||
let showSubtractedPoints = false;
|
||||
if (workItemChange.change.indexOf('Removed') >= 0) {
|
||||
subtractedStoryPoints = wi.storyPoints;
|
||||
showSubtractedPoints = true;
|
||||
}
|
||||
if (workItemChange.change.indexOf('Added') >= 0) {
|
||||
addedStoryPoints = wi.storyPoints;
|
||||
showAddedPoints = true;
|
||||
}
|
||||
if (workItemChange.change.indexOf('Reopened') >= 0) {
|
||||
addedStoryPoints = wi.storyPoints;
|
||||
showAddedPoints = true;
|
||||
}
|
||||
if (storyClosed) {
|
||||
subtractedStoryPoints = wi.storyPoints;
|
||||
showSubtractedPoints = true;
|
||||
}
|
||||
if (storyPointsChanged) {
|
||||
if (!showAddedPoints && !showSubtractedPoints) {
|
||||
addedStoryPoints = wi.storyPoints;
|
||||
subtractedStoryPoints = workItemChange.lastRevision?.storyPoints ?? 0;
|
||||
showAddedPoints = true;
|
||||
showSubtractedPoints = true;
|
||||
} else if (storyClosed) {
|
||||
addedStoryPoints = wi.storyPoints;
|
||||
subtractedStoryPoints += workItemChange.lastRevision?.storyPoints ?? 0;
|
||||
showAddedPoints = true;
|
||||
showSubtractedPoints = true;
|
||||
} else {
|
||||
// TODO potentially?
|
||||
/*console.groupCollapsed(wi.id);
|
||||
console.table(wi);
|
||||
console.log(workItemChange);
|
||||
console.groupEnd();*/
|
||||
}
|
||||
}
|
||||
|
||||
let changeCharacterCode = 160;
|
||||
if (addedStoryPoints > subtractedStoryPoints) {
|
||||
changeCharacterCode = 8593; //8599;
|
||||
} else if (addedStoryPoints < subtractedStoryPoints) {
|
||||
changeCharacterCode = 8595; //8600
|
||||
}
|
||||
|
||||
let updatedTotalStoryPoints = addedStoryPoints - subtractedStoryPoints;
|
||||
totalStoryPoints += updatedTotalStoryPoints;
|
||||
|
||||
const totalStoryPointsClass = 'story-points total' + (updatedTotalStoryPoints > 0 ? ' increase' : updatedTotalStoryPoints < 0 ? ' decrease' : '');
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>{wi.changedDateFull.toLocaleString()}</td>
|
||||
<td><a href={wi.url} target="_blank" title={wi.title}>{wi.id}</a><br />{wi.title}</td>
|
||||
<td>{workItemChange.change.join(', ')}</td>
|
||||
<td className="story-points increase">{addedStoryPoints !== 0 || showAddedPoints ? '+' + addedStoryPoints : ''}</td>
|
||||
<td className="story-points decrease">{subtractedStoryPoints !== 0 || showSubtractedPoints ? '-' + subtractedStoryPoints : ''}</td>
|
||||
<td className={totalStoryPointsClass}>{totalStoryPoints} {String.fromCharCode(changeCharacterCode)}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
{storyPointChanges.map((wi, i, a) => {
|
||||
return (
|
||||
<tr>
|
||||
<td>{wi.changedDateFull.toLocaleString()}</td>
|
||||
<td><a href={wi.url} target="_blank" title={wi.title}>{wi.id}</a><br />{wi.title}</td>
|
||||
<td>{wi.workItemChange.change.join(', ')}</td>
|
||||
<td className="story-points increase">{wi.addedStoryPoints !== 0 || wi.showAddedPoints ? '+' + wi.addedStoryPoints : ''}</td>
|
||||
<td className="story-points decrease">{wi.subtractedStoryPoints !== 0 || wi.showSubtractedPoints ? '-' + wi.subtractedStoryPoints : ''}</td>
|
||||
<td className={wi.totalStoryPointsClass}>{wi.totalStoryPoints} {String.fromCharCode(wi.changeCharacterCode)}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
Loading…
Reference in New Issue