Issue #7 - Add graphs for components detail view
We want to see how well a bucket has been triaged over the past few weeks.
This commit is contained in:
Родитель
55fa4a8a37
Коммит
c8a4c77a61
|
@ -26,9 +26,11 @@
|
|||
"@material-ui/core": "^3.5.1",
|
||||
"@material-ui/icons": "^3.0.1",
|
||||
"@mozilla-frontend-infra/components": "^2.0.0",
|
||||
"chart.js": "^2.7.3",
|
||||
"prop-types": "^15",
|
||||
"query-string": "^6.2.0",
|
||||
"react": "^16",
|
||||
"react-chartjs-2": "^2.7.4",
|
||||
"react-dom": "^16",
|
||||
"react-hot-loader": "^4",
|
||||
"react-router-dom": "^4.3.1",
|
||||
|
|
|
@ -2,6 +2,8 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import DetailView from '../DetailView';
|
||||
import BugzillaGraph from '../../containers/BugzillaGraph';
|
||||
import METRICS from '../../utils/bugzilla/metrics';
|
||||
|
||||
const styles = ({
|
||||
subtitle: {
|
||||
|
@ -10,14 +12,32 @@ const styles = ({
|
|||
},
|
||||
metric: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '0.5fr 0.5fr',
|
||||
gridTemplateColumns: '100px 20px',
|
||||
},
|
||||
metricLabel: {
|
||||
textTransform: 'capitalize',
|
||||
},
|
||||
metricLink: {
|
||||
textAlign: 'center',
|
||||
textAlign: 'right',
|
||||
},
|
||||
graphs: {
|
||||
display: 'flex',
|
||||
},
|
||||
});
|
||||
|
||||
const constructQuery = (metrics, product, component) => Object.values(metrics).map((metric) => {
|
||||
const { label, parameters } = metric;
|
||||
// We need all bugs regardless of their resolution in order to decrease/increase
|
||||
// the number of open bugs per date
|
||||
delete parameters.resolution;
|
||||
return {
|
||||
label,
|
||||
parameters: {
|
||||
product,
|
||||
component,
|
||||
...parameters,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const BugzillaComponentDetails = ({
|
||||
|
@ -26,7 +46,7 @@ const BugzillaComponentDetails = ({
|
|||
<DetailView title={`${product}::${component}`} onGoBack={onGoBack}>
|
||||
<div>
|
||||
<h4 className={classes.subtitle}>{bugzillaEmail}</h4>
|
||||
{Object.keys(metrics).map(metric => (
|
||||
{Object.keys(metrics).sort().map(metric => (
|
||||
metrics[metric] && (
|
||||
<div key={metric} className={classes.metric}>
|
||||
<span className={classes.metricLabel}>{metric}</span>
|
||||
|
@ -36,6 +56,10 @@ const BugzillaComponentDetails = ({
|
|||
</div>
|
||||
)
|
||||
))}
|
||||
<BugzillaGraph
|
||||
label={`${product}::${component}`}
|
||||
queries={constructQuery(METRICS, product, component)}
|
||||
/>
|
||||
</div>
|
||||
</DetailView>
|
||||
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Chart from 'react-chartjs-2';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import generateOptions from '../../utils/chartJs/generateOptions';
|
||||
|
||||
const styles = {
|
||||
// This div helps with canvas size changes
|
||||
// https://www.chartjs.org/docs/latest/general/responsive.html#important-note
|
||||
chartContainer: {
|
||||
width: '800px',
|
||||
},
|
||||
};
|
||||
|
||||
const ChartJsWrapper = ({
|
||||
classes, data, options, title, type,
|
||||
}) => (
|
||||
<div className={classes.chartContainer}>
|
||||
{title && <h2>{title}</h2>}
|
||||
<Chart
|
||||
type={type}
|
||||
data={data}
|
||||
options={generateOptions(options)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
// The properties are to match ChartJs properties
|
||||
ChartJsWrapper.propTypes = {
|
||||
classes: PropTypes.shape({}).isRequired,
|
||||
options: PropTypes.shape({
|
||||
reverse: PropTypes.bool,
|
||||
scaleLabel: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
tooltipFormat: PropTypes.bool,
|
||||
tooltips: PropTypes.shape({
|
||||
callbacks: PropTypes.object,
|
||||
}),
|
||||
ticksCallback: PropTypes.func,
|
||||
}).isRequired,
|
||||
data: PropTypes.shape({
|
||||
datasets: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
// There can be more properties than data and value,
|
||||
// however, we mainly care about these as a minimum requirement
|
||||
data: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
x: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.instanceOf(Date),
|
||||
]).isRequired,
|
||||
y: PropTypes.number.isRequired,
|
||||
}),
|
||||
),
|
||||
label: PropTypes.string.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
}).isRequired,
|
||||
title: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
};
|
||||
|
||||
ChartJsWrapper.defaultProps = {
|
||||
title: '',
|
||||
type: 'scatter',
|
||||
};
|
||||
|
||||
export default withStyles(styles)(ChartJsWrapper);
|
|
@ -0,0 +1,56 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ChartJsWrapper from '../../components/ChartJsWrapper';
|
||||
import generateChartJsData from '../../utils/bugzilla/generateChartJsData';
|
||||
|
||||
class BugzillaGraph extends Component {
|
||||
state = {
|
||||
data: null,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
async fetchData() {
|
||||
const { queries, chartType, startDate } = this.props;
|
||||
this.setState(await generateChartJsData(queries, chartType, startDate));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data } = this.state;
|
||||
const { chartType, title } = this.props;
|
||||
|
||||
return data ? (
|
||||
<ChartJsWrapper
|
||||
type={chartType}
|
||||
data={data}
|
||||
options={{ scaleLabel: '# of bugs' }}
|
||||
title={title}
|
||||
/>
|
||||
) : <div />;
|
||||
}
|
||||
}
|
||||
|
||||
BugzillaGraph.propTypes = {
|
||||
queries: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.string.isRequired,
|
||||
parameters: PropTypes.shape({
|
||||
include_fields: PropTypes.string,
|
||||
component: PropTypes.string,
|
||||
resolution: PropTypes.string,
|
||||
priority: PropTypes.string,
|
||||
}),
|
||||
})).isRequired,
|
||||
chartType: PropTypes.oneOf(['scatter', 'line']),
|
||||
startDate: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
};
|
||||
|
||||
BugzillaGraph.defaultProps = {
|
||||
chartType: 'line',
|
||||
startDate: null,
|
||||
title: '',
|
||||
};
|
||||
|
||||
export default BugzillaGraph;
|
|
@ -81,6 +81,7 @@ class MainContainer extends Component {
|
|||
const { metrics } = bugzillaComponents[`${product}::${component}`];
|
||||
await Promise.all(Object.keys(METRICS).map(async (metric) => {
|
||||
metrics[metric] = await getBugsCountAndLink(product, component, metric);
|
||||
metrics[metric].label = METRICS[metric].label;
|
||||
}));
|
||||
this.setState({ bugzillaComponents });
|
||||
});
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
import queryBugzilla from './queryBugzilla';
|
||||
import generateDatasetStyle from '../chartJs/generateDatasetStyle';
|
||||
import COLORS from '../chartJs/colors';
|
||||
|
||||
const newDate = (datetime, startDate) => {
|
||||
const onlyDate = datetime.substring(0, 10);
|
||||
return startDate && (onlyDate < startDate) ? startDate : onlyDate;
|
||||
};
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
// Count bugs created on each day
|
||||
// startDate allow us to group bugs older than such date
|
||||
const bugsPerDay = (bugs, startDate) => (
|
||||
bugs.reduce((result, { creation_time, cf_last_resolved }) => {
|
||||
const newResult = Object.assign({}, result);
|
||||
const createdDate = newDate(creation_time, startDate);
|
||||
if (!newResult[createdDate]) {
|
||||
newResult[createdDate] = 0;
|
||||
}
|
||||
newResult[createdDate] += 1;
|
||||
|
||||
if (cf_last_resolved) {
|
||||
const resolvedDate = newDate(cf_last_resolved, startDate);
|
||||
if (!newResult[resolvedDate]) {
|
||||
newResult[resolvedDate] = 0;
|
||||
}
|
||||
newResult[resolvedDate] -= 1;
|
||||
}
|
||||
|
||||
return newResult;
|
||||
}, {})
|
||||
);
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
const bugsByCreationDate = (bugs, startDate) => {
|
||||
// Count bugs created on each day
|
||||
const byCreationDate = bugsPerDay(bugs, startDate);
|
||||
|
||||
let count = 0;
|
||||
let lastDataPoint;
|
||||
const accumulatedCount = Object.keys(byCreationDate)
|
||||
.sort().reduce((result, date) => {
|
||||
count += byCreationDate[date];
|
||||
// Read more here http://momentjs.com/guides/#/warnings/js-date/
|
||||
lastDataPoint = { x: new Date(date), y: count };
|
||||
result.push(lastDataPoint);
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
// This guarantees that the line goes all the way to the end of the graph
|
||||
const today = new Date();
|
||||
if (lastDataPoint.x !== today) {
|
||||
accumulatedCount.push({ x: today, y: count });
|
||||
}
|
||||
|
||||
return accumulatedCount;
|
||||
};
|
||||
|
||||
const dataFormatter = (bugSeries, chartType, startDate) => {
|
||||
const newData = { data: { datasets: [] } };
|
||||
|
||||
bugSeries.forEach(({ bugs, label }, index) => {
|
||||
const bugCountPerDay = bugsByCreationDate(bugs, startDate);
|
||||
newData.data.datasets.push({
|
||||
...generateDatasetStyle(chartType, COLORS[index]),
|
||||
data: bugCountPerDay,
|
||||
label,
|
||||
});
|
||||
});
|
||||
|
||||
return newData;
|
||||
};
|
||||
|
||||
|
||||
// It formats the data and options to meet chartJs' data structures
|
||||
// startDate enables counting into a starting date all previous data points
|
||||
const generateChartJsData = async (queries = [], startDate) => {
|
||||
const data = await Promise.all(
|
||||
queries.map(async ({ label, parameters }) => ({
|
||||
label,
|
||||
...(await queryBugzilla(parameters)),
|
||||
})),
|
||||
);
|
||||
return dataFormatter(data, startDate);
|
||||
};
|
||||
|
||||
export default generateChartJsData;
|
|
@ -0,0 +1,4 @@
|
|||
// Less than ideal but it works
|
||||
const COLORS = ['#e55525', '#ffcd02', '#45a1ff', '#b2ff46', '#fd79ff'];
|
||||
|
||||
export default COLORS;
|
|
@ -0,0 +1,19 @@
|
|||
const generateLineChartStyle = color => ({
|
||||
backgroundColor: color,
|
||||
borderColor: color,
|
||||
fill: false,
|
||||
pointRadius: '0',
|
||||
pointHoverBackgroundColor: 'white',
|
||||
});
|
||||
|
||||
const generateScatterChartStyle = color => ({
|
||||
backgroundColor: color,
|
||||
});
|
||||
|
||||
const generateDatasetStyle = (type, color) => (
|
||||
type === 'scatter'
|
||||
? generateScatterChartStyle(color)
|
||||
: generateLineChartStyle(color)
|
||||
);
|
||||
|
||||
export default generateDatasetStyle;
|
|
@ -0,0 +1,55 @@
|
|||
const generateOptions = ({
|
||||
reverse = false, scaleLabel, title, tooltipFormat, tooltips, ticksCallback,
|
||||
}) => {
|
||||
const chartJsOptions = {
|
||||
legend: {
|
||||
labels: {
|
||||
boxWidth: 10,
|
||||
fontSize: 10,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
type: 'time',
|
||||
time: {
|
||||
displayFormats: { hour: 'MMM D' },
|
||||
},
|
||||
}],
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
reverse,
|
||||
},
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
if (ticksCallback) {
|
||||
chartJsOptions.scales.yAxes[0].ticks.callback = ticksCallback;
|
||||
}
|
||||
|
||||
if (title) {
|
||||
chartJsOptions.title = {
|
||||
display: true,
|
||||
text: title,
|
||||
};
|
||||
}
|
||||
|
||||
if (tooltipFormat) {
|
||||
chartJsOptions.scales.xAxes[0].time.tooltipFormat = 'll';
|
||||
}
|
||||
|
||||
if (tooltips) {
|
||||
chartJsOptions.tooltips = tooltips;
|
||||
}
|
||||
|
||||
if (scaleLabel) {
|
||||
chartJsOptions.scales.yAxes[0].scaleLabel = {
|
||||
display: true,
|
||||
labelString: scaleLabel,
|
||||
};
|
||||
}
|
||||
return chartJsOptions;
|
||||
};
|
||||
|
||||
export default generateOptions;
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
exports[`renders the details for a Bugzilla component 1`] = `
|
||||
<div
|
||||
className="DetailView-root-5"
|
||||
className="DetailView-root-6"
|
||||
>
|
||||
<div>
|
||||
<a
|
||||
|
@ -12,7 +12,7 @@ exports[`renders the details for a Bugzilla component 1`] = `
|
|||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="MuiSvgIcon-root-7"
|
||||
className="MuiSvgIcon-root-8"
|
||||
focusable="false"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
|
@ -29,7 +29,7 @@ exports[`renders the details for a Bugzilla component 1`] = `
|
|||
</div>
|
||||
<div>
|
||||
<h2
|
||||
className="DetailView-title-6"
|
||||
className="DetailView-title-7"
|
||||
>
|
||||
Core::DOM: Core & HTML
|
||||
</h2>
|
||||
|
@ -54,6 +54,7 @@ exports[`renders the details for a Bugzilla component 1`] = `
|
|||
944
|
||||
</a>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
48
yarn.lock
48
yarn.lock
|
@ -2028,6 +2028,29 @@ chardet@^0.7.0:
|
|||
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
|
||||
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
|
||||
|
||||
chart.js@^2.7.3:
|
||||
version "2.7.3"
|
||||
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.7.3.tgz#cdb61618830bf216dc887e2f7b1b3c228b73c57e"
|
||||
integrity sha512-3+7k/DbR92m6BsMUYP6M0dMsMVZpMnwkUyNSAbqolHKsbIzH2Q4LWVEHHYq7v0fmEV8whXE0DrjANulw9j2K5g==
|
||||
dependencies:
|
||||
chartjs-color "^2.1.0"
|
||||
moment "^2.10.2"
|
||||
|
||||
chartjs-color-string@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.5.0.tgz#8d3752d8581d86687c35bfe2cb80ac5213ceb8c1"
|
||||
integrity sha512-amWNvCOXlOUYxZVDSa0YOab5K/lmEhbFNKI55PWc4mlv28BDzA7zaoQTGxSBgJMHIW+hGX8YUrvw/FH4LyhwSQ==
|
||||
dependencies:
|
||||
color-name "^1.0.0"
|
||||
|
||||
chartjs-color@^2.1.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.2.0.tgz#84a2fb755787ed85c39dd6dd8c7b1d88429baeae"
|
||||
integrity sha1-hKL7dVeH7YXDndbdjHsdiEKbrq4=
|
||||
dependencies:
|
||||
chartjs-color-string "^0.5.0"
|
||||
color-convert "^0.5.3"
|
||||
|
||||
chokidar@^2.0.0, chokidar@^2.0.2:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26"
|
||||
|
@ -2159,6 +2182,11 @@ collection-visit@^1.0.0:
|
|||
map-visit "^1.0.0"
|
||||
object-visit "^1.0.0"
|
||||
|
||||
color-convert@^0.5.3:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd"
|
||||
integrity sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=
|
||||
|
||||
color-convert@^1.9.0:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
|
@ -2171,6 +2199,11 @@ color-name@1.1.3:
|
|||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
|
||||
|
||||
color-name@^1.0.0:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
|
||||
combined-stream@^1.0.6, combined-stream@~1.0.6:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828"
|
||||
|
@ -5949,6 +5982,11 @@ mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0:
|
|||
dependencies:
|
||||
minimist "0.0.8"
|
||||
|
||||
moment@^2.10.2:
|
||||
version "2.23.0"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.23.0.tgz#759ea491ac97d54bac5ad776996e2a58cc1bc225"
|
||||
integrity sha512-3IE39bHVqFbWWaPOMHZF98Q9c3LDKGTmypMiTM2QygGXXElkFWIH7GxfmlwmY2vwa+wmNsoYZmG2iusf1ZjJoA==
|
||||
|
||||
move-concurrently@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
|
||||
|
@ -6851,7 +6889,7 @@ prompts@^0.1.9:
|
|||
kleur "^2.0.1"
|
||||
sisteransi "^0.1.1"
|
||||
|
||||
prop-types@^15, prop-types@^15.5.4, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2:
|
||||
prop-types@^15, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2:
|
||||
version "15.6.2"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
|
||||
integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==
|
||||
|
@ -7011,6 +7049,14 @@ rc@^1.2.7:
|
|||
minimist "^1.2.0"
|
||||
strip-json-comments "~2.0.1"
|
||||
|
||||
react-chartjs-2@^2.7.4:
|
||||
version "2.7.4"
|
||||
resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-2.7.4.tgz#e41ea4e81491dc78347111126a48e96ee57db1a6"
|
||||
integrity sha512-lXTpBaDlk9rIMjRONjZd76dIUhEm3vOp2jOrJrsFG/UpFI5VqX8Xw83apVHTnUGJ968f8i/i/syLddls4NHy2g==
|
||||
dependencies:
|
||||
lodash "^4.17.4"
|
||||
prop-types "^15.5.8"
|
||||
|
||||
react-codemirror2@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-codemirror2/-/react-codemirror2-5.1.0.tgz#62de4460178adea40eb52eabf7491669bf3794b8"
|
||||
|
|
Загрузка…
Ссылка в новой задаче