More work on burndown chart

This commit is contained in:
Gordon 2021-04-18 20:45:36 +01:00
parent ab42299b6b
commit 2998afa356
4 changed files with 145 additions and 60 deletions

View File

@ -5,13 +5,17 @@ import { v4 as uuidv4 } from "uuid";
export default class KanbnBurndownPanel {
private static readonly viewType = "react";
private static panels: Record<string, KanbnBurndownPanel> = {};
private static panels: KanbnBurndownPanel[] = [];
private readonly _panel: vscode.WebviewPanel;
private readonly _extensionPath: string;
private readonly _workspacePath: string;
private readonly _kanbn: typeof import("@basementuniverse/kanbn/src/main");
private readonly _panelUuid: string;
private sprintMode: boolean = true;
private sprint: string = '';
private startDate: string = '';
private endDate: string = '';
private assigned: string = '';
private _disposables: vscode.Disposable[] = [];
public static async show(
@ -22,51 +26,32 @@ export default class KanbnBurndownPanel {
const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined;
// Create a panel
const panelUuid = uuidv4();
const burndownPanel = new KanbnBurndownPanel(
extensionPath,
workspacePath,
column || vscode.ViewColumn.One,
kanbn,
panelUuid
kanbn
);
KanbnBurndownPanel.panels[panelUuid] = burndownPanel;
KanbnBurndownPanel.panels.push(burndownPanel);
}
public static async updateAll() {
const panels = Object.values(KanbnBurndownPanel.panels);
if (panels.length === 0) {
if (KanbnBurndownPanel.panels.length === 0) {
return;
}
const kanbn = panels[0]._kanbn;
let index: any;
try {
index = await kanbn.getIndex();
} catch (error) {
vscode.window.showErrorMessage(error instanceof Error ? error.message : error);
return;
}
let tasks: any[];
try {
tasks = (await kanbn.loadAllTrackedTasks(index)).map((task) => kanbn.hydrateTask(index, task));
} catch (error) {
vscode.window.showErrorMessage(error instanceof Error ? error.message : error);
return;
}
panels.forEach((panel) => panel._update(index, tasks));
const { index, tasks } = await KanbnBurndownPanel.panels[0].getKanbnIndexAndTasks();
KanbnBurndownPanel.panels.forEach((panel) => panel._update(index, tasks));
}
private constructor(
extensionPath: string,
workspacePath: string,
column: vscode.ViewColumn,
kanbn: typeof import("@basementuniverse/kanbn/src/main"),
panelUuid: string
kanbn: typeof import("@basementuniverse/kanbn/src/main")
) {
this._extensionPath = extensionPath;
this._workspacePath = workspacePath;
this._kanbn = kanbn;
this._panelUuid = panelUuid;
// Create and show a new webview panel
this._panel = vscode.window.createWebviewPanel(KanbnBurndownPanel.viewType, "Burndown Chart", column, {
@ -104,10 +89,21 @@ export default class KanbnBurndownPanel {
this._panel.webview.onDidReceiveMessage(
async (message) => {
switch (message.command) {
// Display error message
case "error":
vscode.window.showErrorMessage(message.text);
return;
// Refresh the kanbn chart
case 'kanbn.refreshBurndownData':
this.sprintMode = message.sprintMode;
this.sprint = message.sprint;
this.startDate = message.startDate;
this.endDate = message.endDate;
this.assigned = message.assigned;
KanbnBurndownPanel.updateAll();
return;
}
},
null,
@ -125,15 +121,46 @@ export default class KanbnBurndownPanel {
}
}
private async _update(index: any, tasks: any[]) {
private async _update(index: any|null, tasks: any[]|null) {
if (!index && !tasks) {
({ index, tasks } = await this.getKanbnIndexAndTasks());
}
this._panel.webview.postMessage({
type: "burndown",
index,
tasks,
dateFormat: this._kanbn.getDateFormat(index),
burndownData: await this._kanbn.burndown(
(this.sprintMode && this.sprint) ? [this.sprint] : null,
(!this.sprintMode && this.startDate && this.endDate)
? [
new Date(Date.parse(this.startDate)),
new Date(Date.parse(this.endDate))
]
: null,
this.assigned || null
)
});
}
private async getKanbnIndexAndTasks(): Promise<{ index: any|null, tasks: any[]|null }> {
let index: any;
try {
index = await this._kanbn.getIndex();
} catch (error) {
vscode.window.showErrorMessage(error instanceof Error ? error.message : error);
return { index: null, tasks: null };
}
let tasks: any[];
try {
tasks = (await this._kanbn.loadAllTrackedTasks(index)).map((task) => this._kanbn.hydrateTask(index, task));
} catch (error) {
vscode.window.showErrorMessage(error instanceof Error ? error.message : error);
return { index: null, tasks: null };
}
return { index, tasks };
}
private _getHtmlForWebview() {
const manifest = require(path.join(this._extensionPath, "build", "asset-manifest.json"));
const mainScript = manifest["main.js"];

View File

@ -117,6 +117,7 @@ function App() {
tasks={tasks}
sprints={sprints}
burndownData={burndownData}
dateFormat={dateFormat}
vscode={vscode}
/>
}

View File

@ -1,9 +1,10 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { ResponsiveContainer, LineChart, Line, CartesianGrid, XAxis, YAxis, Tooltip } from 'recharts';
import VSCodeApi from "./VSCodeApi";
import formatDate from 'dateformat';
import dateFormat from 'dateformat';
const Burndown = ({ name, tasks, sprints, burndownData, vscode }: {
const Burndown = ({ name, tasks, sprints, burndownData, dateFormat, vscode }: {
name: string,
tasks: Record<string, KanbnTask>,
sprints: KanbnSprint[],
@ -14,23 +15,71 @@ const Burndown = ({ name, tasks, sprints, burndownData, vscode }: {
to: string,
dataPoints: Array<{
x: string,
y: number
y: number,
count: number,
tasks: Array<{
eventType: string,
taskId: string
}>
}>
}>
},
dateFormat: string,
vscode: VSCodeApi
}) => {
const [sprintMode, setSprintMode] = useState(sprints.length > 0);
const [sprint, setSprint] = useState(null);
const [startDate, setStartDate] = useState(null);
const [endDate, setEndDate] = useState(null);
const hasSprints = sprints.length > 0;
const [sprintMode, setSprintMode] = useState(hasSprints);
const [sprint, setSprint] = useState(sprintMode ? burndownData.series[0].sprint.name : '');
const [startDate, setStartDate] = useState(sprintMode ? '' : formatDate(burndownData.series[0].from, 'yyyy-mm-dd'));
const [endDate, setEndDate] = useState(sprintMode ? '' : formatDate(burndownData.series[0].to, 'yyyy-mm-dd'));
const [assigned, setAssigned] = useState('');
const data = [
{name: 'one', uv: 20},
{name: 'two', uv: 80},
{name: 'three', uv: 45},
{name: 'four', uv: 32}
];
const refreshBurndownData = () => {
vscode.postMessage({
command: 'kanbn.refreshBurndownData',
sprintMode,
sprint,
startDate,
endDate,
assigned
});
};
const handleChangeSprint = ({ target: { value }}) => {
setSprint(value);
refreshBurndownData();
};
const handleChangeStartDate = ({ target: { value }}) => {
setStartDate(value);
refreshBurndownData();
};
const handleChangeEndDate = ({ target: { value }}) => {
setEndDate(value);
refreshBurndownData();
};
const handleClickSprintMode = () => {
setSprintMode(true);
refreshBurndownData();
};
const handleClickDateMode = () => {
setSprintMode(false);
refreshBurndownData();
};
const chartMin = Date.parse(burndownData.series[0].from);
const chartMax = Date.parse(burndownData.series[0].to);
const chartData = burndownData.series[0].dataPoints.map(dataPoint => ({
x: Date.parse(dataPoint.x),
y: dataPoint.y,
'Active tasks': dataPoint.count,
'Activity': dataPoint.tasks.map(task => `${task.eventType} ${task.taskId}`).join('\n')
}));
const formatXAxis = date => formatDate(date, dateFormat);
return (
<React.Fragment>
@ -41,12 +90,16 @@ const Burndown = ({ name, tasks, sprints, burndownData, vscode }: {
<form>
{
sprintMode
? <select className="kanbn-burndown-settings-sprint-select" onChange={e => { console.log(e); }}>
? <select
value={sprint}
className="kanbn-burndown-settings-sprint-select"
onChange={handleChangeSprint}
>
{
sprints.length > 0
? sprints.map((sprint, i) => {
? sprints.map(sprint => {
return (
<option value={i}>{sprint.name}</option>
<option value={sprint.name}>{sprint.name}</option>
);
})
: <option disabled>No sprints</option>
@ -55,31 +108,30 @@ const Burndown = ({ name, tasks, sprints, burndownData, vscode }: {
: <React.Fragment>
<input
type="date"
value={startDate}
className="kanbn-burndown-settings-input kanbn-burndown-settings-start-date"
onChange={e => { console.log(e); }}
onChange={handleChangeStartDate}
/>
<input
type="date"
value={endDate}
className="kanbn-burndown-settings-input kanbn-burndown-settings-end-date"
onChange={e => { console.log(e); }}
onChange={handleChangeEndDate}
/>
</React.Fragment>
}
<button
{hasSprints && <button
type="button"
className={[
'kanbn-header-button',
'kanbn-burndown-settings-sprint-mode',
sprintMode ? 'kanbn-header-button-active' : 'kanbn-header-button-inactive'
].join(' ')}
onClick={() => {
setSprintMode(true);
// update burndown chart
}}
onClick={handleClickSprintMode}
title="View sprint burndown"
>
<i className="codicon codicon-rocket"></i>
</button>
</button>}
<button
type="button"
className={[
@ -87,10 +139,7 @@ const Burndown = ({ name, tasks, sprints, burndownData, vscode }: {
'kanbn-burndown-settings-date-mode',
sprintMode ? 'kanbn-header-button-inactive' : 'kanbn-header-button-active'
].join(' ')}
onClick={() => {
setSprintMode(false);
// update burndown chart
}}
onClick={handleClickDateMode}
title="View date-range burndown"
>
<i className="codicon codicon-clock"></i>
@ -101,10 +150,10 @@ const Burndown = ({ name, tasks, sprints, burndownData, vscode }: {
</div>
<div className="kanbn-burndown">
<ResponsiveContainer width="100%" height="100%" className="kanbn-burndown-chart">
<LineChart data={data}>
<Line type="monotone" dataKey="uv" stroke="#8884d8" />
<CartesianGrid stroke="#ccc" strokeDasharray="5 5" />
<XAxis dataKey="name" />
<LineChart data={chartData}>
<Line className="kanbn-burndown-line" type="monotone" dataKey="y" />
<CartesianGrid className="kanbn-burndown-grid" strokeDasharray="5 5" vertical={false} />
<XAxis dataKey="x" type="number" domain={[chartMin, chartMax]} tickFormatter={formatXAxis} />
<YAxis />
<Tooltip />
</LineChart>

View File

@ -537,3 +537,11 @@ body.vscode-dark .kanbn-burndown-settings-input[type="date"]::-webkit-calendar-p
height: 85vh;
width: 95vw;
}
.kanbn-burndown-grid {
stroke: var(--vscode-activityBar-inactiveForeground);
}
.kanbn-burndown-line path {
stroke: #3c7;
}