From 2cfae28c6f983f2c172da1fad6f68aa5ae9f5046 Mon Sep 17 00:00:00 2001 From: Patrick Riley Date: Thu, 18 Jul 2019 09:54:43 -0400 Subject: [PATCH] feat(chartGraph): convert graph data, add error/loading states * parse the Redux date and sockets inputs and generate chart data and tooltips * add error state and zeroed array * loading skeletor --- public/locales/en.json | 4 +- .../__snapshots__/graphHelpers.test.js.snap | 73 +++++ src/common/__tests__/graphHelpers.test.js | 39 +++ src/common/graphHelpers.js | 89 +++--- .../__snapshots__/rhelGraphCard.test.js.snap | 257 ++++++++++++++---- .../__tests__/rhelGraphCard.test.js | 14 +- src/components/rhelGraphCard/rhelGraphCard.js | 55 +++- src/services/rhelServices.js | 36 +-- src/styles/_usage-graph.scss | 12 + tests/__snapshots__/html.test.js.snap | 8 +- tests/__snapshots__/i18n.test.js.snap | 14 +- 11 files changed, 462 insertions(+), 139 deletions(-) diff --git a/public/locales/en.json b/public/locales/en.json index da2591543..bf10becfc 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -1,6 +1,8 @@ { "curiosity-graph": { "heading": "Daily CPU socket usage", - "dropdownDefault": "Last 30 Days" + "dropdownDefault": "Last 30 Days", + "socketsOn": "sockets on", + "fromPrevious": "from previous day" } } diff --git a/src/common/__tests__/__snapshots__/graphHelpers.test.js.snap b/src/common/__tests__/__snapshots__/graphHelpers.test.js.snap index 93d63594c..3c1b8a9ea 100644 --- a/src/common/__tests__/__snapshots__/graphHelpers.test.js.snap +++ b/src/common/__tests__/__snapshots__/graphHelpers.test.js.snap @@ -1,11 +1,84 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`GraphHelpers should convert graph data and generate tooltips when usage is populated: usage populated 1`] = ` +Array [ + Object { + "label": "56 sockets on Jun 1", + "x": "Jun 1", + "y": 56, + }, + Object { + "label": "30 sockets on Jun 2 + -26 from previous day", + "x": "Jun 2", + "y": 30, + }, + Object { + "label": "40 sockets on Jun 3 + +10 from previous day", + "x": "Jun 3", + "y": 40, + }, +] +`; + +exports[`GraphHelpers should convert graph data and return zeroed usage array if usage is empty: zeroed array 1`] = ` +Array [ + Object { + "x": "Jun 1", + "y": 0, + }, + Object { + "x": "Jun 2", + "y": 0, + }, + Object { + "x": "Jun 3", + "y": 0, + }, + Object { + "x": "Jun 4", + "y": 0, + }, + Object { + "x": "Jun 5", + "y": 0, + }, +] +`; + +exports[`GraphHelpers should convert graph data and returned zeroed array when usage throws error: throws error 1`] = ` +Array [ + Object { + "x": "Jun 1", + "y": 0, + }, + Object { + "x": "Jun 2", + "y": 0, + }, + Object { + "x": "Jun 3", + "y": 0, + }, + Object { + "x": "Jun 4", + "y": 0, + }, + Object { + "x": "Jun 5", + "y": 0, + }, +] +`; + exports[`GraphHelpers should have specific functions: helpers 1`] = ` Object { "convertGraphData": [Function], "getGraphHeight": [Function], "getTooltipDimensions": [Function], "getTooltipFontSize": [Function], + "zeroedUsageArray": [Function], } `; diff --git a/src/common/__tests__/graphHelpers.test.js b/src/common/__tests__/graphHelpers.test.js index f79cc06b5..c527b58a2 100644 --- a/src/common/__tests__/graphHelpers.test.js +++ b/src/common/__tests__/graphHelpers.test.js @@ -1,13 +1,52 @@ +import moment from 'moment'; import { graphHelpers, getGraphHeight, getTooltipDimensions, getTooltipFontSize } from '../graphHelpers'; import { helpers } from '../helpers'; describe('GraphHelpers', () => { const { breakpoints } = helpers; + const startDate = moment.utc(new Date('2019-06-01T00:00:00Z')); + const endDate = moment.utc(new Date('2019-06-05T00:00:00Z')); + const tSockectsOn = 'sockets on'; + const tFromPrevious = 'from previous day'; it('should have specific functions', () => { expect(graphHelpers).toMatchSnapshot('helpers'); }); + it('should convert graph data and return zeroed usage array if usage is empty', () => { + expect( + graphHelpers.convertGraphData({ usage: [], startDate, endDate, tSockectsOn, tFromPrevious }) + ).toMatchSnapshot('zeroed array'); + }); + + it('should convert graph data and generate tooltips when usage is populated', () => { + expect( + graphHelpers.convertGraphData({ + usage: [ + { cores: 56, date: '2019-06-01T00:00:00Z', instance_count: 28 }, + { cores: 30, date: '2019-06-02T00:00:00Z', instance_count: 28 }, + { cores: 40, date: '2019-06-03T00:00:00Z', instance_count: 28 } + ], + startDate, + endDate, + tSockectsOn, + tFromPrevious + }) + ).toMatchSnapshot('usage populated'); + }); + + it('should convert graph data and returned zeroed array when usage throws error', () => { + expect( + graphHelpers.convertGraphData({ + usage: [null], // unexpected usage, will throw exception + startDate, + endDate, + tSockectsOn, + tFromPrevious + }) + ).toMatchSnapshot('throws error'); + }); + it('should match graph heights at all breakpoints', () => { expect(getGraphHeight(breakpoints, 'xs')).toMatchSnapshot('xs graph height'); expect(getGraphHeight(breakpoints, 'sm')).toMatchSnapshot('sm graph height'); diff --git a/src/common/graphHelpers.js b/src/common/graphHelpers.js index e8780a99d..e9be89635 100644 --- a/src/common/graphHelpers.js +++ b/src/common/graphHelpers.js @@ -1,39 +1,54 @@ -const convertGraphData = () => { - // todo: convert passed params to consumable chart data +import moment from 'moment'; - return [ - { x: 'May 25', y: 30, label: '30 Sockets on May 25' }, - { x: 'May 26', y: 60, label: '60 Sockets on May 26 \r\n +30 from previous day' }, - { x: 'May 27', y: 1 }, - { x: 'May 28', y: 1 }, - { x: 'May 29', y: 2 }, - { x: 'May 30', y: 2 }, - { x: 'May 31', y: 2 }, - { x: 'Jun 1', y: 2 }, - { x: 'Jun 2', y: 2 }, - { x: 'Jun 3', y: 2 }, - { x: 'Jun 4', y: 2 }, - { x: 'Jun 5', y: 2 }, - { x: 'Jun 6', y: 3 }, - { x: 'Jun 7', y: 3 }, - { x: 'Jun 8', y: 3 }, - { x: 'Jun 9', y: 3 }, - { x: 'Jun 10', y: 4 }, - { x: 'Jun 11', y: 4 }, - { x: 'Jun 12', y: 4 }, - { x: 'Jun 13', y: 4 }, - { x: 'Jun 14', y: 4 }, - { x: 'Jun 15', y: 4 }, - { x: 'Jun 16', y: 4 }, - { x: 'Jun 17', y: 3 }, - { x: 'Jun 18', y: 3 }, - { x: 'Jun 19', y: 1 }, - { x: 'Jun 20', y: 2 }, - { x: 'Jun 21', y: 5 }, - { x: 'Jun 22', y: 3 }, - { x: 'Jun 23', y: 1 }, - { x: 'Jun 24', y: 1 } - ]; +const chartDateFormat = 'MMM D'; + +const zeroedUsageArray = (startDate, endDate) => { + const zeroedArray = []; + const diff = endDate.diff(startDate, 'days'); + for (let i = 0; i < diff + 1; i++) { + const clone = moment(startDate); + zeroedArray.push({ x: clone.add(i, 'days').format(chartDateFormat), y: 0 }); + } + return zeroedArray; +}; + +const getLabel = (i, cores, previousCores, formattedDate, tSockectsOn, tFromPrevious) => { + if (i === 0) { + return `${cores} ${tSockectsOn} ${formattedDate}`; + } + const prev = cores - previousCores; + return `${cores} ${tSockectsOn} ${formattedDate} \r\n ${prev > -1 ? `+${prev}` : prev} ${tFromPrevious}`; +}; + +const convertGraphData = ({ usage, startDate, endDate, tSockectsOn, tFromPrevious }) => { + /** + * convert json usage report from this format: + * {cores: 56, date: "2019-06-01T00:00:00Z", instance_count: 28} + * to this format: + * { x: 'Jun 1', y: 56, label: '56 Sockets on Jun 1 \r\n +5 from previous day' }, + */ + if (usage === undefined || usage.length === 0) { + return zeroedUsageArray(startDate, endDate); + } + try { + const chartData = []; + for (let i = 0; i < usage.length; i++) { + const formattedDate = moment.utc(usage[i].date).format(chartDateFormat); + const label = getLabel( + i, + usage[i].cores, + i > 0 ? usage[i - 1].cores : null, + formattedDate, + tSockectsOn, + tFromPrevious + ); + chartData.push({ x: formattedDate, y: usage[i].cores, label }); + } + return chartData; + } catch (e) { + // todo: show error toast ? + return zeroedUsageArray(startDate, endDate); + } }; const getGraphHeight = (breakpoints, currentBreakpoint) => @@ -68,11 +83,13 @@ const getTooltipFontSize = (breakpoints, currentBreakpoint) => { return 14; }; -const graphHelpers = { convertGraphData, getGraphHeight, getTooltipDimensions, getTooltipFontSize }; +const graphHelpers = { convertGraphData, getGraphHeight, getTooltipDimensions, getTooltipFontSize, zeroedUsageArray }; export { graphHelpers as default, graphHelpers, + zeroedUsageArray, + chartDateFormat, convertGraphData, getGraphHeight, getTooltipDimensions, diff --git a/src/components/rhelGraphCard/__tests__/__snapshots__/rhelGraphCard.test.js.snap b/src/components/rhelGraphCard/__tests__/__snapshots__/rhelGraphCard.test.js.snap index a1be66630..3070e5e7d 100644 --- a/src/components/rhelGraphCard/__tests__/__snapshots__/rhelGraphCard.test.js.snap +++ b/src/components/rhelGraphCard/__tests__/__snapshots__/rhelGraphCard.test.js.snap @@ -64,6 +64,7 @@ exports[`RhelGraphCard Component should render a non-connected component: non-co `; -exports[`RhelGraphCard Component should render multiple states: error 1`] = `""`; +exports[`RhelGraphCard Component should render multiple states: error shows zeroed bar values 1`] = ` +Object { + "chartBarData": Array [ + Object { + "x": "Jun 1", + "y": 0, + }, + Object { + "x": "Jun 2", + "y": 0, + }, + Object { + "x": "Jun 3", + "y": 0, + }, + Object { + "x": "Jun 4", + "y": 0, + }, + Object { + "x": "Jun 5", + "y": 0, + }, + Object { + "x": "Jun 6", + "y": 0, + }, + Object { + "x": "Jun 7", + "y": 0, + }, + Object { + "x": "Jun 8", + "y": 0, + }, + Object { + "x": "Jun 9", + "y": 0, + }, + Object { + "x": "Jun 10", + "y": 0, + }, + Object { + "x": "Jun 11", + "y": 0, + }, + Object { + "x": "Jun 12", + "y": 0, + }, + Object { + "x": "Jun 13", + "y": 0, + }, + Object { + "x": "Jun 14", + "y": 0, + }, + Object { + "x": "Jun 15", + "y": 0, + }, + Object { + "x": "Jun 16", + "y": 0, + }, + Object { + "x": "Jun 17", + "y": 0, + }, + Object { + "x": "Jun 18", + "y": 0, + }, + Object { + "x": "Jun 19", + "y": 0, + }, + Object { + "x": "Jun 20", + "y": 0, + }, + Object { + "x": "Jun 21", + "y": 0, + }, + Object { + "x": "Jun 22", + "y": 0, + }, + Object { + "x": "Jun 23", + "y": 0, + }, + Object { + "x": "Jun 24", + "y": 0, + }, + Object { + "x": "Jun 25", + "y": 0, + }, + Object { + "x": "Jun 26", + "y": 0, + }, + Object { + "x": "Jun 27", + "y": 0, + }, + Object { + "x": "Jun 28", + "y": 0, + }, + Object { + "x": "Jun 29", + "y": 0, + }, + Object { + "x": "Jun 30", + "y": 0, + }, + ], +} +`; exports[`RhelGraphCard Component should render multiple states: fulfilled 1`] = `
- - Loading... - + + + +
diff --git a/src/components/rhelGraphCard/__tests__/rhelGraphCard.test.js b/src/components/rhelGraphCard/__tests__/rhelGraphCard.test.js index 1cf8f1665..4d8fad47e 100644 --- a/src/components/rhelGraphCard/__tests__/rhelGraphCard.test.js +++ b/src/components/rhelGraphCard/__tests__/rhelGraphCard.test.js @@ -6,9 +6,11 @@ import { helpers } from '../../../common/helpers'; describe('RhelGraphCard Component', () => { const { breakpoints } = helpers; + const startDate = new Date('2019-06-01T00:00:00Z'); + const endDate = new Date('2019-06-30T00:00:00Z'); it('should render a non-connected component', () => { - const props = {}; + const props = { startDate, endDate }; const component = mount(); @@ -16,7 +18,7 @@ describe('RhelGraphCard Component', () => { }); it('should render multiple states', () => { - const props = {}; + const props = { startDate, endDate }; const component = shallow(); @@ -24,7 +26,9 @@ describe('RhelGraphCard Component', () => { error: true }); - expect(component).toMatchSnapshot('error'); + expect({ + chartBarData: component.find(ChartBar).prop('data') + }).toMatchSnapshot('error shows zeroed bar values'); component.setProps({ error: false, @@ -48,7 +52,9 @@ describe('RhelGraphCard Component', () => { pending: false, fulfilled: true, breakpoints, - currentBreakpoint: 'xs' + currentBreakpoint: 'xs', + startDate, + endDate }; const component = shallow(); diff --git a/src/components/rhelGraphCard/rhelGraphCard.js b/src/components/rhelGraphCard/rhelGraphCard.js index 7230edd85..9025104a2 100644 --- a/src/components/rhelGraphCard/rhelGraphCard.js +++ b/src/components/rhelGraphCard/rhelGraphCard.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { withBreakpoints } from 'react-breakpoints'; +import moment from 'moment'; import { Card, CardHead, @@ -10,6 +11,7 @@ import { DropdownToggle, DropdownPosition } from '@patternfly/react-core'; +import { Skeleton, SkeletonSize } from '@redhat-cloud-services/frontend-components'; import { Chart, ChartBar, ChartBaseTheme, ChartLabel, ChartStack, ChartTooltip } from '@patternfly/react-charts'; import { connectTranslate, reduxActions } from '../../redux'; import { helpers } from '../../common/helpers'; @@ -19,13 +21,20 @@ import { rhelApiTypes } from '../../types/rhelApiTypes'; class RhelGraphCard extends React.Component { state = { isOpen: false }; + constructor(props) { + super(props); + const { startDate, endDate } = props; + this.endDate = endDate ? moment.utc(endDate) : moment(); + this.startDate = startDate ? moment.utc(startDate) : moment().subtract(1, 'months'); + } + componentDidMount() { const { getGraphReports } = this.props; getGraphReports({ [rhelApiTypes.RHSM_API_QUERY_GRANULARITY]: 'daily', - [rhelApiTypes.RHSM_API_QUERY_START_DATE]: '2019-01-01T00:00:00Z', - [rhelApiTypes.RHSM_API_QUERY_END_DATE]: '2019-01-31T00:00:00Z' + [rhelApiTypes.RHSM_API_QUERY_START_DATE]: this.startDate.toISOString(), + [rhelApiTypes.RHSM_API_QUERY_END_DATE]: this.endDate.toISOString() }); } @@ -44,13 +53,21 @@ class RhelGraphCard extends React.Component { render() { const { error, fulfilled, graphData, pending, t, breakpoints, currentBreakpoint } = this.props; const { isOpen } = this.state; + let chartData; if (error) { - return null; + // todo: show error toast? + chartData = graphHelpers.zeroedUsageArray(this.startDate, this.endDate); + } + if (fulfilled) { + chartData = graphHelpers.convertGraphData({ + ...graphData, + startDate: this.startDate, + endDate: this.endDate, + tSockectsOn: t('curiosity-graph.socketsOn', 'sockets on'), + tFromPrevious: t('curiosity-graph.fromPrevious', 'from previous day') + }); } - - // todo: construct chartData using graphData in the reducer... - const chartData = graphHelpers.convertGraphData({ ...graphData }); const dropdownToggle = ( @@ -61,7 +78,11 @@ class RhelGraphCard extends React.Component { // heights are breakpoint specific since they are scaled via svg const graphHeight = graphHelpers.getGraphHeight(breakpoints, currentBreakpoint); const tooltipDimensions = graphHelpers.getTooltipDimensions(breakpoints, currentBreakpoint); - + const chartDomain = { x: [0, 31] }; + if (error) { + // specify a y range if we are showing the zeroed view + chartDomain.y = [0, 100]; + } const tooltipTheme = { ...ChartBaseTheme, tooltip: { @@ -84,7 +105,6 @@ class RhelGraphCard extends React.Component { /> ); - // todo: correct pending/loading display return ( @@ -101,15 +121,18 @@ class RhelGraphCard extends React.Component { {pending && ( -
- Loading... +
+ + + +
)} - {fulfilled && ( + {(fulfilled || error) && (
- + @@ -139,7 +162,9 @@ RhelGraphCard.propTypes = { xl: PropTypes.number, xl2: PropTypes.number }), - currentBreakpoint: PropTypes.string + currentBreakpoint: PropTypes.string, + startDate: PropTypes.instanceOf(Date), + endDate: PropTypes.instanceOf(Date) }; RhelGraphCard.defaultProps = { @@ -152,7 +177,9 @@ RhelGraphCard.defaultProps = { pending: false, t: helpers.noopTranslate, breakpoints: {}, - currentBreakpoint: '' + currentBreakpoint: '', + startDate: null, + endDate: null }; const mapStateToProps = state => ({ diff --git a/src/services/rhelServices.js b/src/services/rhelServices.js index a2fea52a5..1442e4683 100644 --- a/src/services/rhelServices.js +++ b/src/services/rhelServices.js @@ -140,34 +140,34 @@ import serviceConfig from './config'; * "instance_count": 20 * }, * { - * "cores": 0, + * "cores": 6, * "date": "2019-06-21T00:00:00Z", - * "instance_count": 0 + * "instance_count": 6 * }, * { - * "cores": 0, + * "cores": 2, * "date": "2019-06-22T00:00:00Z", - * "instance_count": 0 + * "instance_count": 2 * }, * { - * "cores": 0, + * "cores": 4, * "date": "2019-06-23T00:00:00Z", - * "instance_count": 0 + * "instance_count": 4 * }, * { - * "cores": 0, + * "cores": 6, * "date": "2019-06-24T00:00:00Z", - * "instance_count": 0 + * "instance_count": 6 * }, * { - * "cores": 0, + * "cores": 10, * "date": "2019-06-25T00:00:00Z", - * "instance_count": 0 + * "instance_count": 10 * }, * { - * "cores": 0, + * "cores": 2, * "date": "2019-06-26T00:00:00Z", - * "instance_count": 0 + * "instance_count": 4 * }, * { * "cores": 0, @@ -175,19 +175,19 @@ import serviceConfig from './config'; * "instance_count": 0 * }, * { - * "cores": 0, + * "cores": 6, * "date": "2019-06-28T00:00:00Z", - * "instance_count": 0 + * "instance_count": 2 * }, * { - * "cores": 0, + * "cores": 4, * "date": "2019-06-29T00:00:00Z", - * "instance_count": 0 + * "instance_count": 2 * }, * { - * "cores": 0, + * "cores": 2, * "date": "2019-06-30T00:00:00Z", - * "instance_count": 0 + * "instance_count": 2 * } * ], * "links": { diff --git a/src/styles/_usage-graph.scss b/src/styles/_usage-graph.scss index 4743a8280..3ee0c1d89 100644 --- a/src/styles/_usage-graph.scss +++ b/src/styles/_usage-graph.scss @@ -3,6 +3,18 @@ var(--pf-global--BorderColor--100); } +.curiosity-usage-graph .skeleton-container { + padding-top: var(--pf-c-card--child--PaddingBottom); +} + +.curiosity-usage-graph .skeleton-container .ins-c-skeleton { + margin-bottom: 20px; + + &:last-child { + margin-bottom: 0px; + } +} + .curiosity-usage-graph .stack-chart-container svg { > g:nth-child(2) { > g { diff --git a/tests/__snapshots__/html.test.js.snap b/tests/__snapshots__/html.test.js.snap index 8c59b7b51..c32695222 100644 --- a/tests/__snapshots__/html.test.js.snap +++ b/tests/__snapshots__/html.test.js.snap @@ -9,15 +9,15 @@ exports[`Index.HTML should have a specific html output: html output 1`] = ` Subscription Reporting - - + + - - " diff --git a/tests/__snapshots__/i18n.test.js.snap b/tests/__snapshots__/i18n.test.js.snap index 047d7a84f..e10a915fc 100644 --- a/tests/__snapshots__/i18n.test.js.snap +++ b/tests/__snapshots__/i18n.test.js.snap @@ -5,15 +5,25 @@ exports[`i18n locale should generate a predictable pot output snapshot: pot outp msgstr \\"\\" \\"Content-Type: text/plain; charset=UTF-8\\\\n\\" -#: src/components/rhelGraphCard/rhelGraphCard.js:91 +#: src/components/rhelGraphCard/rhelGraphCard.js:111 msgctxt \\"Daily CPU socket usage\\" msgid \\"curiosity-graph.heading\\" msgstr \\"\\" -#: src/components/rhelGraphCard/rhelGraphCard.js:57 +#: src/components/rhelGraphCard/rhelGraphCard.js:68 +msgctxt \\"from previous day\\" +msgid \\"curiosity-graph.fromPrevious\\" +msgstr \\"\\" + +#: src/components/rhelGraphCard/rhelGraphCard.js:74 msgctxt \\"Last 30 Days\\" msgid \\"curiosity-graph.dropdownDefault\\" msgstr \\"\\" + +#: src/components/rhelGraphCard/rhelGraphCard.js:67 +msgctxt \\"sockets on\\" +msgid \\"curiosity-graph.socketsOn\\" +msgstr \\"\\" " `;