diff --git a/api/src/roadmap/roadmap.service.ts b/api/src/roadmap/roadmap.service.ts index ec9df86..a1bf64a 100644 --- a/api/src/roadmap/roadmap.service.ts +++ b/api/src/roadmap/roadmap.service.ts @@ -43,6 +43,7 @@ export class RoadmapService { export const getTeamId = (teamName: string) => { return String(teamName) + .replace('team-', '') // If team is prefixed by team-, we simply remove it from the string .replace(/[^a-z0-9+]+/gi, '') .toLowerCase(); }; diff --git a/api/src/velocity/velocity.service.ts b/api/src/velocity/velocity.service.ts index 06acb07..e2f2809 100644 --- a/api/src/velocity/velocity.service.ts +++ b/api/src/velocity/velocity.service.ts @@ -46,6 +46,7 @@ export class VelocityService { export const getTeamId = (teamName: string) => { return String(teamName) + .replace('team-', '') // If team is prefixed by team-, we simply remove it from the string .replace(/[^a-z0-9+]+/gi, '') .toLowerCase(); }; diff --git a/cli/src/utils/misc/teamUtils.ts b/cli/src/utils/misc/teamUtils.ts index ab8c571..980f440 100644 --- a/cli/src/utils/misc/teamUtils.ts +++ b/cli/src/utils/misc/teamUtils.ts @@ -3,6 +3,7 @@ */ export const getTeamId = (teamName: string) => { return String(teamName) + .replace('team-', '') // If team is prefixed by team-, we simply remove it from the string .replace(/[^a-z0-9+]+/gi, '') .toLowerCase(); }; diff --git a/ui/package.json b/ui/package.json index da4f5f3..c8132ad 100644 --- a/ui/package.json +++ b/ui/package.json @@ -24,8 +24,10 @@ "cytoscape": "^3.10.0", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-popper": "^1.0.4", + "date-fns": "^2.2.1", "install": "^0.13.0", "loglevel": "^1.6.3", + "material-color-hash": "^0.1.6", "material-table": "^1.50.0", "npm": "^6.11.3", "react": "^16.9.0", diff --git a/ui/src/components/Charts/ChartJS/VelocityChart.tsx b/ui/src/components/Charts/ChartJS/VelocityChart.tsx index 108b69c..bf8b60a 100644 --- a/ui/src/components/Charts/ChartJS/VelocityChart.tsx +++ b/ui/src/components/Charts/ChartJS/VelocityChart.tsx @@ -37,7 +37,6 @@ class VelocityChart extends Component { if (this.chart.destroy !== undefined) { this.chart.destroy(); } - this.chart = new Chart(myChartRef, { type: 'bar', data: { @@ -88,9 +87,10 @@ class VelocityChart extends Component { const issues = dataset[idx].completion.list; if (issues.length > 0 && this.allowClick === true) { this.allowClick = false; - const keys = issues.map((i: any) => i.key); - const url = - issues[0].host + '/issues/?jql=key in (' + keys.toString() + ')'; + // const keys = issues.map((i: any) => i.key); + // const url = + // issues[0].host + '/issues/?jql=key in (' + keys.toString() + ')'; + const url = issues[0].host + '/issues/?jql=' + issues[0].jql; window.open(url, '_blank'); setTimeout(() => { this.resetAllowClick(); diff --git a/ui/src/components/Charts/ChartJS/VelocityChartStacked.tsx b/ui/src/components/Charts/ChartJS/VelocityChartStacked.tsx new file mode 100644 index 0000000..b81c4a0 --- /dev/null +++ b/ui/src/components/Charts/ChartJS/VelocityChartStacked.tsx @@ -0,0 +1,199 @@ +import React, { Component } from 'react'; // let's also import Component +import { Theme, createStyles, withStyles } from '@material-ui/core/styles'; +import Chart from 'chart.js'; +import toMaterialStyle from 'material-color-hash'; + +const styles = (theme: Theme) => + createStyles({ + root: { + // height: 400 + } + }); + +class VelocityChartStacked extends Component { + chartRef: any = React.createRef(); + chart: any = {}; + allowClick: boolean = true; + + componentDidMount() { + this.buildChart(); + } + + componentDidUpdate() { + this.buildChart(); + } + + resetAllowClick = () => { + this.allowClick = true; + }; + + buildChart = () => { + const { dataset, defaultPoints } = this.props; + const myChartRef = this.chartRef.current.getContext('2d'); + let metric = 'points'; + if (!defaultPoints) { + metric = 'issues'; + } + + if (this.chart.destroy !== undefined) { + this.chart.destroy(); + } + + const days = [ + 'Sunday', + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday' + ]; + const weekDays = []; + // @ts-ignore + for (const [idx, day] of days.entries()) { + weekDays.push({ + label: day, + // @ts-ignore + ...toMaterialStyle('t', (idx + 1) * 100), + // backgroundColor: '#64b5f6', + // borderColor: '#64b5f6', + borderWidth: 2, + data: dataset.map( + (w: any) => + w.weekDays.find((d: any) => d.weekdayTxt === day).completion[metric] + .count + ) + }); + } + this.chart = new Chart(myChartRef, { + type: 'bar', + data: { + datasets: [ + { + label: 'Velocity (rolling average)', + data: dataset.map((w: any) => w.completion[metric].velocity), + backgroundColor: '#ef5350', + fill: false, + type: 'line' + }, + ...weekDays + ], + labels: dataset.map((w: any) => w.legend) + }, + options: { + onClick: this.clickChart, + scales: { + yAxes: [ + { + ticks: { + beginAtZero: true + }, + stacked: true + } + ], + xAxes: [ + { + stacked: true + } + ] + }, + tooltips: { + position: 'nearest', + mode: 'index', + intersect: false, + callbacks: { + label: (tooltipItem: any, data: any) => { + if ( + tooltipItem.datasetIndex === 0 || + parseInt(tooltipItem.value, 10) === 0 + ) { + return ( + data.datasets[tooltipItem.datasetIndex].label + + ': ' + + tooltipItem.value + ); + } + const currentWeek = dataset.find( + (w: any) => w.weekTxt === tooltipItem.xLabel + ); + const currentDay = + currentWeek.weekDays[tooltipItem.datasetIndex - 1]; + return ( + currentDay.weekdayTxt + + ' (' + + currentDay.date.toJSON().slice(6, 10) + + '): ' + + tooltipItem.value + ); + }, + title: (tooltipItems: any, data: any) => { + // console.log(tooltipItems); + // console.log(data); + const currentWeek = dataset.find( + (w: any) => w.weekTxt === tooltipItems[0].xLabel + ); + return ( + currentWeek.weekTxt + ' week starting: ' + currentWeek.weekJira + ); + } + } + } + } + }); + }; + + // https://jsfiddle.net/u1szh96g/208/ + clickChart = (event: any) => { + const { dataset } = this.props; + const activePoints = this.chart.getElementsAtEvent(event); + if (activePoints[0] !== undefined) { + const idx = activePoints[0]._index; + const issues = dataset[idx].completion.list; + if (issues.length > 0 && this.allowClick === true) { + this.allowClick = false; + const clickedWeek = dataset[idx]; + let jqlString = ''; + const activeWeeks = clickedWeek.weekDays.filter( + (d: any) => d.jql !== null + ); + for (const [widx, day] of activeWeeks.entries()) { + jqlString = jqlString + ' (' + day.jql + ')'; + if (widx < activeWeeks.length - 1) { + jqlString = jqlString + ' OR'; + } + } + const url = issues[0].host + '/issues/?jql=' + jqlString; + window.open(url, '_blank'); + setTimeout(() => { + this.resetAllowClick(); + }, 1000); + } + // const issues = dataset[idx].completion.list; + // console.log(issues); + /* + if (issues.length > 0 && this.allowClick === true) { + this.allowClick = false; + const keys = issues.map((i: any) => i.key); + //const url = + // issues[0].host + '/issues/?jql=key in (' + keys.toString() + ')'; + const url = issues[0].host + '/issues/?jql=' + issues[0].jql; + window.open(url, '_blank'); + setTimeout(() => { + this.resetAllowClick(); + }, 1000); + } + */ + } + }; + + render() { + const { classes } = this.props; + return ( +
+ +
+ ); + } +} + +export default withStyles(styles)(VelocityChartStacked); diff --git a/ui/src/components/Charts/Nivo/RoadmapFutureChart.tsx b/ui/src/components/Charts/Nivo/RoadmapFutureChart.tsx index 0d223e8..6e2f301 100644 --- a/ui/src/components/Charts/Nivo/RoadmapFutureChart.tsx +++ b/ui/src/components/Charts/Nivo/RoadmapFutureChart.tsx @@ -1,6 +1,9 @@ import React, { Component } from 'react'; // let's also import Component import { Theme, createStyles, withStyles } from '@material-ui/core/styles'; import { ResponsiveHeatMap } from '@nivo/heatmap'; +import toMaterialStyle from 'material-color-hash'; + +import { getInitiativeTitle } from './utils'; const styles = (theme: Theme) => createStyles({ @@ -30,6 +33,13 @@ class RoadmapFutureChart extends Component { Display different background colors based on the percentage of the effort spent on a particular activity for a week */ getCompletionColor = (data: any, value: any) => { + const { roadmap } = this.props; + const initiative = roadmap.byFutureInitiative.find( + (i: any) => getInitiativeTitle(i) === data.yKey + ); + if (initiative !== undefined) { + return toMaterialStyle(initiative.team.name, 200).backgroundColor; + } return 'rgb(65, 171, 93)'; }; @@ -43,7 +53,7 @@ class RoadmapFutureChart extends Component { const dataset: IDatasetObj[] = []; for (const initiative of roadmap.byFutureInitiative) { const initiativeData: IDatasetObj = { - initiative: initiative.fields.summary + ' (' + initiative.key + ')' + initiative: getInitiativeTitle(initiative) }; for (const week of initiative.weeks) { initiativeData[week.weekTxt] = week[metric].count; diff --git a/ui/src/components/Charts/Nivo/utils.ts b/ui/src/components/Charts/Nivo/utils.ts index e453ff1..e5aa099 100644 --- a/ui/src/components/Charts/Nivo/utils.ts +++ b/ui/src/components/Charts/Nivo/utils.ts @@ -19,7 +19,12 @@ export const getCellDataInitiatives = ( }; export const getInitiativeTitle = (initiative: any) => { - return initiative.fields.summary + ' (' + initiative.key + ')'; + const maxTitleLength = 30; + const initiativeTitle = + initiative.fields.summary.length > maxTitleLength + ? initiative.fields.summary.slice(0, maxTitleLength) + '...' + : initiative.fields.summary; + return initiativeTitle + ' (' + initiative.key + ')'; }; export const getNonInitiativeTitle = () => { diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index d3b2b3c..72c5cf1 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -85,7 +85,7 @@ const Header: FC = ({ setShowMenu, showMenu, pageTitle }) => { {pageTitle} - {window._env_.AUTH0_DISABLED !== true && } + {JSON.parse(window._env_.AUTH0_DISABLED) !== true && } ); diff --git a/ui/src/layout/index.tsx b/ui/src/layout/index.tsx index 2173035..689e9c3 100644 --- a/ui/src/layout/index.tsx +++ b/ui/src/layout/index.tsx @@ -50,7 +50,7 @@ export default function Layout(props: LayoutProps) {
- {window._env_.AUTH0_DISABLED !== true && } + {JSON.parse(window._env_.AUTH0_DISABLED) !== true && }
; + + const days = [ + 'Sunday', + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday' + ]; + const datasetWithDays = dataset.map((week: any) => { + const weekDays = []; + // @ts-ignore + for (const [idx, day] of days.entries()) { + const issuesList = week.completion.list.filter((issue: any) => { + // console.log(issue); + const issueDate = parseISO(issue.closedAt); + if (issueDate.getDay() === idx) { + return true; + } + return false; + }); + weekDays.push({ + weekdayTxt: day, + list: issuesList, + date: issuesList.length === 0 ? null : parseISO(issuesList[0].closedAt), + jql: issuesList.length === 0 ? null : issuesList[0].jql, + completion: { + issues: { + count: issuesList.length + }, + points: { + count: issuesList + .map((i: any) => i.points) + .reduce((acc: number, count: number) => acc + count, 0) + } + } + }); + } + return { ...week, weekDays }; + }); + + return ( + + ); } diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 71e63c9..309ff6d 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -1,7 +1,14 @@ { "compilerOptions": { "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "es5", + "es2015.collection", + "es2015.iterable", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -13,7 +20,9 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react" + "jsx": "react", + "downlevelIteration": true, + "importHelpers": true }, "include": ["src"], "rules": {