From cda99a212345145014891578f675f49b1c75dde6 Mon Sep 17 00:00:00 2001 From: jps Date: Wed, 3 Jun 2020 19:25:57 +0300 Subject: [PATCH 01/10] Updated gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 From ca27ba86f5737b42f39c5bed647bc1b72f401102 Mon Sep 17 00:00:00 2001 From: jps Date: Wed, 3 Jun 2020 19:26:33 +0300 Subject: [PATCH 02/10] Suite instability bar chart --- frontend/package.json | 7 +- frontend/src/pages/Dashboard.js | 145 +++++++++++++++++++++++++++++++- 2 files changed, 146 insertions(+), 6 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 67fdbd38..98ad773f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,13 +8,16 @@ "normalize.css": "^8.0.1", "prop-types": "^15.7.2", "ramda": "^0.27.0", - "react": "^16.10.2", + "react": "^16.13.1", "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/pages/Dashboard.js b/frontend/src/pages/Dashboard.js index 3651402f..d0b2caf8 100644 --- a/frontend/src/pages/Dashboard.js +++ b/frontend/src/pages/Dashboard.js @@ -1,17 +1,154 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { useParams } from 'react-router'; import BreadcrumbNav from '../components/BreadcrumbNav'; +import { VegaLite } from 'react-vega'; +import { useStateValue } from '../contexts/state'; +import Loading from '../components/Loading'; const Dashboard = () => { + const seriesId = 8; + const numberOfBuilds = 10; + + const [{ historyDataState, loadingState }, dispatch] = useStateValue(); + + 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]); + const { buildId } = useParams(); const correctStatus = () => (buildId ? 'build' : 'series'); + const spec = { + width: 400, + height: 200, + mark: 'bar', + background: '#e9e8e8', // TODO: from variable + actions: false, + encoding: { + x: { + field: 'a', + type: 'ordinal', + sort: '-y', + axis: { title: 'Suite name' } + }, + y: { + field: 'b', + type: 'quantitative', + axis: { title: 'Number of failing tests' } + }, + color: { + field: 'c', + type: 'quantitative', + sort: 'descending', + axis: { title: 'Stability (average)' } + }, + tooltip: { field: 'c', type: 'quantitative' } + }, + data: { name: 'table' } + // signals: [ + // { + // name: 'tooltip', + // value: {}, + // on: [ + // { events: 'rect:mouseover', update: 'datum' }, + // { events: 'rect:mouseout', update: {} } + // ] + // } + // ] + // Did not have time to figure this out yet + }; + + 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({ + name: suite['name'], + failingTests: failingTests + }); + } + }); + + const table = suites.map(suite => { + const stability = + suite.failingTests.reduce( + (acc, test) => acc + test.stability, + 0 + ) / suite.failingTests.length; + + return { + a: suite.name, + b: suite.failingTests.length, + c: stability + }; + }); + + return { + table: table + }; + }; + + if (!historyDataState || loadingState) { + return ; + } + + const barData = generateBarData(historyDataState); + return ( -
+ <> - Dashboard -
+ + ); }; From fc73a0cedcdba1cb4306c7ce4d97cdcc0ce6f55b Mon Sep 17 00:00:00 2001 From: jps Date: Thu, 4 Jun 2020 10:42:49 +0300 Subject: [PATCH 03/10] Turn on trailing commas in Prettier --- frontend/.prettierrc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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" } From 3787988fbe70a3867bca5b94af7b49cb1c780c8f Mon Sep 17 00:00:00 2001 From: jps Date: Thu, 4 Jun 2020 18:55:59 +0300 Subject: [PATCH 04/10] Interactive chart PoC --- frontend/src/pages/Dashboard.js | 175 ++++++++++++++++++++++++++------ 1 file changed, 142 insertions(+), 33 deletions(-) diff --git a/frontend/src/pages/Dashboard.js b/frontend/src/pages/Dashboard.js index d0b2caf8..5326a37d 100644 --- a/frontend/src/pages/Dashboard.js +++ b/frontend/src/pages/Dashboard.js @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { useParams } from 'react-router'; import BreadcrumbNav from '../components/BreadcrumbNav'; import { VegaLite } from 'react-vega'; @@ -9,6 +9,7 @@ const Dashboard = () => { const seriesId = 8; const numberOfBuilds = 10; + const [selectedSuite, setSelectedSuite] = useState(null); const [{ historyDataState, loadingState }, dispatch] = useStateValue(); useEffect(() => { @@ -21,7 +22,7 @@ const Dashboard = () => { const json = await response.json(); dispatch({ type: 'updateHistory', - historyData: json + historyData: json, }); dispatch({ type: 'setLoadingState', loadingState: false }); } catch (error) { @@ -35,44 +36,80 @@ const Dashboard = () => { const correctStatus = () => (buildId ? 'build' : 'series'); - const spec = { + const barSpec = { + title: 'Suites with unstable tests', width: 400, height: 200, - mark: 'bar', - background: '#e9e8e8', // TODO: from variable + mark: { + type: 'bar', + stroke: '#141312', // TODO: from variable: --revolution-black + }, + background: '#e9e8e8', // TODO: from variable: --mithril-grey actions: false, + selection: { + highlight: { type: 'single', empty: 'none', on: 'mouseover' }, + select: { + type: 'single', + fields: ['id'], + encodings: ['x'], + }, + }, encoding: { x: { - field: 'a', + field: 'name', type: 'ordinal', sort: '-y', - axis: { title: 'Suite name' } + axis: { title: 'Suite name' }, }, y: { - field: 'b', + field: 'numberOfFailingTests', type: 'quantitative', - axis: { title: 'Number of failing tests' } + 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: 'c', + field: 'stability', type: 'quantitative', sort: 'descending', - axis: { title: 'Stability (average)' } + axis: { title: 'Stability (average)' }, + legend: { + direction: 'horizontal', + }, }, - tooltip: { field: 'c', type: 'quantitative' } }, - data: { name: 'table' } - // signals: [ - // { - // name: 'tooltip', - // value: {}, - // on: [ - // { events: 'rect:mouseover', update: 'datum' }, - // { events: 'rect:mouseout', update: {} } - // ] - // } - // ] - // Did not have time to figure this out yet + data: { name: 'failingSuites' }, }; const calculateTestStability = test => { @@ -113,13 +150,14 @@ const Dashboard = () => { if (failingTests.length) { suites.push({ + id: suite['suite_id'], name: suite['name'], - failingTests: failingTests + failingTests: failingTests, }); } }); - const table = suites.map(suite => { + const failingSuites = suites.map(suite => { const stability = suite.failingTests.reduce( (acc, test) => acc + test.stability, @@ -127,27 +165,98 @@ const Dashboard = () => { ) / suite.failingTests.length; return { - a: suite.name, - b: suite.failingTests.length, - c: stability + name: suite.name, + numberOfFailingTests: suite.failingTests.length, + stability: stability, + id: suite.id, }; }); - return { - table: table - }; + 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['test_cases'].map(testCase => ( + + + + + ))} + +
{selectedSuite.name}Test history
{testCase.name}{generateStatusRow(testCase)}
+ )} ); }; From 3adaf42c7b234801f36cd80ab9693a4202b11b44 Mon Sep 17 00:00:00 2001 From: jps Date: Thu, 4 Jun 2020 19:19:25 +0300 Subject: [PATCH 05/10] Use selected history data in dashboard --- frontend/src/pages/Dashboard.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/Dashboard.js b/frontend/src/pages/Dashboard.js index 5326a37d..f2b795bc 100644 --- a/frontend/src/pages/Dashboard.js +++ b/frontend/src/pages/Dashboard.js @@ -6,11 +6,15 @@ import { useStateValue } from '../contexts/state'; import Loading from '../components/Loading'; const Dashboard = () => { - const seriesId = 8; - const numberOfBuilds = 10; - const [selectedSuite, setSelectedSuite] = useState(null); - const [{ historyDataState, loadingState }, dispatch] = useStateValue(); + const [ + { amountOfBuilds, historyDataState, loadingState }, + dispatch, + ] = useStateValue(); + + const { seriesId } = useParams(); + + const numberOfBuilds = amountOfBuilds || 30; // FIXME: magic useEffect(() => { const url = `/data/series/${seriesId}/history?builds=${numberOfBuilds}`; @@ -30,7 +34,7 @@ const Dashboard = () => { } }; fetchData(); - }, [dispatch]); + }, [dispatch, numberOfBuilds, seriesId]); const { buildId } = useParams(); From 5dd6c76d40df9e52e754256f4af48e1495c27ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Rikkil=C3=A4?= Date: Mon, 8 Jun 2020 15:41:28 +0300 Subject: [PATCH 06/10] graphs folder with graph components --- .../src/components/graphs/SuiteInstability.js | 277 ++++++++++++++++++ frontend/src/pages/Dashboard.js | 262 +---------------- 2 files changed, 280 insertions(+), 259 deletions(-) create mode 100644 frontend/src/components/graphs/SuiteInstability.js diff --git a/frontend/src/components/graphs/SuiteInstability.js b/frontend/src/components/graphs/SuiteInstability.js new file mode 100644 index 00000000..628d3a00 --- /dev/null +++ b/frontend/src/components/graphs/SuiteInstability.js @@ -0,0 +1,277 @@ +import React, { useEffect, useState } from 'react'; +import { useParams } from 'react-router'; +import BreadcrumbNav from '../../components/BreadcrumbNav'; +import { VegaLite } from 'react-vega'; +import { useStateValue } from '../../contexts/state'; +/** @jsx jsx */ +import { css, jsx } from '@emotion/core'; +import Loading from '../../components/Loading'; + +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: '#141312', // TODO: from variable: --revolution-black + }, + background: '#e9e8e8', // TODO: from variable: --mithril-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: 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: 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['test_cases'].map(testCase => ( + + + + + ))} + +
{selectedSuite.name}Test history
{testCase.name}{generateStatusRow(testCase)}
+ )} +
+ ); +}; + +export default SuiteInstability; diff --git a/frontend/src/pages/Dashboard.js b/frontend/src/pages/Dashboard.js index f2b795bc..529bde53 100644 --- a/frontend/src/pages/Dashboard.js +++ b/frontend/src/pages/Dashboard.js @@ -1,266 +1,10 @@ -import React, { useEffect, useState } from 'react'; -import { useParams } from 'react-router'; -import BreadcrumbNav from '../components/BreadcrumbNav'; -import { VegaLite } from 'react-vega'; -import { useStateValue } from '../contexts/state'; -import Loading from '../components/Loading'; +import React from 'react'; +import SuiteInstability from '../components/graphs/SuiteInstability'; const Dashboard = () => { - 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: '#141312', // TODO: from variable: --revolution-black - }, - background: '#e9e8e8', // TODO: from variable: --mithril-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: 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: 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['test_cases'].map(testCase => ( - - - - - ))} - -
{selectedSuite.name}Test history
{testCase.name}{generateStatusRow(testCase)}
- )} + ); }; From f640149bba2da62a4ee4bfbc0e98c877a781ab28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Rikkil=C3=A4?= Date: Wed, 10 Jun 2020 21:51:50 +0300 Subject: [PATCH 07/10] added apexcharts, status count pie chart skeleton --- frontend/package.json | 2 + frontend/src/components/graphs/StatusCount.js | 69 +++++++++++++++++++ frontend/src/contexts/reducer.js | 55 ++++++++------- frontend/src/contexts/state.js | 9 +-- frontend/src/pages/Dashboard.js | 8 ++- 5 files changed, 113 insertions(+), 30 deletions(-) create mode 100644 frontend/src/components/graphs/StatusCount.js diff --git a/frontend/package.json b/frontend/package.json index 98ad773f..3bb83c3d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,11 +4,13 @@ "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.13.1", + "react-apexcharts": "^1.3.7", "react-dom": "^16.10.2", "react-fontawesome": "^1.7.1", "react-i18next": "^11.4.0", diff --git a/frontend/src/components/graphs/StatusCount.js b/frontend/src/components/graphs/StatusCount.js new file mode 100644 index 00000000..a938bd1f --- /dev/null +++ b/frontend/src/components/graphs/StatusCount.js @@ -0,0 +1,69 @@ +import React, { useEffect } from 'react'; +import Chart from 'react-apexcharts'; +import { useParams } from 'react-router'; +import { useStateValue } from '../../contexts/state'; + +const StatusCount = () => { + 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 = + + const series = [44, 55, 41, 17, 15]; + const options = { + // chart: { + // type: 'donut', + // }, + // responsive: [ + // { + // breakpoint: 480, + // options: {}, + // }, + // ], + plotOptions: { + pie: { + expandOnClick: true, + donut: { + labels: { + show: true, + name: { + show: true, + }, + total: { + show: true, + }, + }, + }, + }, + }, + // dataLabels: { + // enabled: true, + // }, + }; + return ( +
+ +
+ ); +}; + +export default StatusCount; 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/pages/Dashboard.js b/frontend/src/pages/Dashboard.js index 529bde53..c8457036 100644 --- a/frontend/src/pages/Dashboard.js +++ b/frontend/src/pages/Dashboard.js @@ -1,10 +1,16 @@ import React from 'react'; +import { useLocation } from 'react-router-dom'; import SuiteInstability from '../components/graphs/SuiteInstability'; +import StatusCount from '../components/graphs/StatusCount'; const Dashboard = () => { + const pathname = useLocation().pathname; + const buildUrl = pathname.includes('build'); + return ( <> - + {/* */} + {buildUrl && } ); }; From 704354584eaf44a8c839f517cbf19af1ddfcfb00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Rikkil=C3=A4?= Date: Mon, 15 Jun 2020 22:02:29 +0300 Subject: [PATCH 08/10] added new colors, added pie chart for suite and test status --- frontend/src/components/Card.js | 2 +- frontend/src/components/SelectedTeam.js | 12 +++--- frontend/src/components/graphs/StatusCount.js | 37 +++++++++++-------- .../src/components/graphs/SuiteInstability.js | 6 +-- frontend/src/components/historyTable/Body.js | 4 +- .../src/components/historyTable/Filter.js | 8 ++-- frontend/src/components/historyTable/Table.js | 8 ++-- .../src/components/parentData/ParentBuild.js | 4 +- frontend/src/index.css | 14 ++++--- frontend/src/pages/Build.js | 8 ++-- frontend/src/pages/Dashboard.js | 28 ++++++++++++-- frontend/src/pages/History.js | 10 ++--- frontend/src/utils/colorTypes.js | 13 +++++++ frontend/src/utils/graphTypes.js | 13 +++++++ frontend/src/utils/parentDataTypes.js | 4 +- 15 files changed, 113 insertions(+), 58 deletions(-) create mode 100644 frontend/src/utils/colorTypes.js create mode 100644 frontend/src/utils/graphTypes.js 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 index a938bd1f..a51dfd74 100644 --- a/frontend/src/components/graphs/StatusCount.js +++ b/frontend/src/components/graphs/StatusCount.js @@ -1,9 +1,11 @@ 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 = () => { +const StatusCount = ({ labels }) => { const { seriesId, buildId } = useParams(); const [{ statusCount }, dispatch] = useStateValue(); @@ -26,19 +28,18 @@ const StatusCount = () => { fetchData(); }, [buildId, dispatch, seriesId]); - // const data = + const data = + statusCount && labels.map(label => pluck(label, statusCount)).flat(); - const series = [44, 55, 41, 17, 15]; + const series = data; const options = { - // chart: { - // type: 'donut', - // }, - // responsive: [ - // { - // breakpoint: 480, - // options: {}, - // }, - // ], + labels: labels, + colors: [ + colorTypes['semolina red'], + colorTypes['pirlo blue'], + colorTypes['titan green'], + colorTypes['kumpula yellow'], + ], plotOptions: { pie: { expandOnClick: true, @@ -55,13 +56,17 @@ const StatusCount = () => { }, }, }, - // dataLabels: { - // enabled: true, - // }, }; return (
- + {statusCount && ( + + )}
); }; diff --git a/frontend/src/components/graphs/SuiteInstability.js b/frontend/src/components/graphs/SuiteInstability.js index 628d3a00..818780ea 100644 --- a/frontend/src/components/graphs/SuiteInstability.js +++ b/frontend/src/components/graphs/SuiteInstability.js @@ -1,11 +1,11 @@ import React, { useEffect, useState } from 'react'; import { useParams } from 'react-router'; -import BreadcrumbNav from '../../components/BreadcrumbNav'; +import BreadcrumbNav from '../BreadcrumbNav'; import { VegaLite } from 'react-vega'; import { useStateValue } from '../../contexts/state'; /** @jsx jsx */ import { css, jsx } from '@emotion/core'; -import Loading from '../../components/Loading'; +import Loading from '../Loading'; const SuiteInstability = () => { const canvasStyles = css` @@ -56,7 +56,7 @@ const SuiteInstability = () => { type: 'bar', stroke: '#141312', // TODO: from variable: --revolution-black }, - background: '#e9e8e8', // TODO: from variable: --mithril-grey + background: '#e9e8e8', // TODO: from variable: --hermanni-grey actions: false, selection: { highlight: { type: 'single', empty: 'none', on: 'mouseover' }, 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/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 c8457036..d1d98d10 100644 --- a/frontend/src/pages/Dashboard.js +++ b/frontend/src/pages/Dashboard.js @@ -1,17 +1,37 @@ import React from 'react'; 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 pathname = useLocation().pathname; const buildUrl = pathname.includes('build'); + const status = buildUrl ? 'build' : 'series'; + + const dashBoardStyles = css` + .pieContainer { + padding: 20px; + display: flex; + flex-wrap: wrap; + } + `; + return ( - <> - {/* */} - {buildUrl && } - +
+ + {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', ]; From c83c17b0652b0e61c5ec7ec2ef3e099fa8b8271f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Rikkil=C3=A4?= Date: Tue, 16 Jun 2020 11:11:42 +0300 Subject: [PATCH 09/10] codacy fixes --- frontend/src/components/graphs/StatusCount.js | 2 +- frontend/src/components/graphs/SuiteInstability.js | 11 +++++------ frontend/src/pages/Dashboard.js | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/graphs/StatusCount.js b/frontend/src/components/graphs/StatusCount.js index a51dfd74..aa7a13fc 100644 --- a/frontend/src/components/graphs/StatusCount.js +++ b/frontend/src/components/graphs/StatusCount.js @@ -33,7 +33,7 @@ const StatusCount = ({ labels }) => { const series = data; const options = { - labels: labels, + labels, colors: [ colorTypes['semolina red'], colorTypes['pirlo blue'], diff --git a/frontend/src/components/graphs/SuiteInstability.js b/frontend/src/components/graphs/SuiteInstability.js index 818780ea..1e15e672 100644 --- a/frontend/src/components/graphs/SuiteInstability.js +++ b/frontend/src/components/graphs/SuiteInstability.js @@ -1,11 +1,11 @@ import React, { useEffect, useState } from 'react'; import { useParams } from 'react-router'; -import BreadcrumbNav from '../BreadcrumbNav'; 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` @@ -54,9 +54,9 @@ const SuiteInstability = () => { height: 200, mark: { type: 'bar', - stroke: '#141312', // TODO: from variable: --revolution-black + stroke: colorTypes['gradient black'], }, - background: '#e9e8e8', // TODO: from variable: --hermanni-grey + background: colorTypes['hermanni grey'], actions: false, selection: { highlight: { type: 'single', empty: 'none', on: 'mouseover' }, @@ -164,7 +164,7 @@ const SuiteInstability = () => { suites.push({ id: suite['suite_id'], name: suite['name'], - failingTests: failingTests, + failingTests, }); } }); @@ -179,7 +179,7 @@ const SuiteInstability = () => { return { name: suite.name, numberOfFailingTests: suite.failingTests.length, - stability: stability, + stability, id: suite.id, }; }); @@ -245,7 +245,6 @@ const SuiteInstability = () => { return ( - {
{' '} - {/* */}
)} + ); }; From e3225c8e0cba470f1c9ec7f095abf188b238f7e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Rikkil=C3=A4?= Date: Tue, 16 Jun 2020 11:20:08 +0300 Subject: [PATCH 10/10] codacy fix --- frontend/src/components/graphs/SuiteInstability.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/graphs/SuiteInstability.js b/frontend/src/components/graphs/SuiteInstability.js index 1e15e672..7fc41061 100644 --- a/frontend/src/components/graphs/SuiteInstability.js +++ b/frontend/src/components/graphs/SuiteInstability.js @@ -104,7 +104,7 @@ const SuiteInstability = () => { { selection: 'select', }, - "length(data('select_store'))", + 'length(data("select_store"))', ], }, },