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:
Armen Zambrano G 2018-12-19 15:21:29 -05:00 коммит произвёл Armen Zambrano
Родитель 55fa4a8a37
Коммит c8a4c77a61
11 изменённых файлов: 370 добавлений и 7 удалений

Просмотреть файл

@ -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>

Просмотреть файл

@ -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"