diff --git a/.gitignore b/.gitignore
index 156567c1..a31c3081 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,4 +13,6 @@ test/*
.vscode/
robot_test/logs/
*.env
-*.pyc
\ No newline at end of file
+*.pyc
+
+xeno
diff --git a/frontend/.prettierrc b/frontend/.prettierrc
index 8421bbcf..a262ba54 100644
--- a/frontend/.prettierrc
+++ b/frontend/.prettierrc
@@ -1,4 +1,5 @@
{
"singleQuote": true,
- "semi": true
+ "semi": true,
+ "trailingComma": "es5"
}
diff --git a/frontend/package.json b/frontend/package.json
index 67fdbd38..3bb83c3d 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -4,17 +4,22 @@
"private": true,
"dependencies": {
"@emotion/core": "^10.0.21",
+ "apexcharts": "^3.19.2",
"i18next": "^19.4.4",
"normalize.css": "^8.0.1",
"prop-types": "^15.7.2",
"ramda": "^0.27.0",
- "react": "^16.10.2",
+ "react": "^16.13.1",
+ "react-apexcharts": "^1.3.7",
"react-dom": "^16.10.2",
"react-fontawesome": "^1.7.1",
"react-i18next": "^11.4.0",
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
- "react-scripts": "^3.3.0"
+ "react-scripts": "^3.3.0",
+ "react-vega": "^7.3.0",
+ "vega": "^5.13.0",
+ "vega-lite": "^4.13.0"
},
"devDependencies": {
"babel-eslint": "^10.0.3",
diff --git a/frontend/src/components/Card.js b/frontend/src/components/Card.js
index 921ed7b7..08107f70 100644
--- a/frontend/src/components/Card.js
+++ b/frontend/src/components/Card.js
@@ -9,7 +9,7 @@ const Card = ({ team, numberOfSeries }) => {
let history = useHistory();
const Mongolia = css`
- background-color: var(--powder-white);
+ background-color: var(--nero-white);
box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 4px,
rgba(0, 0, 0, 0.23) 0px 3px 4px;
margin: 10px;
diff --git a/frontend/src/components/SelectedTeam.js b/frontend/src/components/SelectedTeam.js
index 185c085e..8f1bd75b 100644
--- a/frontend/src/components/SelectedTeam.js
+++ b/frontend/src/components/SelectedTeam.js
@@ -14,7 +14,7 @@ const SelectedTeam = ({ selectedTeam }) => {
const [t] = useTranslation(['team']);
const cardStyles = css`
- background-color: var(--powder-white);
+ background-color: var(--nero-white);
box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 4px,
rgba(0, 0, 0, 0.23) 0px 3px 4px;
margin: 10px;
@@ -35,7 +35,7 @@ const SelectedTeam = ({ selectedTeam }) => {
margin-top: 0;
}
.cardInfoContainer:hover {
- background-color: var(--mithril-grey);
+ background-color: var(--hermanni-grey);
}
.cardValue {
@@ -55,7 +55,7 @@ const SelectedTeam = ({ selectedTeam }) => {
const flexContainer = {
display: 'flex',
flexWrap: 'wrap',
- paddingTop: '20px'
+ paddingTop: '20px',
};
const TeamCard = ({ data }) => {
@@ -66,7 +66,7 @@ const SelectedTeam = ({ selectedTeam }) => {
last_build,
last_build_id,
last_started,
- last_status
+ last_status,
} = data;
const LastStarted = last_started.slice(0, 16);
@@ -143,8 +143,8 @@ SelectedTeam.propTypes = {
all_builds: PropTypes.object,
name: PropTypes.string,
series: PropTypes.array,
- series_count: PropTypes.number
- })
+ series_count: PropTypes.number,
+ }),
};
export default SelectedTeam;
diff --git a/frontend/src/components/graphs/StatusCount.js b/frontend/src/components/graphs/StatusCount.js
new file mode 100644
index 00000000..aa7a13fc
--- /dev/null
+++ b/frontend/src/components/graphs/StatusCount.js
@@ -0,0 +1,74 @@
+import React, { useEffect } from 'react';
+import Chart from 'react-apexcharts';
+import { useParams } from 'react-router';
+import { pluck } from 'ramda';
+import { useStateValue } from '../../contexts/state';
+import { colorTypes } from '../../utils/colorTypes';
+
+const StatusCount = ({ labels }) => {
+ const { seriesId, buildId } = useParams();
+
+ const [{ statusCount }, dispatch] = useStateValue();
+
+ useEffect(() => {
+ const url = `/data/series/${seriesId}/status_counts/?start_from=${buildId}&builds=1`;
+
+ const fetchData = async () => {
+ dispatch({ type: 'setLoadingState', loadingState: true });
+ try {
+ const res = await fetch(url);
+ const json = await res.json();
+ dispatch({ type: 'setLoadingState', loadingState: false });
+ const statusCount = json.status_counts;
+ dispatch({ type: 'setStatusCount', statusCount });
+ } catch (error) {
+ dispatch({ type: 'setErrorState', errorState: error });
+ }
+ };
+ fetchData();
+ }, [buildId, dispatch, seriesId]);
+
+ const data =
+ statusCount && labels.map(label => pluck(label, statusCount)).flat();
+
+ const series = data;
+ const options = {
+ labels,
+ colors: [
+ colorTypes['semolina red'],
+ colorTypes['pirlo blue'],
+ colorTypes['titan green'],
+ colorTypes['kumpula yellow'],
+ ],
+ plotOptions: {
+ pie: {
+ expandOnClick: true,
+ donut: {
+ labels: {
+ show: true,
+ name: {
+ show: true,
+ },
+ total: {
+ show: true,
+ },
+ },
+ },
+ },
+ },
+ };
+ return (
+
+ {statusCount && (
+
+ )}
+
+ );
+};
+
+export default StatusCount;
diff --git a/frontend/src/components/graphs/SuiteInstability.js b/frontend/src/components/graphs/SuiteInstability.js
new file mode 100644
index 00000000..7fc41061
--- /dev/null
+++ b/frontend/src/components/graphs/SuiteInstability.js
@@ -0,0 +1,276 @@
+import React, { useEffect, useState } from 'react';
+import { useParams } from 'react-router';
+import { VegaLite } from 'react-vega';
+import { useStateValue } from '../../contexts/state';
+/** @jsx jsx */
+import { css, jsx } from '@emotion/core';
+import Loading from '../Loading';
+import { colorTypes } from '../../utils/colorTypes';
+
+const SuiteInstability = () => {
+ const canvasStyles = css`
+ summary {
+ display: none;
+ }
+ `;
+
+ const [selectedSuite, setSelectedSuite] = useState(null);
+ const [
+ { amountOfBuilds, historyDataState, loadingState },
+ dispatch,
+ ] = useStateValue();
+
+ const { seriesId } = useParams();
+
+ const numberOfBuilds = amountOfBuilds || 30; // FIXME: magic
+
+ useEffect(() => {
+ const url = `/data/series/${seriesId}/history?builds=${numberOfBuilds}`;
+
+ const fetchData = async () => {
+ dispatch({ type: 'setLoadingState', loadingState: true });
+ try {
+ const response = await fetch(url, {});
+ const json = await response.json();
+ dispatch({
+ type: 'updateHistory',
+ historyData: json,
+ });
+ dispatch({ type: 'setLoadingState', loadingState: false });
+ } catch (error) {
+ dispatch({ type: 'setErrorState', errorState: error });
+ }
+ };
+ fetchData();
+ }, [dispatch, numberOfBuilds, seriesId]);
+
+ const { buildId } = useParams();
+
+ const correctStatus = () => (buildId ? 'build' : 'series');
+
+ const barSpec = {
+ title: 'Suites with unstable tests',
+ width: 400,
+ height: 200,
+ mark: {
+ type: 'bar',
+ stroke: colorTypes['gradient black'],
+ },
+ background: colorTypes['hermanni grey'],
+ actions: false,
+ selection: {
+ highlight: { type: 'single', empty: 'none', on: 'mouseover' },
+ select: {
+ type: 'single',
+ fields: ['id'],
+ encodings: ['x'],
+ },
+ },
+ encoding: {
+ x: {
+ field: 'name',
+ type: 'ordinal',
+ sort: '-y',
+ axis: { title: 'Suite name' },
+ },
+ y: {
+ field: 'numberOfFailingTests',
+ type: 'quantitative',
+ axis: { title: 'Number of failing tests' },
+ },
+ tooltip: [
+ {
+ field: 'stability',
+ type: 'quantitative',
+ title: 'Stability',
+ },
+ ],
+ fillOpacity: {
+ condition: {
+ selection: 'select',
+ value: 1,
+ },
+ value: 0.3,
+ },
+ strokeWidth: {
+ condition: [
+ {
+ selection: 'highlight',
+ value: 1,
+ },
+ {
+ test: {
+ and: [
+ {
+ selection: 'select',
+ },
+ 'length(data("select_store"))',
+ ],
+ },
+ },
+ ],
+ value: 0,
+ },
+ color: {
+ field: 'stability',
+ type: 'quantitative',
+ sort: 'descending',
+ axis: { title: 'Stability (average)' },
+ legend: {
+ direction: 'horizontal',
+ },
+ },
+ },
+ data: { name: 'failingSuites' },
+ };
+
+ const calculateTestStability = test => {
+ var states = 1;
+ var passes = 0;
+ let previousStatus;
+
+ test['builds'].forEach(testRun => {
+ if (!previousStatus) {
+ previousStatus = testRun.status;
+ } else if (previousStatus !== testRun.status) {
+ states += 1;
+ }
+
+ if (testRun.status === 'PASS') {
+ passes++;
+ }
+ });
+
+ return passes / states / test['builds'].length;
+ };
+
+ const generateBarData = historyDataState => {
+ const suites = [];
+
+ historyDataState['history'].forEach(suite => {
+ const failingTests = [];
+
+ suite['test_cases'].forEach(testCase => {
+ const isFailingTest = testCase['builds'].some(testRun => {
+ return testRun['status'] === 'FAIL';
+ });
+ if (isFailingTest) {
+ const stability = calculateTestStability(testCase);
+ failingTests.push({ name: testCase.name, stability });
+ }
+ });
+
+ if (failingTests.length) {
+ suites.push({
+ id: suite['suite_id'],
+ name: suite['name'],
+ failingTests,
+ });
+ }
+ });
+
+ const failingSuites = suites.map(suite => {
+ const stability =
+ suite.failingTests.reduce(
+ (acc, test) => acc + test.stability,
+ 0
+ ) / suite.failingTests.length;
+
+ return {
+ name: suite.name,
+ numberOfFailingTests: suite.failingTests.length,
+ stability,
+ id: suite.id,
+ };
+ });
+
+ return { failingSuites };
+ };
+
+ if (!historyDataState || loadingState) {
+ return ;
+ }
+
+ const selectSuite = id => {
+ const suite = historyDataState['history'].find(
+ suite => suite['suite_id'] === id
+ );
+
+ if (suite) {
+ setSelectedSuite(suite);
+ }
+ };
+
+ const deselectSuite = () => setSelectedSuite(null);
+
+ const handleBarChartClick = (_name, values) => {
+ if (values.id) {
+ selectSuite(values.id[0]);
+ } else {
+ deselectSuite();
+ }
+ };
+
+ const signalListeners = { select: handleBarChartClick };
+
+ const barData = generateBarData(historyDataState);
+
+ const buildsInTotal = Math.min(
+ historyDataState['max_build_num'],
+ numberOfBuilds
+ );
+
+ const generateStatusRow = testCase => {
+ let statuses = [];
+
+ for (let i = 0; i < buildsInTotal; i++) {
+ const testStatus = testCase['builds'].find(build => {
+ return build['build_number'] === i + 1;
+ });
+
+ if (testStatus) {
+ if (testStatus['status'] === 'PASS') {
+ statuses.push('x');
+ } else if (testStatus['status'] === 'FAIL') {
+ statuses.push('o');
+ }
+ } else {
+ // Impute
+ statuses.push('_');
+ }
+ }
+
+ return statuses;
+ };
+
+ return (
+
+
+ {selectedSuite && (
+
+
+
+ | {selectedSuite.name} |
+ Test history |
+
+
+
+ {selectedSuite['test_cases'].map(testCase => (
+
+ | {testCase.name} |
+ {generateStatusRow(testCase)} |
+
+ ))}
+
+
+ )}
+
+ );
+};
+
+export default SuiteInstability;
diff --git a/frontend/src/components/historyTable/Body.js b/frontend/src/components/historyTable/Body.js
index ef6e8398..2b41fcb8 100644
--- a/frontend/src/components/historyTable/Body.js
+++ b/frontend/src/components/historyTable/Body.js
@@ -7,8 +7,8 @@ const Body = () => {
const [
{
historyDataState: { history },
- amountOfBuilds
- }
+ amountOfBuilds,
+ },
] = useStateValue();
const queryParams = useQueryParams();
diff --git a/frontend/src/components/historyTable/Filter.js b/frontend/src/components/historyTable/Filter.js
index dcf8f1bb..2bd9c8d5 100644
--- a/frontend/src/components/historyTable/Filter.js
+++ b/frontend/src/components/historyTable/Filter.js
@@ -27,8 +27,8 @@ const Filter = () => {
}
.selected {
background-color: transparent;
- border: 2px solid var(--revolution-black);
- color: var(--revolution-black);
+ border: 2px solid var(--gradient-black);
+ color: var(--gradient-black);
}
.button-group {
display: flex;
@@ -38,7 +38,7 @@ const Filter = () => {
border: 1px solid #eee;
width: 100px;
border-radius: 10px;
- background-color: var(--powder-white);
+ background-color: var(--nero-white);
padding: 5px;
margin: 5px;
cursor: pointer;
@@ -76,7 +76,7 @@ const FilterButton = ({ title }) => {
history.push({
pathname: `${location.pathname}`,
search: `?${updateTags(e.target.value)}`,
- state: {}
+ state: {},
});
};
diff --git a/frontend/src/components/historyTable/Table.js b/frontend/src/components/historyTable/Table.js
index 31cc72ec..02cad728 100644
--- a/frontend/src/components/historyTable/Table.js
+++ b/frontend/src/components/historyTable/Table.js
@@ -28,10 +28,10 @@ const Table = () => {
background: #ddd;
}
td {
- background: var(--powder-white);
+ background: var(--nero-white);
}
td.test-result-undefined {
- background: var(--mithril-grey);
+ background: var(--hermanni-grey);
}
.centerTableCellContent {
text-align: center;
@@ -40,8 +40,8 @@ const Table = () => {
`;
const [
{
- historyDataState: { max_build_num }
- }
+ historyDataState: { max_build_num },
+ },
] = useStateValue();
if (max_build_num > 0) {
diff --git a/frontend/src/components/parentData/ParentBuild.js b/frontend/src/components/parentData/ParentBuild.js
index 268931c3..37c83ad5 100644
--- a/frontend/src/components/parentData/ParentBuild.js
+++ b/frontend/src/components/parentData/ParentBuild.js
@@ -9,9 +9,9 @@ const ParentSeries = () => {
const { seriesId, buildId, testId } = useParams();
const [
{
- parentData: { buildData }
+ parentData: { buildData },
},
- dispatch
+ dispatch,
] = useStateValue();
useEffect(() => {
diff --git a/frontend/src/contexts/reducer.js b/frontend/src/contexts/reducer.js
index bc773e78..56c5dd70 100644
--- a/frontend/src/contexts/reducer.js
+++ b/frontend/src/contexts/reducer.js
@@ -3,68 +3,68 @@ const reducer = (state, action) => {
case 'updateHistory':
return {
...state,
- historyDataState: action.historyData
+ historyDataState: action.historyData,
};
case 'setAmountOfBuilds':
return {
...state,
- amountOfBuilds: action.amountOfBuilds
+ amountOfBuilds: action.amountOfBuilds,
};
case 'setLoadingState':
return {
...state,
- loadingState: action.loadingState
+ loadingState: action.loadingState,
};
case 'setErrorState':
return {
...state,
- errorState: action.errorState
+ errorState: action.errorState,
};
case 'setHistoryFilterType':
return {
...state,
historyFilter: {
filterType: action.filterType,
- isChecked: action.isChecked
- }
+ isChecked: action.isChecked,
+ },
};
case 'setHistoryFilterPass':
return {
...state,
historyFilterPass: {
filterType: action.filterType,
- isChecked: action.isChecked
- }
+ isChecked: action.isChecked,
+ },
};
case 'setHistoryFilterFail':
return {
...state,
historyFilterFail: {
filterType: action.filterType,
- isChecked: action.isChecked
- }
+ isChecked: action.isChecked,
+ },
};
case 'setLastRunFilterFail':
return {
...state,
lastRunFilterFail: {
filterType: action.filterType,
- isChecked: action.isChecked
- }
+ isChecked: action.isChecked,
+ },
};
case 'setLastRunFilterPass':
return {
...state,
lastRunFilterPass: {
filterType: action.filterType,
- isChecked: action.isChecked
- }
+ isChecked: action.isChecked,
+ },
};
case 'setBranches':
return {
...state,
- branchesState: action.branches
+ branchesState: action.branches,
};
case 'setSelectedBranch':
return {
@@ -72,44 +72,49 @@ const reducer = (state, action) => {
selectedBranchState: {
name: action.name,
id: action.id,
- team: action.team
- }
+ team: action.team,
+ },
};
case 'setMetadata':
return {
...state,
- metadataState: action.metadata
+ metadataState: action.metadata,
};
case 'setSelectedBuild':
return {
...state,
- selectedBuildState: action.selectedBuild
+ selectedBuildState: action.selectedBuild,
};
case 'setTeams':
return {
...state,
- teamsState: action.teams
+ teamsState: action.teams,
};
case 'setSelectedSuiteState':
return {
...state,
- selectedSuiteState: action.suite
+ selectedSuiteState: action.suite,
};
case 'setSeriesData':
return {
...state,
parentData: {
...state.parentData,
- seriesData: action.seriesData
- }
+ seriesData: action.seriesData,
+ },
};
case 'setBuildData':
return {
...state,
parentData: {
...state.parentData,
- buildData: action.buildData
- }
+ buildData: action.buildData,
+ },
+ };
+ case 'setStatusCount':
+ return {
+ ...state,
+ statusCount: action.statusCount,
};
default:
return state;
diff --git a/frontend/src/contexts/state.js b/frontend/src/contexts/state.js
index 1bb3b64e..b1cec911 100644
--- a/frontend/src/contexts/state.js
+++ b/frontend/src/contexts/state.js
@@ -10,11 +10,11 @@ const initialState = {
amountFilteredData: null,
lastRunFilterFail: {
isChecked: false,
- filterType: ''
+ filterType: '',
},
lastRunFilterPass: {
isChecked: false,
- filterType: ''
+ filterType: '',
},
branchesState: null,
selectedBranchState: { name: 'All builds', id: 1 },
@@ -22,8 +22,9 @@ const initialState = {
selectedBuildState: {},
parentData: {
seriesData: null,
- buildData: null
- }
+ buildData: null,
+ },
+ statusCount: null,
};
export const StateProvider = ({ reducer, children }) => {
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 27f9a63d..8eebf432 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -21,12 +21,16 @@ html {
----------------------*/
:root {
--nelson-purple: #b8557b;
- --whipped-red: #c63757;
+ --semolina-red: #c63757;
--pirlo-blue: #56a2c3;
- --revolution-black: #141312;
- --dove-grey: #7b756f;
- --mithril-grey: #e9e8e8;
- --powder-white: #ffffff;
+ --titan-green: #2e8d6e;
+ --gradient-black: #141312;
+ --evidence-grey: #7b756f;
+ --hermanni-grey: #edecec;
+ --toukola-green: #e9f5e1;
+ --kumpula-yellow: #faf3e1;
+ --vallila-blue: #e1f1f7;
+ --nero-white: #ffffff;
}
/** Basics **/
diff --git a/frontend/src/pages/Build.js b/frontend/src/pages/Build.js
index 1b268f11..2d9cb64f 100644
--- a/frontend/src/pages/Build.js
+++ b/frontend/src/pages/Build.js
@@ -26,7 +26,7 @@ const Build = () => {
`;
const [
{ loadingState, historyDataState, selectedBranchState, branchesState },
- dispatch
+ dispatch,
] = useStateValue();
let { buildId, seriesId } = useParams();
@@ -45,7 +45,7 @@ const Build = () => {
dispatch({ type: 'setLoadingState', loadingState: false });
dispatch({
type: 'setMetadata',
- metadata: json
+ metadata: json,
});
} catch (error) {
//console.log(error);
@@ -62,7 +62,7 @@ const Build = () => {
type: 'setSelectedBranch',
name: branch?.name,
id: branch_id,
- team: branch?.team || ' '
+ team: branch?.team || ' ',
});
dispatch({ type: 'setSelectedBuild', selectedBuild: buildId });
try {
@@ -74,7 +74,7 @@ const Build = () => {
dispatch({ type: 'setLoadingState', loadingState: false });
dispatch({
type: 'updateHistory',
- historyData: json
+ historyData: json,
});
} catch (error) {
dispatch({ type: 'setErrorState', errorState: error });
diff --git a/frontend/src/pages/Dashboard.js b/frontend/src/pages/Dashboard.js
index 3651402f..d9dd3348 100644
--- a/frontend/src/pages/Dashboard.js
+++ b/frontend/src/pages/Dashboard.js
@@ -1,17 +1,37 @@
import React from 'react';
-import { useParams } from 'react-router';
+import { useLocation } from 'react-router-dom';
+/** @jsx jsx */
+import { css, jsx } from '@emotion/core';
+import SuiteInstability from '../components/graphs/SuiteInstability';
+import StatusCount from '../components/graphs/StatusCount';
import BreadcrumbNav from '../components/BreadcrumbNav';
+import { suiteLabels, testLabels } from '../utils/graphTypes';
const Dashboard = () => {
- const { buildId } = useParams();
+ const pathname = useLocation().pathname;
+ const buildUrl = pathname.includes('build');
- const correctStatus = () => (buildId ? 'build' : 'series');
+ const status = buildUrl ? 'build' : 'series';
+
+ const dashBoardStyles = css`
+ .pieContainer {
+ padding: 20px;
+ display: flex;
+ flex-wrap: wrap;
+ }
+ `;
return (
-
-
- Dashboard
-
+
+
+ {buildUrl && (
+
+ {' '}
+
+
+ )}
+
+
);
};
diff --git a/frontend/src/pages/History.js b/frontend/src/pages/History.js
index f77d587f..110b89d1 100644
--- a/frontend/src/pages/History.js
+++ b/frontend/src/pages/History.js
@@ -37,9 +37,9 @@ const History = () => {
historyDataState,
selectedBranchState,
amountOfBuilds,
- branchesState
+ branchesState,
},
- dispatch
+ dispatch,
] = useStateValue();
const { seriesId } = useParams();
const queryParams = useQueryParams();
@@ -57,20 +57,20 @@ const History = () => {
dispatch({ type: 'setLoadingState', loadingState: true });
dispatch({
type: 'setAmountOfBuilds',
- amountOfBuilds: number_of_builds
+ amountOfBuilds: number_of_builds,
});
dispatch({
type: 'setSelectedBranch',
name: branch?.name || ' ',
id: series_id,
- team: branch?.team || ' '
+ team: branch?.team || ' ',
});
try {
const res = await fetch(url, {});
const json = await res.json();
dispatch({
type: 'updateHistory',
- historyData: json
+ historyData: json,
});
dispatch({ type: 'setLoadingState', loadingState: false });
} catch (error) {
diff --git a/frontend/src/utils/colorTypes.js b/frontend/src/utils/colorTypes.js
new file mode 100644
index 00000000..a9948df1
--- /dev/null
+++ b/frontend/src/utils/colorTypes.js
@@ -0,0 +1,13 @@
+export const colorTypes = {
+ 'gradient black': '#141312',
+ 'evidence grey': '#7B756F',
+ 'nero white': '#ffffff',
+ 'nelson purple': '#b8557b',
+ 'semolina red': '#C63757',
+ 'pirlo blue': '#56a2c3',
+ 'titan green': '#2E8D6E',
+ 'kumpula yellow': '#FAF3E1',
+ 'toukola green': '#E9F5E1',
+ 'vallila blue': '#E1F1F7',
+ 'hermanni grey': '#EDECEC',
+};
diff --git a/frontend/src/utils/graphTypes.js b/frontend/src/utils/graphTypes.js
new file mode 100644
index 00000000..35128055
--- /dev/null
+++ b/frontend/src/utils/graphTypes.js
@@ -0,0 +1,13 @@
+export const suiteLabels = [
+ 'suites_failed',
+ 'suites_other',
+ 'suites_passed',
+ 'suites_skipped',
+];
+
+export const testLabels = [
+ 'tests_failed',
+ 'tests_other',
+ 'tests_passed',
+ 'tests_skipped',
+];
diff --git a/frontend/src/utils/parentDataTypes.js b/frontend/src/utils/parentDataTypes.js
index a467a622..6ebc3641 100644
--- a/frontend/src/utils/parentDataTypes.js
+++ b/frontend/src/utils/parentDataTypes.js
@@ -5,12 +5,12 @@ export const buildTypes = [
'build_number',
'build_id',
'status',
- 'start_time'
+ 'start_time',
];
export const suiteTypes = [
'team',
'name',
'build_number',
'build_id',
- 'start_time'
+ 'start_time',
];