From 46055ceab61891e59e596b3c3c096a65547d3aa8 Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Sun, 9 Aug 2020 22:42:08 -0400 Subject: [PATCH 01/28] fix(table,tableSkeleton): issues/10 align content prop to pf (#372) * table, pf fixme annotations, align to pf title/content prop * tableSkeleton, switch to generic node, related to pf title align --- .../__snapshots__/table.test.js.snap | 50 +++-- .../__snapshots__/tableSkeleton.test.js.snap | 200 +++++++----------- src/components/table/table.js | 65 +++++- src/components/table/tableSkeleton.js | 2 +- 4 files changed, 172 insertions(+), 145 deletions(-) diff --git a/src/components/table/__tests__/__snapshots__/table.test.js.snap b/src/components/table/__tests__/__snapshots__/table.test.js.snap index f98043d7d..726a07fd3 100644 --- a/src/components/table/__tests__/__snapshots__/table.test.js.snap +++ b/src/components/table/__tests__/__snapshots__/table.test.js.snap @@ -14,7 +14,9 @@ exports[`Table Component should allow expandable content: expandable content 1`] canSelectAll={true} cells={ Array [ - "lorem ipsum", + Object { + "title": "lorem ipsum", + }, ] } className="curiosity-table " @@ -81,7 +83,9 @@ exports[`Table Component should allow expandable content: expanded row 1`] = ` canSelectAll={true} cells={ Array [ - "lorem ipsum", + Object { + "title": "lorem ipsum", + }, ] } className="curiosity-table " @@ -148,7 +152,9 @@ exports[`Table Component should allow variations in table layout: ariaLabel and canSelectAll={true} cells={ Array [ - "lorem ipsum", + Object { + "title": "lorem ipsum", + }, ] } className="curiosity-table-no-border " @@ -158,7 +164,6 @@ exports[`Table Component should allow variations in table layout: ariaLabel and expandId="expandable-toggle" gridBreakPoint="grid-md" isStickyHeader={false} - onCollapse={[Function]} ouiaSafe={true} role="grid" rowLabeledBy="simple-node" @@ -203,7 +208,9 @@ exports[`Table Component should allow variations in table layout: borders and ta canSelectAll={true} cells={ Array [ - "lorem ipsum", + Object { + "title": "lorem ipsum", + }, ] } className="curiosity-table-no-border " @@ -213,7 +220,6 @@ exports[`Table Component should allow variations in table layout: borders and ta expandId="expandable-toggle" gridBreakPoint="grid-md" isStickyHeader={false} - onCollapse={[Function]} ouiaSafe={true} role="grid" rowLabeledBy="simple-node" @@ -258,7 +264,9 @@ exports[`Table Component should allow variations in table layout: className and canSelectAll={true} cells={ Array [ - "lorem ipsum", + Object { + "title": "lorem ipsum", + }, ] } className="curiosity-table-no-border lorem-ipsum-class" @@ -268,7 +276,6 @@ exports[`Table Component should allow variations in table layout: className and expandId="expandable-toggle" gridBreakPoint="grid-md" isStickyHeader={false} - onCollapse={[Function]} ouiaSafe={true} role="grid" rowLabeledBy="simple-node" @@ -313,7 +320,9 @@ exports[`Table Component should allow variations in table layout: generated rows canSelectAll={true} cells={ Array [ - "lorem ipsum", + Object { + "title": "lorem ipsum", + }, ] } className="curiosity-table " @@ -323,7 +332,6 @@ exports[`Table Component should allow variations in table layout: generated rows expandId="expandable-toggle" gridBreakPoint="grid-md" isStickyHeader={false} - onCollapse={[Function]} ouiaSafe={true} role="grid" rowLabeledBy="simple-node" @@ -369,7 +377,9 @@ exports[`Table Component should pass child components, nodes when there are no r canSelectAll={true} cells={ Array [ - "lorem ipsum", + Object { + "title": "lorem ipsum", + }, ] } className="curiosity-table " @@ -379,7 +389,6 @@ exports[`Table Component should pass child components, nodes when there are no r expandId="expandable-toggle" gridBreakPoint="grid-md" isStickyHeader={false} - onCollapse={[Function]} ouiaSafe={true} role="grid" rowLabeledBy="simple-node" @@ -409,10 +418,18 @@ exports[`Table Component should render a non-connected component: non-connected canSelectAll={true} cells={ Array [ - "lorem", - "ipsum", - "dolor", - "sit", + Object { + "title": "lorem", + }, + Object { + "title": "ipsum", + }, + Object { + "title": "dolor", + }, + Object { + "title": "sit", + }, ] } className="curiosity-table " @@ -422,7 +439,6 @@ exports[`Table Component should render a non-connected component: non-connected expandId="expandable-toggle" gridBreakPoint="grid-md" isStickyHeader={false} - onCollapse={[Function]} ouiaSafe={true} role="grid" rowLabeledBy="simple-node" diff --git a/src/components/table/__tests__/__snapshots__/tableSkeleton.test.js.snap b/src/components/table/__tests__/__snapshots__/tableSkeleton.test.js.snap index 47a7ac248..c769e1b2c 100644 --- a/src/components/table/__tests__/__snapshots__/tableSkeleton.test.js.snap +++ b/src/components/table/__tests__/__snapshots__/tableSkeleton.test.js.snap @@ -34,36 +34,26 @@ exports[`TableSkeleton Component should allow variations in table layout: border Array [ Object { "cells": Array [ - Object { - "cell": , - }, - Object { - "cell": , - }, - Object { - "cell": , - }, - Object { - "cell": , - }, - Object { - "cell": , - }, + , + , + , + , + , ], }, ] @@ -108,36 +98,26 @@ exports[`TableSkeleton Component should allow variations in table layout: classN Array [ Object { "cells": Array [ - Object { - "cell": , - }, - Object { - "cell": , - }, - Object { - "cell": , - }, - Object { - "cell": , - }, - Object { - "cell": , - }, + , + , + , + , + , ], }, ] @@ -182,36 +162,26 @@ exports[`TableSkeleton Component should allow variations in table layout: column Array [ Object { "cells": Array [ - Object { - "cell": , - }, - Object { - "cell": , - }, - Object { - "cell": , - }, - Object { - "cell": , - }, - Object { - "cell": , - }, + , + , + , + , + , ], }, ] @@ -240,52 +210,42 @@ exports[`TableSkeleton Component should render a non-connected component: non-co Array [ Object { "cells": Array [ - Object { - "cell": , - }, + , ], }, Object { "cells": Array [ - Object { - "cell": , - }, + , ], }, Object { "cells": Array [ - Object { - "cell": , - }, + , ], }, Object { "cells": Array [ - Object { - "cell": , - }, + , ], }, Object { "cells": Array [ - Object { - "cell": , - }, + , ], }, ] diff --git a/src/components/table/table.js b/src/components/table/table.js index 5f5a48d8b..24991be3b 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -8,6 +8,37 @@ import { TableEmpty } from './tableEmpty'; import { helpers } from '../../common'; import { translate } from '../i18n/i18n'; +/** + * FixMe: PF tables uses sequentially ordered lists/arrays to denote expandable sections + * Ideal solution is to nest expandable sections within their respective parents. There + * are scenarios where manipulating a sequentially ordered list/array can, and will, + * lead to unintended results. And forces an equal, if not greater, amount of effort on + * state tracking in the consuming GUI/app. Compounding the issue, the index is used/returned + * during events for a particular row being "expanded". This ends up making after-the-fact + * application of the expandable list/array items problematic since said index is + * constantly updating/shifting as rows are dynamically added. Add in a "not every row + * needs to be expandable" scenario, you can start to see the issues. All of that + * effort means that in some cases it may actually be quicker to simply use PF styles and + * a HTML table directly, bypassing PF-React entirely. + * + * Applying a React node as content in the sequentially ordered list/array appears to + * automatically "mount" said node. How this handles in child components where subsequent + * AJAX/XHR calls are called means you are potentially hitting an API with dozens of calls, + * row dependent, even when a user may not expand the nested row/child. This could easily + * be resolved by nesting and applying the expandable contents after-the-fact. + */ +/** + * FixMe: PF tables misalignment of the column headers list/array and row cells breaks the component + * The related PF error for this is cryptic. An immediate solution would be to simply fix the error + * messaging, or do a length check before consuming the lists/arrays. This happens when a consumer + * places more or less column header cells than row cells. + */ +/** + * FixMe: PF tables break when an empty list/array is used for the column headers + * This appears related to the mismatched column header and row cells. The solution is to simply not + * render the header row instead of throwing an error. This is similar to how the empty rows + * list/array is handled. + */ /** * A table. * @@ -16,6 +47,7 @@ import { translate } from '../i18n/i18n'; */ class Table extends React.Component { state = { + isCollapsible: false, updatedColumnHeaders: null, updatedRows: null }; @@ -67,9 +99,15 @@ class Table extends React.Component { const { columnHeaders, rows } = this.props; const updatedColumnHeaders = []; const updatedRows = []; + let isCollapsible = false; columnHeaders.forEach(columnHeader => { - updatedColumnHeaders.push(columnHeader); + if (columnHeader?.title) { + const { title, ...settings } = columnHeader; + updatedColumnHeaders.push({ title, ...settings }); + } else { + updatedColumnHeaders.push({ title: columnHeader }); + } }); rows.forEach(({ expandedContent, cells, isExpanded }) => { @@ -79,6 +117,7 @@ class Table extends React.Component { updatedRows.push(rowObj); if (expandedContent) { + isCollapsible = true; rowObj.isOpen = isExpanded || false; updatedRows.push({ @@ -90,9 +129,9 @@ class Table extends React.Component { } cells.forEach(cell => { - if (cell?.cell) { - const { cell: contentCell, ...settings } = cell; - rowObj.cells.push({ title: contentCell, ...settings }); + if (cell?.title) { + const { title, ...settings } = cell; + rowObj.cells.push({ title, ...settings }); } else { rowObj.cells.push({ title: cell }); } @@ -100,19 +139,27 @@ class Table extends React.Component { }); this.setState({ + isCollapsible, updatedColumnHeaders, updatedRows }); } + /** + * FixMe: PF Tables automatically applies an expandable className when using the "onCollapse" prop? + * Not every row may need to be expandable. In certain scenarios a table may have no expandable + * sections, however it appears the "onCollapse" styling, and additionally added "table cells" are still + * applied. + */ /** * Apply props to table. * * @returns {Node} */ renderTable() { - const { updatedColumnHeaders, updatedRows } = this.state; + const { isCollapsible, updatedColumnHeaders, updatedRows } = this.state; const { ariaLabel, borders, children, className, isHeader, summary, t, variant } = this.props; + const pfTableProps = {}; let emptyTable = null; if (!updatedRows?.length) { @@ -125,6 +172,10 @@ class Table extends React.Component { ); } + if (isCollapsible) { + pfTableProps.onCollapse = (event, index, isOpen, data) => this.onCollapse({ event, index, isOpen, data }); + } + return ( this.onCollapse({ event, index, isOpen, data })} rows={(updatedRows?.length && updatedRows) || []} cells={updatedColumnHeaders || []} + {...pfTableProps} > {isHeader && } @@ -186,7 +237,7 @@ Table.propTypes = { PropTypes.oneOfType([ PropTypes.node, PropTypes.shape({ - cell: PropTypes.node + title: PropTypes.node }) ]) ), diff --git a/src/components/table/tableSkeleton.js b/src/components/table/tableSkeleton.js index d03b66079..703449691 100644 --- a/src/components/table/tableSkeleton.js +++ b/src/components/table/tableSkeleton.js @@ -22,7 +22,7 @@ const TableSkeleton = ({ className, borders, colCount, isHeader, rowCount, t, va const updatedColumnHeaders = [...new Array(colCount)].map(() => ); const updatedRows = [...new Array(rowCount)].map(() => ({ - cells: [...new Array(colCount)].map(() => ({ cell: })) + cells: [...new Array(colCount)].map(() => ) })); return ( From 5851658a3b5fcc517499cc1c175d3d3e6644e706 Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Sun, 9 Aug 2020 23:03:59 -0400 Subject: [PATCH 02/28] fix(inventoryListSelectors): issues/10 itemCount, deep equals (#372) * inventoryListSelectors, itemCount, deep equals adjustment * rhsmApiTypes, added inventory_id * reduxHelpers add setNormalizedResponse * rhsmServices mock update --- .../__snapshots__/reduxHelpers.test.js.snap | 52 +++++++++++++++ .../common/__tests__/reduxHelpers.test.js | 33 ++++++++++ src/redux/common/reduxHelpers.js | 63 ++++++++++++++++++- .../inventoryListSelectors.test.js.snap | 17 +++++ .../__tests__/inventoryListSelectors.test.js | 8 +-- src/redux/selectors/inventoryListSelectors.js | 62 +++++++++--------- src/services/rhsmServices.js | 5 +- .../__snapshots__/index.test.js.snap | 4 ++ src/types/rhsmApiTypes.js | 11 ++-- tests/__snapshots__/code.test.js.snap | 4 +- 10 files changed, 216 insertions(+), 43 deletions(-) diff --git a/src/redux/common/__tests__/__snapshots__/reduxHelpers.test.js.snap b/src/redux/common/__tests__/__snapshots__/reduxHelpers.test.js.snap index e68451e1e..26fac6c7a 100644 --- a/src/redux/common/__tests__/__snapshots__/reduxHelpers.test.js.snap +++ b/src/redux/common/__tests__/__snapshots__/reduxHelpers.test.js.snap @@ -101,6 +101,57 @@ Object { } `; +exports[`ReduxHelpers should generate a normalized API response using a schema: array response 1`] = ` +Array [ + Array [ + Object { + "ipsum": 1, + "sit": undefined, + }, + Object { + "ipsum": undefined, + "sit": "hello world", + }, + ], +] +`; + +exports[`ReduxHelpers should generate a normalized API response using a schema: object response 1`] = ` +Array [ + Array [ + Object { + "ipsum": 1, + "sit": undefined, + }, + ], +] +`; + +exports[`ReduxHelpers should generate a normalized API response using a schema: parameters 1`] = ` +Array [ + Array [ + Object { + "ipsum": 1, + "sit": undefined, + }, + Object { + "ipsum": undefined, + "sit": "hello world", + }, + ], + Array [ + Object { + "ipsum": "custom value = 1", + "sit": "custom value = undefined", + }, + Object { + "ipsum": "custom value = undefined", + "sit": "custom value = hello world", + }, + ], +] +`; + exports[`ReduxHelpers should generate an expected API response with an existing schema: array 1`] = ` Array [ Object { @@ -210,6 +261,7 @@ Object { "getMessageFromResults": [Function], "getSingleResponseFromResultArray": [Function], "getStatusFromResults": [Function], + "setNormalizedResponse": [Function], "setResponseSchemas": [Function], "setStateProp": [Function], "singlePromiseDataResponseFromArray": [Function], diff --git a/src/redux/common/__tests__/reduxHelpers.test.js b/src/redux/common/__tests__/reduxHelpers.test.js index 8eb96bb93..8b194705f 100644 --- a/src/redux/common/__tests__/reduxHelpers.test.js +++ b/src/redux/common/__tests__/reduxHelpers.test.js @@ -22,6 +22,39 @@ describe('ReduxHelpers', () => { ).toMatchSnapshot('array'); }); + it('should generate a normalized API response using a schema', () => { + const setNormalizedResponseParams = { + schema: { + LOREM: 'ipsum', + DOLOR: 'sit' + }, + data: [ + { + ipsum: 1 + }, + { sit: 'hello world' } + ] + }; + + expect( + reduxHelpers.setNormalizedResponse(setNormalizedResponseParams, { + ...setNormalizedResponseParams, + customResponseValue: ({ value }) => `custom value = ${value}` + }) + ).toMatchSnapshot('parameters'); + + expect(reduxHelpers.setNormalizedResponse(setNormalizedResponseParams)).toMatchSnapshot('array response'); + + expect( + reduxHelpers.setNormalizedResponse({ + ...setNormalizedResponseParams, + data: { + ipsum: 1 + } + }) + ).toMatchSnapshot('object response'); + }); + it('should get a message from a service call response', () => { expect( reduxHelpers.getMessageFromResults({ diff --git a/src/redux/common/reduxHelpers.js b/src/redux/common/reduxHelpers.js index d33c964c2..3107dc0ab 100644 --- a/src/redux/common/reduxHelpers.js +++ b/src/redux/common/reduxHelpers.js @@ -1,5 +1,6 @@ import _get from 'lodash/get'; import _isPlainObject from 'lodash/isPlainObject'; +import _camelCase from 'lodash/camelCase'; import { helpers } from '../../common'; /** @@ -34,6 +35,7 @@ const REJECTED_ACTION = (base = '') => `${base}_REJECTED`; */ const HTTP_STATUS_RANGE = status => `${status}_STATUS_RANGE`; +// ToDo: research applying a maintained schema map/normalizer such as, normalizr /** * Apply a set of schemas using either an array of objects in the * form of [{ madeUpKey: 'some_api_key' }], or an array of arrays @@ -55,6 +57,62 @@ const setResponseSchemas = (schemas = [], initialValue) => return generated; }); +/** + * Normalize an API response. + * + * @param {*} responses + * @param {object} responses.response + * @param {object} responses.response.schema + * @param {Array|object} responses.response.data + * @param {Function} responses.response.customResponseValue + * @param {string} responses.response.keyPrefix + * @returns {Array} + */ +const setNormalizedResponse = (...responses) => { + const parsedResponses = []; + + responses.forEach(({ schema = {}, data, customResponseValue, keyPrefix: prefix }) => { + const isArray = Array.isArray(data); + const updatedData = (isArray && data) || (data && [data]) || []; + const [generatedSchema = {}] = setResponseSchemas([schema]); + const parsedResponse = []; + + updatedData.forEach((value, index) => { + const generateReflectedData = ({ dataObj, keyPrefix = '', customValue = null, update = helpers.noop }) => { + const updatedDataObj = {}; + + Object.entries(dataObj).forEach(([dataObjKey, dataObjValue]) => { + const casedDataObjKey = _camelCase(`${keyPrefix} ${dataObjKey}`).trim(); + let val = dataObjValue; + + if (typeof val === 'number') { + val = (Number.isInteger(val) && Number.parseInt(val, 10)) || Number.parseFloat(val) || val; + } + + if (typeof customValue === 'function') { + updatedDataObj[casedDataObjKey] = customValue({ data: dataObj, key: dataObjKey, value: val, index }); + } else { + updatedDataObj[casedDataObjKey] = val; + } + }); + + update(updatedDataObj); + }; + + generateReflectedData({ + keyPrefix: prefix, + dataObj: { ...generatedSchema, ...value }, + customValue: customResponseValue, + update: generatedData => parsedResponse.push(generatedData) + }); + }); + + parsedResponses.push(parsedResponse); + }); + + return parsedResponses; +}; + /** * Create a single response from an array of service call responses. * Aids in handling a Promise.all response. @@ -163,11 +221,11 @@ const setStateProp = (prop, data, options) => { const { state = {}, initialState = {}, reset = true } = options; let obj = { ...state }; - if (process.env.REACT_APP_ENV === 'development' && prop && !state[prop]) { + if (helpers.DEV_MODE && prop && !state[prop]) { console.error(`Error: Property ${prop} does not exist within the passed state.`, state); } - if (process.env.REACT_APP_ENV === 'development' && reset && prop && !initialState[prop]) { + if (helpers.DEV_MODE && reset && prop && !initialState[prop]) { console.warn(`Warning: Property ${prop} does not exist within the passed initialState.`, initialState); } @@ -335,6 +393,7 @@ const reduxHelpers = { REJECTED_ACTION, HTTP_STATUS_RANGE, setResponseSchemas, + setNormalizedResponse, generatedPromiseActionReducer, getDataFromResults, getDateFromResults, diff --git a/src/redux/selectors/__tests__/__snapshots__/inventoryListSelectors.test.js.snap b/src/redux/selectors/__tests__/__snapshots__/inventoryListSelectors.test.js.snap index 61eca7fe1..7b51170ee 100644 --- a/src/redux/selectors/__tests__/__snapshots__/inventoryListSelectors.test.js.snap +++ b/src/redux/selectors/__tests__/__snapshots__/inventoryListSelectors.test.js.snap @@ -4,6 +4,7 @@ exports[`InventoryListSelectors should handle pending state on a product ID: pen Object { "error": false, "fulfilled": false, + "itemCount": 0, "listData": Array [], "pending": true, "status": undefined, @@ -14,12 +15,14 @@ exports[`InventoryListSelectors should map a fulfilled product ID response to an Object { "error": false, "fulfilled": true, + "itemCount": 0, "listData": Array [ Object { "cores": 2, "displayName": "db.lorem.com", "hardwareType": "physical", "insightsId": "d6214a0b-b344-4778-831c-d53dcacb2da3", + "inventoryId": null, "lastSeen": "17 days ago", "numberOfGuests": null, "sockets": 1, @@ -30,6 +33,7 @@ Object { "displayName": "db.ipsum.com", "hardwareType": "physical", "insightsId": "9358e312-1c9f-42f4-8910-dcef6e970852", + "inventoryId": null, "lastSeen": "in a month", "numberOfGuests": null, "sockets": 1, @@ -45,6 +49,7 @@ exports[`InventoryListSelectors should pass minimal data on a product ID without Object { "error": false, "fulfilled": false, + "itemCount": 0, "listData": Array [], "pending": false, "status": undefined, @@ -55,6 +60,7 @@ exports[`InventoryListSelectors should pass minimal data on missing a reducer re Object { "error": false, "fulfilled": false, + "itemCount": 0, "listData": Array [], "pending": false, "status": undefined, @@ -65,12 +71,14 @@ exports[`InventoryListSelectors should populate data from the in memory cache: c Object { "error": false, "fulfilled": true, + "itemCount": 0, "listData": Array [ Object { "cores": 2, "displayName": "db.ipsum.com", "hardwareType": "physical", "insightsId": "9358e312-1c9f-42f4-8910-dcef6e970852", + "inventoryId": null, "lastSeen": "in a month", "numberOfGuests": null, "sockets": 1, @@ -86,12 +94,14 @@ exports[`InventoryListSelectors should populate data from the in memory cache: c Object { "error": false, "fulfilled": true, + "itemCount": 0, "listData": Array [ Object { "cores": 2, "displayName": "db.lorem.com", "hardwareType": "physical", "insightsId": "d6214a0b-b344-4778-831c-d53dcacb2da3", + "inventoryId": null, "lastSeen": "17 days ago", "numberOfGuests": null, "sockets": 1, @@ -107,12 +117,14 @@ exports[`InventoryListSelectors should populate data from the in memory cache: c Object { "error": false, "fulfilled": true, + "itemCount": 0, "listData": Array [ Object { "cores": 2, "displayName": "db.lorem.com", "hardwareType": "physical", "insightsId": "d6214a0b-b344-4778-831c-d53dcacb2da3", + "inventoryId": null, "lastSeen": "17 days ago", "numberOfGuests": null, "sockets": 1, @@ -128,12 +140,14 @@ exports[`InventoryListSelectors should populate data from the in memory cache: c Object { "error": false, "fulfilled": true, + "itemCount": 0, "listData": Array [ Object { "cores": 2, "displayName": "db.ipsum.com", "hardwareType": "physical", "insightsId": "9358e312-1c9f-42f4-8910-dcef6e970852", + "inventoryId": null, "lastSeen": "in a month", "numberOfGuests": null, "sockets": 1, @@ -149,12 +163,14 @@ exports[`InventoryListSelectors should populate data on a product ID when the ap Object { "error": false, "fulfilled": true, + "itemCount": 0, "listData": Array [ Object { "cores": 2, "displayName": null, "hardwareType": null, "insightsId": "d6214a0b-b344-4778-831c-d53dcacb2da3", + "inventoryId": null, "lastSeen": "in a year", "numberOfGuests": null, "sockets": 1, @@ -165,6 +181,7 @@ Object { "displayName": "db.example.com", "hardwareType": "physical", "insightsId": "9358e312-1c9f-42f4-8910-dcef6e970852", + "inventoryId": null, "lastSeen": "in a month", "numberOfGuests": null, "sockets": null, diff --git a/src/redux/selectors/__tests__/inventoryListSelectors.test.js b/src/redux/selectors/__tests__/inventoryListSelectors.test.js index 6a7c895d4..e60ea061d 100644 --- a/src/redux/selectors/__tests__/inventoryListSelectors.test.js +++ b/src/redux/selectors/__tests__/inventoryListSelectors.test.js @@ -15,7 +15,7 @@ describe('InventoryListSelectors', () => { const props = { viewId: 'test', productId: undefined, - listQuery: {} + query: {} }; const state = { inventory: { @@ -56,7 +56,7 @@ describe('InventoryListSelectors', () => { const props = { viewId: 'test', productId: 'Lorem Ipsum missing expected properties', - listQuery: { + query: { [rhsmApiTypes.RHSM_API_QUERY_SLA_TYPES]: rhsmApiTypes.RHSM_API_QUERY_SLA_TYPES.PREMIUM } }; @@ -96,7 +96,7 @@ describe('InventoryListSelectors', () => { const props = { viewId: 'test', productId: 'Lorem Ipsum fulfilled aggregated output', - listQuery: { + query: { [rhsmApiTypes.RHSM_API_QUERY_SLA_TYPES]: rhsmApiTypes.RHSM_API_QUERY_SLA_TYPES.PREMIUM } }; @@ -141,7 +141,7 @@ describe('InventoryListSelectors', () => { const props = { viewId: 'cache-test', productId: 'Lorem Ipsum ID cached', - listQuery: { + query: { [rhsmApiTypes.RHSM_API_QUERY_SLA_TYPES]: rhsmApiTypes.RHSM_API_QUERY_SLA_TYPES.PREMIUM } }; diff --git a/src/redux/selectors/inventoryListSelectors.js b/src/redux/selectors/inventoryListSelectors.js index 79620151a..66e130762 100644 --- a/src/redux/selectors/inventoryListSelectors.js +++ b/src/redux/selectors/inventoryListSelectors.js @@ -1,11 +1,18 @@ -import { createSelector } from 'reselect'; +import { createSelectorCreator, defaultMemoize } from 'reselect'; import moment from 'moment'; import _isEqual from 'lodash/isEqual'; -import _camelCase from 'lodash/camelCase'; import { rhsmApiTypes } from '../../types/rhsmApiTypes'; import { reduxHelpers } from '../common/reduxHelpers'; import { getCurrentDate } from '../../common/dateHelpers'; +/** + * Create a custom "are objects equal" selector. + * + * @private + * @type {Function}} + */ +const createDeepEqualSelector = createSelectorCreator(defaultMemoize, _isEqual); + /** * Selector cache. * @@ -27,7 +34,7 @@ const statePropsFilter = (state, props = {}) => ({ ...{ viewId: props.viewId, productId: props.productId, - query: props.listQuery + query: props.query } }); @@ -36,7 +43,7 @@ const statePropsFilter = (state, props = {}) => ({ * * @type {{pending: boolean, fulfilled: boolean, listData: object, error: boolean, status: (*|number)}} */ -const selector = createSelector([statePropsFilter], response => { +const selector = createDeepEqualSelector([statePropsFilter], response => { const { viewId = null, productId = null, query = {}, metaId, metaQuery = {}, ...responseData } = response || {}; const updatedResponseData = { @@ -44,6 +51,7 @@ const selector = createSelector([statePropsFilter], response => { fulfilled: false, pending: responseData.pending || responseData.cancelled || false, listData: [], + itemCount: 0, status: responseData.status }; @@ -54,23 +62,25 @@ const selector = createSelector([statePropsFilter], response => { Object.assign(updatedResponseData, { ...cache }); + // Reset cache on viewId update if (viewId && selectorCache.dataId !== viewId) { selectorCache.dataId = viewId; selectorCache.data = {}; } if (responseData.fulfilled && productId === metaId && _isEqual(query, responseMetaQuery)) { - const inventory = responseData.data; - const listData = inventory?.[rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_DATA] || []; + const { + [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_DATA]: listData = [], + [rhsmApiTypes.RHSM_API_RESPONSE_META]: listMeta = {} + } = responseData.data || {}; updatedResponseData.listData.length = 0; - // Populate expected API response values with undefined - const [hostsSchema = {}] = reduxHelpers.setResponseSchemas([rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_DATA_TYPES]); - // Apply "display logic" then return a custom value for entries const customInventoryValue = ({ key, value }) => { switch (key) { + case rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_DATA_TYPES.HARDWARE: + return value?.toLowerCase() || null; case rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_DATA_TYPES.LAST_SEEN: return moment.utc(value).startOf('day').from(getCurrentDate()) || null; default: @@ -78,28 +88,24 @@ const selector = createSelector([statePropsFilter], response => { } }; - // Generate reflected properties - listData.forEach(value => { - const generateReflectedData = ({ dataObj, keyPrefix = '', customValue = null }) => { - const updatedDataObj = {}; - - Object.keys(dataObj).forEach(dataObjKey => { - const casedDataObjKey = _camelCase(`${keyPrefix} ${dataObjKey}`).trim(); - - if (typeof customValue === 'function') { - updatedDataObj[casedDataObjKey] = customValue({ data: dataObj, key: dataObjKey, value: value[dataObjKey] }); - } else { - updatedDataObj[casedDataObjKey] = value[dataObjKey]; - } - }); - - updatedResponseData.listData.push(updatedDataObj); - }; + // Generate normalized properties + const [updatedListData, updatedListMeta] = reduxHelpers.setNormalizedResponse( + { + schema: rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_DATA_TYPES, + data: listData, + customResponseValue: customInventoryValue + }, + { + schema: rhsmApiTypes.RHSM_API_RESPONSE_META_TYPES, + data: listMeta + } + ); - generateReflectedData({ dataObj: { ...hostsSchema, ...value }, customValue: customInventoryValue }); - }); + const [meta = {}] = updatedListMeta || []; // Update response and cache + updatedResponseData.itemCount = meta[rhsmApiTypes.RHSM_API_RESPONSE_META_TYPES.COUNT] ?? 0; + updatedResponseData.listData = updatedListData; updatedResponseData.fulfilled = true; selectorCache.data[`${viewId}_${productId}_${JSON.stringify(query)}`] = { ...updatedResponseData diff --git a/src/services/rhsmServices.js b/src/services/rhsmServices.js index 9eb99c91a..b033eef99 100644 --- a/src/services/rhsmServices.js +++ b/src/services/rhsmServices.js @@ -946,6 +946,7 @@ const getGraphCapacity = (id, params = {}) => * "data" : [ * { * "insights_id": "498cff02-8b4b-46f8-a655-56043XXX0d2f", + * "inventory_id": "498cff02-8b4b-46f8-a655-56043XXX0d2f", * "display_name": "ipsum.example.com", * "subscription_manager_id": "b6028fa4-cd26-449a-b122-2e65ad8e7d3e", * "cores": 4, @@ -956,12 +957,12 @@ const getGraphCapacity = (id, params = {}) => * }, * { * "insights_id": "499cff02-8b4b-46f8-a6xx-56043FFF0d2e", - * "display_name": "lorem.example.com", + * "inventory_id": "499cff02-8b4b-46f8-a6xx-56043FFF0d2e", * "subscription_manager_id": "b6028fa4-cd26-449a-b123-2e25aa8e7d3e", * "cores": 4, * "sockets": 6, * "hardware_type": "physical", - * "number_of_guests": 2, + * "number_of_guests": 0, * "last_seen": "2020-06-20T00:00:00Z" * } * ], diff --git a/src/types/__tests__/__snapshots__/index.test.js.snap b/src/types/__tests__/__snapshots__/index.test.js.snap index f2110742b..e8906bacc 100644 --- a/src/types/__tests__/__snapshots__/index.test.js.snap +++ b/src/types/__tests__/__snapshots__/index.test.js.snap @@ -90,6 +90,7 @@ Object { "GUESTS": "number_of_guests", "HARDWARE": "hardware_type", "ID": "insights_id", + "INVENTORY_ID": "inventory_id", "LAST_SEEN": "last_seen", "NAME": "display_name", "SOCKETS": "sockets", @@ -216,6 +217,7 @@ Object { "GUESTS": "number_of_guests", "HARDWARE": "hardware_type", "ID": "insights_id", + "INVENTORY_ID": "inventory_id", "LAST_SEEN": "last_seen", "NAME": "display_name", "SOCKETS": "sockets", @@ -341,6 +343,7 @@ Object { "GUESTS": "number_of_guests", "HARDWARE": "hardware_type", "ID": "insights_id", + "INVENTORY_ID": "inventory_id", "LAST_SEEN": "last_seen", "NAME": "display_name", "SOCKETS": "sockets", @@ -470,6 +473,7 @@ Object { "GUESTS": "number_of_guests", "HARDWARE": "hardware_type", "ID": "insights_id", + "INVENTORY_ID": "inventory_id", "LAST_SEEN": "last_seen", "NAME": "display_name", "SOCKETS": "sockets", diff --git a/src/types/rhsmApiTypes.js b/src/types/rhsmApiTypes.js index 441c7b3ba..38304f5a8 100644 --- a/src/types/rhsmApiTypes.js +++ b/src/types/rhsmApiTypes.js @@ -105,14 +105,15 @@ const RHSM_API_RESPONSE_INVENTORY_DATA = 'data'; * @type {{CORES: string, HARDWARE: string, SOCKETS: string, ID: string, NAME: string, LAST_SEEN: string}} */ const RHSM_API_RESPONSE_INVENTORY_DATA_TYPES = { + CORES: 'cores', + GUESTS: 'number_of_guests', + HARDWARE: 'hardware_type', ID: 'insights_id', + INVENTORY_ID: 'inventory_id', + LAST_SEEN: 'last_seen', NAME: 'display_name', - SUBSCRIPTION_ID: 'subscription_manager_id', - CORES: 'cores', SOCKETS: 'sockets', - HARDWARE: 'hardware_type', - GUESTS: 'number_of_guests', - LAST_SEEN: 'last_seen' + SUBSCRIPTION_ID: 'subscription_manager_id' }; /** diff --git a/tests/__snapshots__/code.test.js.snap b/tests/__snapshots__/code.test.js.snap index 75b7ff9e8..53619333c 100644 --- a/tests/__snapshots__/code.test.js.snap +++ b/tests/__snapshots__/code.test.js.snap @@ -2,8 +2,8 @@ exports[`General code checks should only have specific console.[warn|log|info|error] methods: console methods 1`] = ` Array [ - "redux/common/reduxHelpers.js:167: console.error(\`Error: Property \${prop} does not exist within the passed state.\`, state);", - "redux/common/reduxHelpers.js:171: console.warn(\`Warning: Property \${prop} does not exist within the passed initialState.\`, initialState);", + "redux/common/reduxHelpers.js:225: console.error(\`Error: Property \${prop} does not exist within the passed state.\`, state);", + "redux/common/reduxHelpers.js:229: console.warn(\`Warning: Property \${prop} does not exist within the passed initialState.\`, initialState);", "setupTests.js:70: console.error = (message, ...args) => {", ] `; From a56b27bcbd8c76439f92369b0e0e73a241a17c6b Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Sun, 9 Aug 2020 22:29:31 -0400 Subject: [PATCH 03/28] feat(inventoryList,views): issues/10 display inventory list (#372) * styles, min-heights graphCard, inventoryList * inventoryList, initial inventory list display * inventoryListHelpers, parse table cells * inventoryList perPageDefault for skeleton loader * pagination, initial pagination * rhelView, openshiftView add inventoryList * locale, i18n strings --- public/locales/en-US.json | 12 +- .../__tests__/__snapshots__/i18n.test.js.snap | 38 ++- .../__snapshots__/inventoryList.test.js.snap | 206 +++++++++++++++ .../inventoryListHelpers.test.js.snap | 124 +++++++++ .../__tests__/inventoryList.test.js | 42 +++ .../__tests__/inventoryListHelpers.test.js | 50 ++++ src/components/inventoryList/inventoryList.js | 198 +++++++++++++++ .../inventoryList/inventoryListHelpers.js | 65 +++++ .../__snapshots__/openshiftView.test.js.snap | 240 ++++++++++++++++++ .../__tests__/openshiftView.test.js | 21 ++ src/components/openshiftView/openshiftView.js | 114 +++++++-- .../__snapshots__/pagination.test.js.snap | 123 +++++++++ .../pagination/__tests__/pagination.test.js | 86 +++++++ src/components/pagination/pagination.js | 154 +++++++++++ .../__snapshots__/rhelView.test.js.snap | 223 ++++++++++++++++ .../rhelView/__tests__/rhelView.test.js | 21 ++ src/components/rhelView/rhelView.js | 85 ++++++- src/styles/_fade.scss | 9 + src/styles/_inventoryList.scss | 37 +++ src/styles/_usage-graph.scss | 2 + src/styles/index.scss | 1 + 21 files changed, 1823 insertions(+), 28 deletions(-) create mode 100644 src/components/inventoryList/__tests__/__snapshots__/inventoryList.test.js.snap create mode 100644 src/components/inventoryList/__tests__/__snapshots__/inventoryListHelpers.test.js.snap create mode 100644 src/components/inventoryList/__tests__/inventoryList.test.js create mode 100644 src/components/inventoryList/__tests__/inventoryListHelpers.test.js create mode 100644 src/components/inventoryList/inventoryList.js create mode 100644 src/components/inventoryList/inventoryListHelpers.js create mode 100644 src/components/pagination/__tests__/__snapshots__/pagination.test.js.snap create mode 100644 src/components/pagination/__tests__/pagination.test.js create mode 100644 src/components/pagination/pagination.js create mode 100644 src/styles/_inventoryList.scss diff --git a/public/locales/en-US.json b/public/locales/en-US.json index 09fb083cb..cb4b724ea 100644 --- a/public/locales/en-US.json +++ b/public/locales/en-US.json @@ -42,11 +42,21 @@ "tooltipSummary": "Your subscription data facets. With one level of column and row headers." }, "curiosity-inventory": { + "cardHeading": "Current systems", "tableAriaLabel": "{{appName}} systems inventory table.", "tableSummary": "A generated table with one level of column headers.", "tableEmptyInventoryTitle": "No results found", "tableEmptyInventoryMessage": "No results match the filter criteria. Remove filters or clear all filters to show results.", - "tableSkeletonAriaLabel": "Loading" + "tableSkeletonAriaLabel": "Loading", + "hardwareType_hypervisor": "Hypervisor", + "hardwareType_physical": "Physical", + "hardwareType_virtual": "Virtual", + "header": "{{context}}", + "header_cores": "Cores", + "header_displayName": "Name", + "header_hardwareType": "Infrastructure", + "header_sockets": "Sockets", + "header_lastSeen": "Last seen" }, "curiosity-toolbar": { "category": "Filter by", diff --git a/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap b/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap index f56f847f5..e4e01ba0f 100644 --- a/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap +++ b/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap @@ -181,6 +181,15 @@ Array [ }, ], }, + Object { + "file": "./src/components/inventoryList/inventoryListHelpers.js", + "keys": Array [ + Object { + "key": "curiosity-inventory.header", + "match": "translate('curiosity-inventory.header', { context: key })", + }, + ], + }, Object { "file": "./src/components/openshiftView/openshiftView.js", "keys": Array [ @@ -196,6 +205,14 @@ Array [ "key": "curiosity-graph.cardHeading", "match": "t('curiosity-graph.cardHeading')", }, + Object { + "key": "curiosity-inventory.cardHeading", + "match": "t('curiosity-inventory.cardHeading')", + }, + Object { + "key": "curiosity-inventory.hardwareType", + "match": "translate('curiosity-inventory.hardwareType', { context: hardwareType.value })", + }, ], }, Object { @@ -295,6 +312,14 @@ Array [ "key": "curiosity-graph.socketsHeading", "match": "t('curiosity-graph.socketsHeading')", }, + Object { + "key": "curiosity-inventory.cardHeading", + "match": "t('curiosity-inventory.cardHeading')", + }, + Object { + "key": "curiosity-inventory.hardwareType", + "match": "translate('curiosity-inventory.hardwareType', { context: hardwareType.value })", + }, ], }, Object { @@ -421,7 +446,18 @@ Array [ ] `; -exports[`I18n Component should have locale keys that exist in the default language JSON: missing locale keys 1`] = `Array []`; +exports[`I18n Component should have locale keys that exist in the default language JSON: missing locale keys 1`] = ` +Array [ + Object { + "file": "./src/components/openshiftView/openshiftView.js", + "key": "curiosity-inventory.hardwareType", + }, + Object { + "file": "./src/components/rhelView/rhelView.js", + "key": "curiosity-inventory.hardwareType", + }, +] +`; exports[`I18n Component should pass children: children 1`] = `"lorem ipsum"`; diff --git a/src/components/inventoryList/__tests__/__snapshots__/inventoryList.test.js.snap b/src/components/inventoryList/__tests__/__snapshots__/inventoryList.test.js.snap new file mode 100644 index 000000000..87db72be3 --- /dev/null +++ b/src/components/inventoryList/__tests__/__snapshots__/inventoryList.test.js.snap @@ -0,0 +1,206 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InventoryList Component should handle variations in data: filtered data 1`] = ` + + + + + </CardTitle> + <CardActions + className="" + > + <Connect(Pagination) + isCompact={true} + isDisabled={false} + itemCount={0} + itemsPerPageDefault={10} + productId="lorem" + viewId="inventoryList" + /> + </CardActions> + </CardHeader> + <CardBody> + <div + className="fadein" + > + <Table + ariaLabel={null} + borders={true} + className="curiosity-inventory-list" + columnHeaders={ + Array [ + "t(curiosity-inventory.header, [object Object])", + ] + } + isHeader={true} + rows={ + Array [ + Object { + "cells": Array [ + "ipsum", + ], + }, + Object { + "cells": Array [ + "sit", + ], + }, + ] + } + summary={null} + t={[Function]} + variant="compact" + /> + </div> + </CardBody> + <CardFooter + className="" + > + <Connect(Pagination) + dropDirection="up" + isDisabled={false} + itemCount={0} + perPageDefault={10} + productId="lorem" + viewId="inventoryList" + /> + </CardFooter> +</Card> +`; + +exports[`InventoryList Component should handle variations in data: variable data 1`] = ` +<Card + className="curiosity-inventory-card" +> + <CardHeader> + <CardTitle> + <Title + headingLevel="h2" + size="lg" + /> + </CardTitle> + <CardActions + className="" + > + <Connect(Pagination) + isCompact={true} + isDisabled={false} + itemCount={0} + itemsPerPageDefault={10} + productId="lorem" + viewId="inventoryList" + /> + </CardActions> + </CardHeader> + <CardBody> + <div + className="fadein" + > + <Table + ariaLabel={null} + borders={true} + className="curiosity-inventory-list" + columnHeaders={ + Array [ + "t(curiosity-inventory.header, [object Object])", + "t(curiosity-inventory.header, [object Object])", + ] + } + isHeader={true} + rows={ + Array [ + Object { + "cells": Array [ + "ipsum", + "sit", + ], + }, + Object { + "cells": Array [ + "sit", + "amet", + ], + }, + ] + } + summary={null} + t={[Function]} + variant="compact" + /> + </div> + </CardBody> + <CardFooter + className="" + > + <Connect(Pagination) + dropDirection="up" + isDisabled={false} + itemCount={0} + perPageDefault={10} + productId="lorem" + viewId="inventoryList" + /> + </CardFooter> +</Card> +`; + +exports[`InventoryList Component should render a non-connected component: non-connected 1`] = ` +<Card + className="curiosity-inventory-card" +> + <CardHeader> + <CardTitle> + <Title + headingLevel="h2" + size="lg" + /> + </CardTitle> + <CardActions + className="" + > + <Connect(Pagination) + isCompact={true} + isDisabled={false} + itemCount={0} + itemsPerPageDefault={10} + productId="lorem" + viewId="inventoryList" + /> + </CardActions> + </CardHeader> + <CardBody> + <div + className="fadein" + > + <Table + ariaLabel={null} + borders={true} + className="curiosity-inventory-list" + columnHeaders={Array []} + isHeader={true} + rows={Array []} + summary={null} + t={[Function]} + variant="compact" + /> + </div> + </CardBody> + <CardFooter + className="" + > + <Connect(Pagination) + dropDirection="up" + isDisabled={false} + itemCount={0} + perPageDefault={10} + productId="lorem" + viewId="inventoryList" + /> + </CardFooter> +</Card> +`; diff --git a/src/components/inventoryList/__tests__/__snapshots__/inventoryListHelpers.test.js.snap b/src/components/inventoryList/__tests__/__snapshots__/inventoryListHelpers.test.js.snap new file mode 100644 index 000000000..260902cb9 --- /dev/null +++ b/src/components/inventoryList/__tests__/__snapshots__/inventoryListHelpers.test.js.snap @@ -0,0 +1,124 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InventoryListHelpers parseRowCellsListData should parse and return formatted/filtered table cells.: basic cell data 1`] = ` +Object { + "cells": Array [ + "ipsum", + "sit", + ], + "columnHeaders": Array [ + "t(curiosity-inventory.header, [object Object])", + "t(curiosity-inventory.header, [object Object])", + ], + "data": Object { + "dolor": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": "sit", + }, + "lorem": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": "ipsum", + }, + }, +} +`; + +exports[`InventoryListHelpers parseRowCellsListData should parse and return formatted/filtered table cells.: custom callback data 1`] = ` +Object { + "cells": Array [ + "ipsum/sit", + ], + "columnHeaders": Array [ + "t(curiosity-inventory.header, [object Object])/t(curiosity-inventory.header, [object Object])", + ], + "data": Object { + "dolor": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": "sit", + }, + "lorem": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": "ipsum", + }, + }, +} +`; + +exports[`InventoryListHelpers parseRowCellsListData should parse and return formatted/filtered table cells.: custom cell data 1`] = ` +Object { + "cells": Array [ + Object { + "props": Object { + "textCenter": true, + }, + "title": "object, cell, lorem", + }, + ], + "columnHeaders": Array [ + "t(curiosity-inventory.header, [object Object])", + ], + "data": Object { + "dolor": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": "sit", + }, + "lorem": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": "ipsum", + }, + }, +} +`; + +exports[`InventoryListHelpers parseRowCellsListData should parse and return formatted/filtered table cells.: custom header data 1`] = ` +Object { + "cells": Array [ + "ipsum", + ], + "columnHeaders": Array [ + Object { + "props": Object { + "textCenter": true, + }, + "title": "object, header, lorem", + }, + ], + "data": Object { + "dolor": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": "sit", + }, + "lorem": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": "ipsum", + }, + }, +} +`; + +exports[`InventoryListHelpers parseRowCellsListData should parse and return formatted/filtered table cells.: filtered data 1`] = ` +Object { + "cells": Array [ + "ipsum", + ], + "columnHeaders": Array [ + "t(curiosity-inventory.header, [object Object])", + ], + "data": Object { + "dolor": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": "sit", + }, + "lorem": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": "ipsum", + }, + }, +} +`; + +exports[`InventoryListHelpers should have specific functions: inventoryListHelpers 1`] = ` +Object { + "parseRowCellsListData": [Function], +} +`; diff --git a/src/components/inventoryList/__tests__/inventoryList.test.js b/src/components/inventoryList/__tests__/inventoryList.test.js new file mode 100644 index 000000000..6d6e0420b --- /dev/null +++ b/src/components/inventoryList/__tests__/inventoryList.test.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { InventoryList } from '../inventoryList'; +import { rhsmApiTypes } from '../../../types'; + +describe('InventoryList Component', () => { + it('should render a non-connected component', () => { + const props = { + query: { + [rhsmApiTypes.RHSM_API_QUERY_LIMIT]: 10, + [rhsmApiTypes.RHSM_API_QUERY_OFFSET]: 0 + }, + productId: 'lorem' + }; + + const component = shallow(<InventoryList {...props} />); + expect(component).toMatchSnapshot('non-connected'); + }); + + it('should handle variations in data', () => { + const props = { + query: { + [rhsmApiTypes.RHSM_API_QUERY_LIMIT]: 10, + [rhsmApiTypes.RHSM_API_QUERY_OFFSET]: 0 + }, + productId: 'lorem', + listData: [ + { lorem: 'ipsum', dolor: 'sit' }, + { lorem: 'sit', dolor: 'amet' } + ] + }; + + const component = shallow(<InventoryList {...props} />); + expect(component).toMatchSnapshot('variable data'); + + component.setProps({ + filterInventoryData: [{ id: 'lorem' }] + }); + + expect(component).toMatchSnapshot('filtered data'); + }); +}); diff --git a/src/components/inventoryList/__tests__/inventoryListHelpers.test.js b/src/components/inventoryList/__tests__/inventoryListHelpers.test.js new file mode 100644 index 000000000..adb09d794 --- /dev/null +++ b/src/components/inventoryList/__tests__/inventoryListHelpers.test.js @@ -0,0 +1,50 @@ +import { inventoryListHelpers, parseRowCellsListData } from '../inventoryListHelpers'; + +describe('InventoryListHelpers', () => { + it('should have specific functions', () => { + expect(inventoryListHelpers).toMatchSnapshot('inventoryListHelpers'); + }); + + it('parseRowCellsListData should parse and return formatted/filtered table cells.', () => { + const filters = []; + const cellData = { + lorem: 'ipsum', + dolor: 'sit' + }; + + expect(parseRowCellsListData({ filters, cellData })).toMatchSnapshot('basic cell data'); + + filters.push({ + id: 'lorem' + }); + + expect(parseRowCellsListData({ filters, cellData })).toMatchSnapshot('filtered data'); + + filters[0] = { + id: 'lorem', + cell: { + title: 'object, cell, lorem', + props: { textCenter: true } + } + }; + + expect(parseRowCellsListData({ filters, cellData })).toMatchSnapshot('custom cell data'); + + filters[0] = { + id: 'lorem', + header: { + title: 'object, header, lorem', + props: { textCenter: true } + } + }; + + expect(parseRowCellsListData({ filters, cellData })).toMatchSnapshot('custom header data'); + + filters[0] = { + header: ({ lorem, dolor }) => `${lorem.title}/${dolor.title}`, + cell: ({ lorem, dolor }) => `${lorem.value}/${dolor.value}` + }; + + expect(parseRowCellsListData({ filters, cellData })).toMatchSnapshot('custom callback data'); + }); +}); diff --git a/src/components/inventoryList/inventoryList.js b/src/components/inventoryList/inventoryList.js new file mode 100644 index 000000000..f5ca0ac85 --- /dev/null +++ b/src/components/inventoryList/inventoryList.js @@ -0,0 +1,198 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import _isEqual from 'lodash/isEqual'; +import { TableVariant } from '@patternfly/react-table'; +import { Card, CardActions, CardBody, CardFooter, CardHeader, CardTitle, Title } from '@patternfly/react-core'; +import { helpers } from '../../common'; +import { connect, reduxActions, reduxSelectors } from '../../redux'; +import Table from '../table/table'; +import { Loader } from '../loader/loader'; +import { inventoryListHelpers } from './inventoryListHelpers'; +import Pagination from '../pagination/pagination'; +import { rhsmApiTypes } from '../../types'; + +class InventoryList extends React.Component { + componentDidMount() { + this.onUpdateInventoryData(); + } + + componentDidUpdate(prevProps) { + const { productId, query } = this.props; + + if (productId !== prevProps.productId || !_isEqual(query, prevProps.query)) { + this.onUpdateInventoryData(); + } + } + + onUpdateInventoryData = () => { + const { getHostsInventory, isDisabled, productId, query } = this.props; + + if (!isDisabled && productId) { + getHostsInventory(productId, query); + } + }; + + renderTable() { + const { filterInventoryData, listData } = this.props; + let updatedColumnHeaders = []; + + const updatedRows = listData.map(({ ...cellData }) => { + const { columnHeaders, cells } = inventoryListHelpers.parseRowCellsListData({ + filters: filterInventoryData, + cellData + }); + + updatedColumnHeaders = columnHeaders; + + return { + cells + }; + }); + + return ( + <Table + borders + variant={TableVariant.compact} + className="curiosity-inventory-list" + columnHeaders={updatedColumnHeaders} + rows={updatedRows} + /> + ); + } + + render() { + const { + cardTitle, + error, + filterInventoryData, + isDisabled, + itemCount, + listData, + pending, + perPageDefault, + productId, + query, + viewId + } = this.props; + + if (isDisabled) { + return null; + } + + const updatedPerPage = query?.[rhsmApiTypes.RHSM_API_QUERY_LIMIT] || perPageDefault; + const loaderPerPage = updatedPerPage / 2; + + return ( + <Card className="curiosity-inventory-card"> + <CardHeader> + <CardTitle> + <Title headingLevel="h2" size="lg"> + {cardTitle} + + + + + + + +
+ {pending && ( + + )} + {!pending && this.renderTable()} +
+
+ + + +
+ ); + } +} + +InventoryList.propTypes = { + error: PropTypes.bool, + cardTitle: PropTypes.string, + filterInventoryData: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + header: PropTypes.oneOfType([ + PropTypes.shape({ + title: PropTypes.string + }), + PropTypes.func + ]), + cell: PropTypes.oneOfType([ + PropTypes.shape({ + title: PropTypes.string + }), + PropTypes.func + ]) + }).isRequired + ), + getHostsInventory: PropTypes.func, + isDisabled: PropTypes.bool, + itemCount: PropTypes.number, + listData: PropTypes.array, + pending: PropTypes.bool, + productId: PropTypes.string.isRequired, + perPageDefault: PropTypes.number, + query: PropTypes.object.isRequired, + viewId: PropTypes.string +}; + +InventoryList.defaultProps = { + error: false, + cardTitle: null, + filterInventoryData: [], + getHostsInventory: helpers.noop, + isDisabled: helpers.UI_DISABLED_TABLE, + itemCount: 0, + listData: [], + pending: false, + perPageDefault: 10, + viewId: 'inventoryList' +}; + +/** + * Apply actions to props. + * + * @param {Function} dispatch + * @returns {object} + */ +const mapDispatchToProps = dispatch => ({ + getHostsInventory: (id, query) => dispatch(reduxActions.rhsm.getHostsInventory(id, query)) +}); + +/** + * Create a selector from applied state, props. + * + * @type {Function} + */ +const makeMapStateToProps = reduxSelectors.inventoryList.makeInventoryList(); + +const ConnectedInventoryList = connect(makeMapStateToProps, mapDispatchToProps)(InventoryList); + +export { ConnectedInventoryList as default, ConnectedInventoryList, InventoryList }; diff --git a/src/components/inventoryList/inventoryListHelpers.js b/src/components/inventoryList/inventoryListHelpers.js new file mode 100644 index 000000000..eb224541d --- /dev/null +++ b/src/components/inventoryList/inventoryListHelpers.js @@ -0,0 +1,65 @@ +import { translate } from '../i18n/i18n'; + +/** + * Parse and return formatted/filtered table cells. + * + * @param {object} params + * @param {Array} params.filters + * @param {object}params.cellData + * @returns {{columnHeaders: Array, cells: Array, data: object}} + */ +const parseRowCellsListData = ({ filters = [], cellData = {} }) => { + const updatedColumnHeaders = []; + const updatedCells = []; + const allCells = {}; + + // Apply translation and value, "pre" filters/callbacks + Object.entries(cellData).forEach(([key, value]) => { + allCells[key] = { + title: translate('curiosity-inventory.header', { context: key }), + value + }; + + updatedColumnHeaders.push(allCells[key].title); + updatedCells.push(value); + }); + + // Apply header and cell values, apply filters/callbacks + if (filters?.length) { + updatedColumnHeaders.length = 0; + updatedCells.length = 0; + + filters.forEach(({ id, cell, header }) => { + let headerUpdated; + let cellUpdated; + + if (allCells[id]) { + headerUpdated = allCells[id].title; + cellUpdated = allCells[id].value; + } + + if (header) { + headerUpdated = (typeof header === 'function' && header({ ...allCells })) || header; + } + + if (cell) { + cellUpdated = (typeof cell === 'function' && cell({ ...allCells })) || cell; + } + + updatedColumnHeaders.push(headerUpdated); + updatedCells.push(cellUpdated); + }); + } + + return { + columnHeaders: updatedColumnHeaders, + cells: updatedCells, + data: { ...allCells } + }; +}; + +const inventoryListHelpers = { + parseRowCellsListData +}; + +export { inventoryListHelpers as default, inventoryListHelpers, parseRowCellsListData }; diff --git a/src/components/openshiftView/__tests__/__snapshots__/openshiftView.test.js.snap b/src/components/openshiftView/__tests__/__snapshots__/openshiftView.test.js.snap index 9d03e20fd..9f51f4a3e 100644 --- a/src/components/openshiftView/__tests__/__snapshots__/openshiftView.test.js.snap +++ b/src/components/openshiftView/__tests__/__snapshots__/openshiftView.test.js.snap @@ -20,6 +20,8 @@ exports[`OpenshiftView Component should display an alternate graph on query-stri query={ Object { "granularity": "daily", + "limit": 10, + "offset": 0, } } viewId="OpenShift" @@ -34,10 +36,12 @@ exports[`OpenshiftView Component should display an alternate graph on query-stri "color": "#06c", "fill": "#8bc1f7", "id": "cores", + "optional": true, "stroke": "#06c", }, Object { "id": "thresholdCores", + "optional": true, }, ] } @@ -47,6 +51,8 @@ exports[`OpenshiftView Component should display an alternate graph on query-stri query={ Object { "granularity": "daily", + "limit": 10, + "offset": 0, } } viewId="OpenShift" @@ -78,6 +84,40 @@ exports[`OpenshiftView Component should display an alternate graph on query-stri /> + + + `; @@ -101,6 +141,8 @@ exports[`OpenshiftView Component should have a fallback title: title 1`] = ` query={ Object { "granularity": "daily", + "limit": 10, + "offset": 0, } } viewId="OpenShift" @@ -115,10 +157,12 @@ exports[`OpenshiftView Component should have a fallback title: title 1`] = ` "color": "#06c", "fill": "#8bc1f7", "id": "cores", + "optional": true, "stroke": "#06c", }, Object { "id": "thresholdCores", + "optional": true, }, ] } @@ -128,6 +172,8 @@ exports[`OpenshiftView Component should have a fallback title: title 1`] = ` query={ Object { "granularity": "daily", + "limit": 10, + "offset": 0, } } viewId="OpenShift" @@ -159,9 +205,163 @@ exports[`OpenshiftView Component should have a fallback title: title 1`] = ` /> + + + `; +exports[`OpenshiftView Component should have default props that set product configuration: filter results 1`] = ` +Object { + "cells": Array [ + , + + t(curiosity-inventory.hardwareType, [object Object]) + + + 3 + + , + 10, + 12, + "lorem month ago", + ], + "columnHeaders": Array [ + "t(curiosity-inventory.header, [object Object])", + "t(curiosity-inventory.header, [object Object])", + "t(curiosity-inventory.header, [object Object])", + "t(curiosity-inventory.header, [object Object])", + "t(curiosity-inventory.header, [object Object])", + ], + "data": Object { + "cores": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": 12, + }, + "displayName": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": "lorem", + }, + "hardwareType": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": "ipsum", + }, + "inventoryId": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": "lorem inventory id", + }, + "lastSeen": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": "lorem month ago", + }, + "numberOfGuests": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": 3, + }, + "sockets": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": 10, + }, + }, +} +`; + +exports[`OpenshiftView Component should have default props that set product configuration: initial configuration 1`] = ` +Object { + "initialGraphFilters": Array [ + Object { + "color": "#06c", + "fill": "#8bc1f7", + "id": "cores", + "optional": true, + "stroke": "#06c", + }, + Object { + "color": "#06c", + "fill": "#8bc1f7", + "id": "sockets", + "optional": true, + "stroke": "#06c", + }, + Object { + "id": "thresholdSockets", + "optional": true, + }, + Object { + "id": "thresholdCores", + "optional": true, + }, + ], + "initialInventoryFilters": Array [ + Object { + "cell": [Function], + "id": "displayName", + }, + Object { + "cell": [Function], + "id": "hardwareType", + }, + Object { + "id": "sockets", + "optional": true, + }, + Object { + "id": "cores", + "optional": true, + }, + Object { + "id": "lastSeen", + }, + ], + "query": Object { + "granularity": "daily", + "limit": 10, + "offset": 0, + }, +} +`; + exports[`OpenshiftView Component should render a non-connected component: non-connected 1`] = ` + + + `; diff --git a/src/components/openshiftView/__tests__/openshiftView.test.js b/src/components/openshiftView/__tests__/openshiftView.test.js index 73e7e87ed..aa3b8b5bb 100644 --- a/src/components/openshiftView/__tests__/openshiftView.test.js +++ b/src/components/openshiftView/__tests__/openshiftView.test.js @@ -1,6 +1,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { OpenshiftView } from '../openshiftView'; +import { parseRowCellsListData } from '../../inventoryList/inventoryListHelpers'; describe('OpenshiftView Component', () => { it('should render a non-connected component', () => { @@ -46,4 +47,24 @@ describe('OpenshiftView Component', () => { const component = shallow(); expect(component).toMatchSnapshot('alternate graph'); }); + + it('should have default props that set product configuration', () => { + const { initialGraphFilters, initialInventoryFilters, query } = OpenshiftView.defaultProps; + expect({ initialGraphFilters, initialInventoryFilters, query }).toMatchSnapshot('initial configuration'); + + const filteredInventoryData = parseRowCellsListData({ + filters: initialInventoryFilters, + cellData: { + displayName: 'lorem', + inventoryId: 'lorem inventory id', + hardwareType: 'ipsum', + numberOfGuests: 3, + sockets: 10, + cores: 12, + lastSeen: 'lorem month ago' + } + }); + + expect(filteredInventoryData).toMatchSnapshot('filter results'); + }); }); diff --git a/src/components/openshiftView/openshiftView.js b/src/components/openshiftView/openshiftView.js index fff7d6a69..0daa7228f 100644 --- a/src/components/openshiftView/openshiftView.js +++ b/src/components/openshiftView/openshiftView.js @@ -4,6 +4,7 @@ import { chart_color_blue_100 as chartColorBlueLight, chart_color_blue_300 as chartColorBlueDark } from '@patternfly/react-tokens'; +import { Badge, Button } from '@patternfly/react-core'; import { PageLayout, PageHeader, PageSection, PageToolbar } from '../pageLayout/pageLayout'; import { RHSM_API_QUERY_GRANULARITY_TYPES as GRANULARITY_TYPES, rhsmApiTypes } from '../../types/rhsmApiTypes'; import { connect, reduxSelectors } from '../../redux'; @@ -11,6 +12,7 @@ import GraphCard from '../graphCard/graphCard'; import C3GraphCard from '../c3GraphCard/c3GraphCard'; import { Select } from '../form/select'; import Toolbar from '../toolbar/toolbar'; +import InventoryList from '../inventoryList/inventoryList'; import { helpers } from '../../common'; import { translate } from '../i18n/i18n'; @@ -23,7 +25,8 @@ import { translate } from '../i18n/i18n'; class OpenshiftView extends React.Component { state = { option: null, - filters: [] + graphFilters: [], + inventoryFilters: [] }; componentDidMount() { @@ -39,14 +42,24 @@ class OpenshiftView extends React.Component { */ onSelect = (event = {}) => { const { option } = this.state; - const { initialFilters } = this.props; + const { initialGraphFilters, initialInventoryFilters } = this.props; const { value } = event; if (value !== option) { - const filters = initialFilters.filter(val => new RegExp(value, 'i').test(val.id)); + const filter = ({ id, optional }) => { + if (!optional) { + return true; + } + return new RegExp(value, 'i').test(id); + }; + + const graphFilters = initialGraphFilters.filter(filter); + const inventoryFilters = initialInventoryFilters.filter(filter); + this.setState({ option, - filters + graphFilters, + inventoryFilters }); } }; @@ -73,7 +86,7 @@ class OpenshiftView extends React.Component { * @returns {Node} */ render() { - const { filters } = this.state; + const { graphFilters, inventoryFilters } = this.state; const { query, initialToolbarFilters, location, routeDetail, t, viewId } = this.props; const isC3 = location?.parsedSearch?.c3 === ''; @@ -89,7 +102,7 @@ class OpenshiftView extends React.Component { {(isC3 && ( )} + + + ); } @@ -120,15 +143,16 @@ class OpenshiftView extends React.Component { /** * Prop types. * - * @type {{initialFilters: Array, initialOption: string, initialToolbarFilters: Array, viewId: string, - * t: Function, query: object, routeDetail: object, location: object}} + * @type {{initialOption: string, initialToolbarFilters: Array, viewId: string, t: Function, query: object, + * initialGraphFilters: Array, routeDetail: object, location: object, initialInventoryFilters: Array}} */ OpenshiftView.propTypes = { query: PropTypes.shape({ [rhsmApiTypes.RHSM_API_QUERY_GRANULARITY]: PropTypes.oneOf([...Object.values(GRANULARITY_TYPES)]) }), initialOption: PropTypes.oneOf(['cores', 'sockets']), - initialFilters: PropTypes.array, + initialGraphFilters: PropTypes.array, + initialInventoryFilters: PropTypes.array, initialToolbarFilters: PropTypes.array, location: PropTypes.shape({ parsedSearch: PropTypes.objectOf(PropTypes.string) @@ -147,24 +171,80 @@ OpenshiftView.propTypes = { /** * Default props. * - * @type {{initialFilters: Array, initialOption: string, initialToolbarFilters: Array, viewId: string, - * t: translate, query: object}} + * @type {{initialOption: string, initialToolbarFilters: Array, viewId: string, t: translate, query: object, + * initialGraphFilters: Array, initialInventoryFilters: Array}} */ OpenshiftView.defaultProps = { query: { - [rhsmApiTypes.RHSM_API_QUERY_GRANULARITY]: GRANULARITY_TYPES.DAILY + [rhsmApiTypes.RHSM_API_QUERY_GRANULARITY]: GRANULARITY_TYPES.DAILY, + [rhsmApiTypes.RHSM_API_QUERY_LIMIT]: 10, + [rhsmApiTypes.RHSM_API_QUERY_OFFSET]: 0 }, initialOption: 'cores', - initialFilters: [ - { id: 'cores', fill: chartColorBlueLight.value, stroke: chartColorBlueDark.value, color: chartColorBlueDark.value }, + initialGraphFilters: [ + { + id: 'cores', + optional: true, + fill: chartColorBlueLight.value, + stroke: chartColorBlueDark.value, + color: chartColorBlueDark.value + }, { id: 'sockets', + optional: true, fill: chartColorBlueLight.value, stroke: chartColorBlueDark.value, color: chartColorBlueDark.value }, - { id: 'thresholdSockets' }, - { id: 'thresholdCores' } + { id: 'thresholdSockets', optional: true }, + { id: 'thresholdCores', optional: true } + ], + initialInventoryFilters: [ + { + id: 'displayName', + cell: obj => { + const { displayName, inventoryId } = obj; + + if (!inventoryId.value) { + return displayName.value; + } + + return ( + + ); + } + }, + { + id: 'hardwareType', + cell: obj => { + const { hardwareType, numberOfGuests } = obj; + return ( + + {translate('curiosity-inventory.hardwareType', { context: hardwareType.value })}{' '} + {(numberOfGuests.value && {numberOfGuests.value}) || ''} + + ); + } + }, + { + id: 'sockets', + optional: true + }, + { + id: 'cores', + optional: true + }, + { + id: 'lastSeen' + } ], initialToolbarFilters: [ { diff --git a/src/components/pagination/__tests__/__snapshots__/pagination.test.js.snap b/src/components/pagination/__tests__/__snapshots__/pagination.test.js.snap new file mode 100644 index 000000000..f0e037b9c --- /dev/null +++ b/src/components/pagination/__tests__/__snapshots__/pagination.test.js.snap @@ -0,0 +1,123 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Pagination Component should handle per-page limit, and page offset updates: page, offset 1`] = ` +Object { + "pageOffset": 0, + "pagePage": 2, + "pagePerPage": 20, +} +`; + +exports[`Pagination Component should handle per-page limit, and page offset updates: per-page, limit 1`] = ` +Object { + "perPageOffset": 0, + "perPagePage": 1, + "perPagePerPage": 20, +} +`; + +exports[`Pagination Component should handle updating paging for redux state: dispatch filter 1`] = ` +Array [ + Array [ + Array [ + Object { + "offset": 10, + "type": "SET_QUERY_PAGE_OFFSET_RHSM", + "viewId": "pagination", + }, + Object { + "offset": 10, + "type": "SET_QUERY_PAGE_OFFSET_RHSM", + "viewId": "lorem", + }, + Object { + "limit": 10, + "type": "SET_QUERY_PAGE_LIMIT_RHSM", + "viewId": "pagination", + }, + Object { + "limit": 10, + "type": "SET_QUERY_PAGE_LIMIT_RHSM", + "viewId": "lorem", + }, + ], + ], + Array [ + Array [ + Object { + "limit": 20, + "type": "SET_QUERY_PAGE_LIMIT_RHSM", + "viewId": "pagination", + }, + Object { + "limit": 20, + "type": "SET_QUERY_PAGE_LIMIT_RHSM", + "viewId": "lorem", + }, + ], + ], +] +`; + +exports[`Pagination Component should render a non-connected component: non-connected 1`] = ` + +`; diff --git a/src/components/pagination/__tests__/pagination.test.js b/src/components/pagination/__tests__/pagination.test.js new file mode 100644 index 000000000..e2f2ec7ca --- /dev/null +++ b/src/components/pagination/__tests__/pagination.test.js @@ -0,0 +1,86 @@ +import React from 'react'; +import { mount, shallow } from 'enzyme'; +import { Pagination } from '../pagination'; +import { store } from '../../../redux'; +import { rhsmApiTypes } from '../../../types/rhsmApiTypes'; + +describe('Pagination Component', () => { + let mockDispatch; + + beforeEach(() => { + mockDispatch = jest.spyOn(store, 'dispatch').mockImplementation((type, data) => ({ type, data })); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render a non-connected component', () => { + const props = { + productId: 'lorem' + }; + + const component = shallow(); + expect(component).toMatchSnapshot('non-connected'); + }); + + it('should handle per-page limit, and page offset updates', () => { + const props = { + productId: 'lorem', + itemCount: 39, + query: { + [rhsmApiTypes.RHSM_API_QUERY_LIMIT]: 20 + } + }; + + const component = shallow(); + const { offset: perPageOffset, page: perPagePage, perPage: perPagePerPage } = component.props(); + expect({ + perPageOffset, + perPagePage, + perPagePerPage + }).toMatchSnapshot('per-page, limit'); + + component.setProps({ + query: { + ...props.query, + [rhsmApiTypes.RHSM_API_QUERY_OFFSET]: 20 + } + }); + + const { offset: pageOffset, page: pagePage, perPage: pagePerPage } = component.props(); + expect({ + pageOffset, + pagePage, + pagePerPage + }).toMatchSnapshot('page, offset'); + }); + + it('should handle updating paging for redux state', () => { + const props = { + productId: 'lorem', + itemCount: 39, + query: { + [rhsmApiTypes.RHSM_API_QUERY_LIMIT]: 10, + [rhsmApiTypes.RHSM_API_QUERY_OFFSET]: 0 + } + }; + const component = mount(); + + const filterMethods = () => { + const componentInstance = component.instance(); + + const filters = [ + { method: 'onPage', value: { page: 2 } }, + { method: 'onPerPage', value: { perPage: 20 } } + ]; + + filters.forEach(({ method, value }) => { + componentInstance[method](value); + }); + }; + + filterMethods(); + expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch filter'); + }); +}); diff --git a/src/components/pagination/pagination.js b/src/components/pagination/pagination.js new file mode 100644 index 000000000..c7fa564a4 --- /dev/null +++ b/src/components/pagination/pagination.js @@ -0,0 +1,154 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Pagination as PfPagination } from '@patternfly/react-core'; +import { connect, reduxTypes, store } from '../../redux'; +import { rhsmApiTypes } from '../../types/rhsmApiTypes'; + +// ToDo: Apply locale/translation to the PF Pagination "titles" prop. +/** + * Contained pagination. + * + * @augments React.Component + * @fires onClear + * @fires onPage + * @fires onPerPage + */ +class Pagination extends React.Component { + /** + * Update page state. + * + * @event onPage + * @param {object} params + * @param {number} params.page + */ + onPage = ({ page }) => { + const { query, perPageDefault, productId, viewId } = this.props; + const updatedPerPage = query?.[rhsmApiTypes.RHSM_API_QUERY_LIMIT] || perPageDefault; + const offset = updatedPerPage * (page - 1) || 0; + + store.dispatch([ + { + type: reduxTypes.query.SET_QUERY_PAGE_OFFSET_RHSM, + viewId, + [rhsmApiTypes.RHSM_API_QUERY_OFFSET]: offset + }, + { + type: reduxTypes.query.SET_QUERY_PAGE_OFFSET_RHSM, + viewId: productId, + [rhsmApiTypes.RHSM_API_QUERY_OFFSET]: offset + }, + { + type: reduxTypes.query.SET_QUERY_PAGE_LIMIT_RHSM, + viewId, + [rhsmApiTypes.RHSM_API_QUERY_LIMIT]: updatedPerPage + }, + { + type: reduxTypes.query.SET_QUERY_PAGE_LIMIT_RHSM, + viewId: productId, + [rhsmApiTypes.RHSM_API_QUERY_LIMIT]: updatedPerPage + } + ]); + }; + + /** + * Update per-page state. + * + * @event onPerPage + * @param {object} params + * @param {number} params.perPage + */ + onPerPage = ({ perPage }) => { + const { productId, viewId } = this.props; + + store.dispatch([ + { + type: reduxTypes.query.SET_QUERY_PAGE_LIMIT_RHSM, + viewId, + [rhsmApiTypes.RHSM_API_QUERY_LIMIT]: perPage + }, + { + type: reduxTypes.query.SET_QUERY_PAGE_LIMIT_RHSM, + viewId: productId, + [rhsmApiTypes.RHSM_API_QUERY_LIMIT]: perPage + } + ]); + }; + + // ToDo: Consider using the PfPagination "offset" prop + /** + * Render pagination. + * + * @returns {Node} + */ + render() { + const { query, dropDirection, isCompact, isDisabled, itemCount, perPageDefault, variant } = this.props; + const updatedPage = query[rhsmApiTypes.RHSM_API_QUERY_OFFSET] / query[rhsmApiTypes.RHSM_API_QUERY_LIMIT] + 1 || 1; + const updatedPerPage = query[rhsmApiTypes.RHSM_API_QUERY_LIMIT] || perPageDefault; + + return ( + this.onPage({ event, page })} + onPerPageSelect={(event, perPage) => this.onPerPage({ event, perPage })} + variant={variant} + /> + ); + } +} + +/** + * Prop types + * + * @type {{}} + */ +Pagination.propTypes = { + query: PropTypes.shape({ + [rhsmApiTypes.RHSM_API_QUERY_LIMIT]: PropTypes.number, + [rhsmApiTypes.RHSM_API_QUERY_OFFSET]: PropTypes.number + }), + dropDirection: PropTypes.oneOf(['up', 'down']), + isCompact: PropTypes.bool, + isDisabled: PropTypes.bool, + itemCount: PropTypes.number, + perPageDefault: PropTypes.number, + productId: PropTypes.string.isRequired, + variant: PropTypes.string, + viewId: PropTypes.string +}; + +/** + * Default props. + * + * @type {{}} + */ +Pagination.defaultProps = { + query: {}, + dropDirection: 'down', + isCompact: false, + isDisabled: false, + itemCount: 0, + perPageDefault: 10, + variant: null, + viewId: 'pagination' +}; + +/** + * Apply state to props. + * + * @param {object} state + * @param {object} state.view + * @param {object} props + * @param {string} props.productId + * @returns {object} + */ +const mapStateToProps = ({ view }, { productId }) => ({ query: view.query?.[productId] }); + +const ConnectedPagination = connect(mapStateToProps)(Pagination); + +export { ConnectedPagination as default, ConnectedPagination, Pagination }; diff --git a/src/components/rhelView/__tests__/__snapshots__/rhelView.test.js.snap b/src/components/rhelView/__tests__/__snapshots__/rhelView.test.js.snap index 99b150ec8..4807c4724 100644 --- a/src/components/rhelView/__tests__/__snapshots__/rhelView.test.js.snap +++ b/src/components/rhelView/__tests__/__snapshots__/rhelView.test.js.snap @@ -24,6 +24,8 @@ exports[`RhelView Component should display an alternate graph on query-string up query={ Object { "granularity": "daily", + "limit": 10, + "offset": 0, } } viewId="RHEL" @@ -63,6 +65,41 @@ exports[`RhelView Component should display an alternate graph on query-string up query={ Object { "granularity": "daily", + "limit": 10, + "offset": 0, + } + } + viewId="RHEL" + /> + + + + + + `; +exports[`RhelView Component should have default props that set product configuration: filter results 1`] = ` +Object { + "cells": Array [ + , + + t(curiosity-inventory.hardwareType, [object Object]) + + + 3 + + , + 10, + "lorem month ago", + ], + "columnHeaders": Array [ + "t(curiosity-inventory.header, [object Object])", + "t(curiosity-inventory.header, [object Object])", + "t(curiosity-inventory.header, [object Object])", + "t(curiosity-inventory.header, [object Object])", + ], + "data": Object { + "cores": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": 12, + }, + "displayName": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": "lorem", + }, + "hardwareType": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": "ipsum", + }, + "inventoryId": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": "lorem inventory id", + }, + "lastSeen": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": "lorem month ago", + }, + "numberOfGuests": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": 3, + }, + "sockets": Object { + "title": "t(curiosity-inventory.header, [object Object])", + "value": 10, + }, + }, +} +`; + +exports[`RhelView Component should have default props that set product configuration: initial configuration 1`] = ` +Object { + "initialGraphFilters": Array [ + Object { + "color": "#06c", + "fill": "#8bc1f7", + "id": "physicalSockets", + "stroke": "#06c", + }, + Object { + "color": "#009596", + "fill": "#a2d9d9", + "id": "hypervisorSockets", + "stroke": "#009596", + }, + Object { + "color": "#5752d1", + "fill": "#b2b0ea", + "id": "cloudSockets", + "stroke": "#5752d1", + }, + Object { + "id": "thresholdSockets", + }, + ], + "initialInventoryFilters": Array [ + Object { + "cell": [Function], + "id": "displayName", + }, + Object { + "cell": [Function], + "id": "hardwareType", + }, + Object { + "id": "sockets", + }, + Object { + "id": "lastSeen", + }, + ], + "query": Object { + "granularity": "daily", + "limit": 10, + "offset": 0, + }, +} +`; + exports[`RhelView Component should render a non-connected component: non-connected 1`] = ` + + + { it('should render a non-connected component', () => { @@ -46,4 +47,24 @@ describe('RhelView Component', () => { const component = shallow(); expect(component).toMatchSnapshot('alternate graph'); }); + + it('should have default props that set product configuration', () => { + const { initialGraphFilters, initialInventoryFilters, query } = RhelView.defaultProps; + expect({ initialGraphFilters, initialInventoryFilters, query }).toMatchSnapshot('initial configuration'); + + const filteredInventoryData = parseRowCellsListData({ + filters: initialInventoryFilters, + cellData: { + displayName: 'lorem', + inventoryId: 'lorem inventory id', + hardwareType: 'ipsum', + numberOfGuests: 3, + sockets: 10, + cores: 12, + lastSeen: 'lorem month ago' + } + }); + + expect(filteredInventoryData).toMatchSnapshot('filter results'); + }); }); diff --git a/src/components/rhelView/rhelView.js b/src/components/rhelView/rhelView.js index 1d8218dd5..f967eace6 100644 --- a/src/components/rhelView/rhelView.js +++ b/src/components/rhelView/rhelView.js @@ -8,12 +8,14 @@ import { chart_color_purple_100 as chartColorPurpleLight, chart_color_purple_300 as chartColorPurpleDark } from '@patternfly/react-tokens'; +import { Badge, Button } from '@patternfly/react-core'; import { PageLayout, PageHeader, PageSection, PageToolbar } from '../pageLayout/pageLayout'; import { RHSM_API_QUERY_GRANULARITY_TYPES as GRANULARITY_TYPES, rhsmApiTypes } from '../../types/rhsmApiTypes'; import { connect, reduxSelectors } from '../../redux'; import GraphCard from '../graphCard/graphCard'; import C3GraphCard from '../c3GraphCard/c3GraphCard'; import Toolbar from '../toolbar/toolbar'; +import InventoryList from '../inventoryList/inventoryList'; import { helpers } from '../../common'; import { translate } from '../i18n/i18n'; @@ -31,7 +33,16 @@ class RhelView extends React.Component { * @returns {Node} */ render() { - const { query, initialFilters, initialToolbarFilters, location, routeDetail, t, viewId } = this.props; + const { + query, + initialGraphFilters, + initialInventoryFilters, + initialToolbarFilters, + location, + routeDetail, + t, + viewId + } = this.props; const isC3 = location?.parsedSearch?.c3 === ''; return ( @@ -46,7 +57,7 @@ class RhelView extends React.Component { {(isC3 && ( )} + + + ); } @@ -73,14 +94,15 @@ class RhelView extends React.Component { /** * Prop types. * - * @type {{initialFilters: Array, initialToolbarFilters: Array, viewId: string, t: Function, query: object, - * routeDetail: object, location: object}} + * @type {{initialToolbarFilters: Array, viewId: string, t: Function, query: object, initialGraphFilters: Array, + * routeDetail: object, location: object, initialInventoryFilters: Array}} */ RhelView.propTypes = { query: PropTypes.shape({ [rhsmApiTypes.RHSM_API_QUERY_GRANULARITY]: PropTypes.oneOf([...Object.values(GRANULARITY_TYPES)]) }), - initialFilters: PropTypes.array, + initialGraphFilters: PropTypes.array, + initialInventoryFilters: PropTypes.array, initialToolbarFilters: PropTypes.array, location: PropTypes.shape({ parsedSearch: PropTypes.objectOf(PropTypes.string) @@ -99,13 +121,16 @@ RhelView.propTypes = { /** * Default props. * - * @type {{initialFilters: Array, initialToolbarFilters: Array, viewId: string, t: translate, query: object}} + * @type {{initialToolbarFilters: Array, viewId: string, t: translate, query: object, + * initialGraphFilters: Array, initialInventoryFilters: Array}} */ RhelView.defaultProps = { query: { - [rhsmApiTypes.RHSM_API_QUERY_GRANULARITY]: GRANULARITY_TYPES.DAILY + [rhsmApiTypes.RHSM_API_QUERY_GRANULARITY]: GRANULARITY_TYPES.DAILY, + [rhsmApiTypes.RHSM_API_QUERY_LIMIT]: 10, + [rhsmApiTypes.RHSM_API_QUERY_OFFSET]: 0 }, - initialFilters: [ + initialGraphFilters: [ { id: 'physicalSockets', fill: chartColorBlueLight.value, @@ -126,6 +151,48 @@ RhelView.defaultProps = { }, { id: 'thresholdSockets' } ], + initialInventoryFilters: [ + { + id: 'displayName', + cell: obj => { + const { displayName, inventoryId } = obj; + + if (!inventoryId.value) { + return displayName.value; + } + + return ( + + ); + } + }, + { + id: 'hardwareType', + cell: obj => { + const { hardwareType, numberOfGuests } = obj; + return ( + + {translate('curiosity-inventory.hardwareType', { context: hardwareType.value })}{' '} + {(numberOfGuests.value && {numberOfGuests.value}) || ''} + + ); + } + }, + { + id: 'sockets' + }, + { + id: 'lastSeen' + } + ], initialToolbarFilters: [ { id: rhsmApiTypes.RHSM_API_QUERY_SLA diff --git a/src/styles/_fade.scss b/src/styles/_fade.scss index 3788bb919..b93b897d4 100644 --- a/src/styles/_fade.scss +++ b/src/styles/_fade.scss @@ -22,4 +22,13 @@ } @keyframes fadeout { 0%{opacity:1} 100%{opacity:0} } + + .hidden { + position:absolute; + left:-10000px; + top:auto; + width:1px; + height:1px; + overflow:hidden; + } } diff --git a/src/styles/_inventoryList.scss b/src/styles/_inventoryList.scss new file mode 100644 index 000000000..762418ab1 --- /dev/null +++ b/src/styles/_inventoryList.scss @@ -0,0 +1,37 @@ +.curiosity { + &.pf-c-page__main-section { + box-shadow: var(--pf-global--BoxShadow--sm); + + .pf-c-card { + box-shadow: none; + } + } + + .curiosity-inventory-card { + .pf-c-card__header { + border-bottom: var(--pf-global--BorderWidth--sm) solid var(--pf-global--BorderColor--100); + padding-top: 0; + padding-bottom: 1em; + } + + .pf-c-card__title { + padding: 0; + } + + .pf-c-card__body { + padding-left: 0; + padding-right: 0; + min-height: 300px; + } + } + + .curiosity-inventory-list { + .pf-c-table.pf-m-compact tr:not(.pf-c-table__expandable-row) > :first-child { + padding-left: var(--pf-c-table--m-compact--cell--first-last-child--PaddingLeft);; + } + } + + .curiosity-inventory-guestlist { + border-top-width: 0; + } +} diff --git a/src/styles/_usage-graph.scss b/src/styles/_usage-graph.scss index dcece156b..57014b7cf 100644 --- a/src/styles/_usage-graph.scss +++ b/src/styles/_usage-graph.scss @@ -1,5 +1,7 @@ .curiosity { .curiosity-usage-graph { + min-height: 410px; + .pf-c-card__header { border-bottom: var(--pf-global--BorderWidth--sm) solid var(--pf-global--BorderColor--100); padding-bottom: 1em; diff --git a/src/styles/index.scss b/src/styles/index.scss index 88b76eb3f..d2707c78c 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -16,3 +16,4 @@ @import 'table'; @import 'form'; @import 'toolbar'; +@import 'inventoryList'; From 9c72a70563156cd014af00c0aeb0ee329725269c Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Tue, 11 Aug 2020 14:23:41 -0400 Subject: [PATCH 04/28] fix(graphCard,c3GraphCard): issues/10 align card component (#372) * c3GraphCard, align to graphCard * graphCard, align to inventoryList card --- src/components/c3GraphCard/c3GraphCard.js | 52 ++- .../__snapshots__/c3GraphCard.test.js.snap | 380 ++++++++++++++++-- .../c3GraphCard/tests/c3GraphCard.test.js | 4 +- .../__snapshots__/graphCard.test.js.snap | 71 +++- src/components/graphCard/graphCard.js | 28 +- 5 files changed, 449 insertions(+), 86 deletions(-) diff --git a/src/components/c3GraphCard/c3GraphCard.js b/src/components/c3GraphCard/c3GraphCard.js index 83c10930c..949154da3 100644 --- a/src/components/c3GraphCard/c3GraphCard.js +++ b/src/components/c3GraphCard/c3GraphCard.js @@ -1,7 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Card, CardTitle, CardHeader, CardActions, CardBody } from '@patternfly/react-core'; -import { Skeleton, SkeletonSize } from '@redhat-cloud-services/frontend-components/components/cjs/Skeleton'; +import { Card, CardTitle, CardHeader, CardActions, CardBody, Title } from '@patternfly/react-core'; import _isEqual from 'lodash/isEqual'; import { Select } from '../form/select'; import { connect, reduxActions, reduxSelectors, reduxTypes, store } from '../../redux'; @@ -11,6 +10,7 @@ import { c3GraphCardHelpers } from './c3GraphCardHelpers'; import { C3GraphCardLegendItem } from './c3GraphCardLegendItem'; import { graphCardTypes } from '../graphCard/graphCardTypes'; import { C3Chart } from '../c3Chart/c3Chart'; +import { Loader } from '../loader/loader'; import { translate } from '../i18n/i18n'; /** @@ -41,8 +41,8 @@ class C3GraphCard extends React.Component { * @event onUpdateGraphData */ onUpdateGraphData = () => { - const { getGraphReportsCapacity, query, isDisabled, productId } = this.props; - const graphGranularity = query && query[rhsmApiTypes.RHSM_API_QUERY_GRANULARITY]; + const { getGraphReportsCapacity, isDisabled, productId, query } = this.props; + const graphGranularity = query?.[rhsmApiTypes.RHSM_API_QUERY_GRANULARITY]; if (!isDisabled && graphGranularity && productId) { const { startDate, endDate } = dateHelpers.getRangedDateTime(graphGranularity); @@ -121,9 +121,8 @@ class C3GraphCard extends React.Component { * @returns {Node} */ renderChart() { - const { filterGraphData, graphData, query, selectOptionsType, productId, productShortLabel } = this.props; - - const graphGranularity = query && query[rhsmApiTypes.RHSM_API_QUERY_GRANULARITY]; + const { filterGraphData, graphData, productId, productShortLabel, query, selectOptionsType } = this.props; + const graphGranularity = query?.[rhsmApiTypes.RHSM_API_QUERY_GRANULARITY]; const { selected } = graphCardTypes.getGranularityOptions(selectOptionsType); const updatedGranularity = graphGranularity || selected; @@ -171,20 +170,24 @@ class C3GraphCard extends React.Component { * @returns {Node} */ render() { - const { cardTitle, children, error, query, isDisabled, selectOptionsType, pending, t } = this.props; + const { cardTitle, children, error, isDisabled, pending, query, selectOptionsType, t } = this.props; if (isDisabled) { return null; } const { options } = graphCardTypes.getGranularityOptions(selectOptionsType); - const graphGranularity = query && query[rhsmApiTypes.RHSM_API_QUERY_GRANULARITY]; + const graphGranularity = query?.[rhsmApiTypes.RHSM_API_QUERY_GRANULARITY]; return ( - + - {cardTitle} - + + + {cardTitle} + + + {children}
@@ -184,13 +191,20 @@ Object { } `; -exports[`C3GraphCard Component should render multiple states: fulfilled 1`] = ` +exports[`C3GraphCard Component should render multiple states: error with 403 status 1`] = ` - - + + + </CardTitle> + <CardActions + className="blur" + > <Select aria-label="t(curiosity-graph.dropdownPlaceholder)" ariaLabel="Select option" @@ -230,7 +244,7 @@ exports[`C3GraphCard Component should render multiple states: fulfilled 1`] = ` </CardHeader> <CardBody> <div - className="curiosity-skeleton-container " + className="blur" > <C3Chart className={null} @@ -334,13 +348,20 @@ exports[`C3GraphCard Component should render multiple states: fulfilled 1`] = ` </Card> `; -exports[`C3GraphCard Component should render multiple states: pending 1`] = ` +exports[`C3GraphCard Component should render multiple states: error with 500 status 1`] = ` <Card - className="curiosity-usage-graph fadein" + className="curiosity-usage-graph" > <CardHeader> - <CardTitle /> - <CardActions> + <CardTitle> + <Title + headingLevel="h2" + size="lg" + /> + </CardTitle> + <CardActions + className="blur" + > <Select aria-label="t(curiosity-graph.dropdownPlaceholder)" ariaLabel="Select option" @@ -380,24 +401,331 @@ exports[`C3GraphCard Component should render multiple states: pending 1`] = ` </CardHeader> <CardBody> <div - className="curiosity-skeleton-container " + className="blur" > - <Skeleton - isDark={false} - size="xs" - /> - <Skeleton - isDark={false} - size="sm" + <C3Chart + className={null} + config={ + Object { + "axis": Object { + "x": Object { + "padding": 0, + "tick": Object { + "format": [Function], + }, + "type": "timeseries", + }, + "y": Object { + "default": Array [ + 0, + 50, + ], + "min": 0, + "padding": Object { + "bottom": 0, + }, + "tick": Object { + "format": [Function], + "outer": false, + "show": false, + }, + }, + }, + "data": Object { + "colors": Object { + "physicalSockets": undefined, + }, + "columns": Array [ + Array [ + "x", + "2019-06-01", + "2019-06-08", + "2019-06-25", + ], + Array [ + "physicalSockets", + 10, + 12, + 3, + ], + ], + "groups": Array [ + Array [ + "physicalSockets", + ], + ], + "names": Object { + "physicalSockets": "t(curiosity-graph.physicalSocketsLabel, [object Object])", + }, + "types": Object { + "physicalSockets": "area-spline", + }, + "x": "x", + }, + "grid": Object { + "y": Object { + "show": true, + }, + }, + "legend": Object { + "show": false, + }, + "padding": Object { + "bottom": 10, + "left": 40, + "right": 40, + "top": 10, + }, + "point": Object { + "show": false, + }, + "spline": Object { + "interpolation": Object { + "type": "monotone", + }, + }, + "tooltip": Object { + "format": Object { + "title": [Function], + "value": [Function], + }, + "order": [Function], + }, + "unloadBeforeLoad": true, + } + } + key="chart-lorem-daily" + onComplete={[Function]} + style={Object {}} + > + <Component /> + </C3Chart> + </div> + </CardBody> +</Card> +`; + +exports[`C3GraphCard Component should render multiple states: fulfilled 1`] = ` +<Card + className="curiosity-usage-graph" +> + <CardHeader> + <CardTitle> + <Title + headingLevel="h2" + size="lg" /> - <Skeleton - isDark={false} - size="md" + </CardTitle> + <CardActions + className="" + > + <Select + aria-label="t(curiosity-graph.dropdownPlaceholder)" + ariaLabel="Select option" + className="" + id="generatedid-" + isDisabled={false} + isToggleText={true} + name={null} + onSelect={[Function]} + options={ + Array [ + Object { + "selected": true, + "title": "t(curiosity-graph.dropdownDaily)", + "value": "daily", + }, + Object { + "title": "t(curiosity-graph.dropdownWeekly)", + "value": "weekly", + }, + Object { + "title": "t(curiosity-graph.dropdownMonthly)", + "value": "monthly", + }, + Object { + "title": "t(curiosity-graph.dropdownQuarterly)", + "value": "quarterly", + }, + ] + } + placeholder="t(curiosity-graph.dropdownPlaceholder)" + selectedOptions="daily" + toggleIcon={null} + variant="single" /> - <Skeleton - isDark={false} + </CardActions> + </CardHeader> + <CardBody> + <div + className="fadein" + > + <C3Chart + className={null} + config={ + Object { + "axis": Object { + "x": Object { + "padding": 0, + "tick": Object { + "format": [Function], + }, + "type": "timeseries", + }, + "y": Object { + "default": Array [ + 0, + 50, + ], + "min": 0, + "padding": Object { + "bottom": 0, + }, + "tick": Object { + "format": [Function], + "outer": false, + "show": false, + }, + }, + }, + "data": Object { + "colors": Object { + "physicalSockets": undefined, + }, + "columns": Array [ + Array [ + "x", + "2019-06-01", + "2019-06-08", + "2019-06-25", + ], + Array [ + "physicalSockets", + 10, + 12, + 3, + ], + ], + "groups": Array [ + Array [ + "physicalSockets", + ], + ], + "names": Object { + "physicalSockets": "t(curiosity-graph.physicalSocketsLabel, [object Object])", + }, + "types": Object { + "physicalSockets": "area-spline", + }, + "x": "x", + }, + "grid": Object { + "y": Object { + "show": true, + }, + }, + "legend": Object { + "show": false, + }, + "padding": Object { + "bottom": 10, + "left": 40, + "right": 40, + "top": 10, + }, + "point": Object { + "show": false, + }, + "spline": Object { + "interpolation": Object { + "type": "monotone", + }, + }, + "tooltip": Object { + "format": Object { + "title": [Function], + "value": [Function], + }, + "order": [Function], + }, + "unloadBeforeLoad": true, + } + } + key="chart-lorem-daily" + onComplete={[Function]} + style={Object {}} + > + <Component /> + </C3Chart> + </div> + </CardBody> +</Card> +`; + +exports[`C3GraphCard Component should render multiple states: pending 1`] = ` +<Card + className="curiosity-usage-graph" +> + <CardHeader> + <CardTitle> + <Title + headingLevel="h2" size="lg" /> + </CardTitle> + <CardActions + className="" + > + <Select + aria-label="t(curiosity-graph.dropdownPlaceholder)" + ariaLabel="Select option" + className="" + id="generatedid-" + isDisabled={false} + isToggleText={true} + name={null} + onSelect={[Function]} + options={ + Array [ + Object { + "selected": true, + "title": "t(curiosity-graph.dropdownDaily)", + "value": "daily", + }, + Object { + "title": "t(curiosity-graph.dropdownWeekly)", + "value": "weekly", + }, + Object { + "title": "t(curiosity-graph.dropdownMonthly)", + "value": "monthly", + }, + Object { + "title": "t(curiosity-graph.dropdownQuarterly)", + "value": "quarterly", + }, + ] + } + placeholder="t(curiosity-graph.dropdownPlaceholder)" + selectedOptions="daily" + toggleIcon={null} + variant="single" + /> + </CardActions> + </CardHeader> + <CardBody> + <div + className="fadein" + > + <Loader + skeletonProps={ + Object { + "size": "sm", + } + } + tableProps={Object {}} + variant="graph" + /> </div> </CardBody> </Card> diff --git a/src/components/c3GraphCard/tests/c3GraphCard.test.js b/src/components/c3GraphCard/tests/c3GraphCard.test.js index 7427e249e..2a9bd3ace 100644 --- a/src/components/c3GraphCard/tests/c3GraphCard.test.js +++ b/src/components/c3GraphCard/tests/c3GraphCard.test.js @@ -54,13 +54,13 @@ describe('C3GraphCard Component', () => { status: 403 }); - expect(component.find('.curiosity-skeleton-container').hasClass('blur')).toBe(true); + expect(component).toMatchSnapshot('error with 403 status'); component.setProps({ status: 500 }); - expect(component.find('.curiosity-skeleton-container').hasClass('blur')).toBe(true); + expect(component).toMatchSnapshot('error with 500 status'); component.setProps({ error: false, diff --git a/src/components/graphCard/__tests__/__snapshots__/graphCard.test.js.snap b/src/components/graphCard/__tests__/__snapshots__/graphCard.test.js.snap index 661293ad6..48389e879 100644 --- a/src/components/graphCard/__tests__/__snapshots__/graphCard.test.js.snap +++ b/src/components/graphCard/__tests__/__snapshots__/graphCard.test.js.snap @@ -66,11 +66,18 @@ Array [ exports[`GraphCard Component should render a non-connected component: non-connected 1`] = ` <Card - className="curiosity-usage-graph fadein" + className="curiosity-usage-graph" > <CardHeader> - <CardTitle /> - <CardActions> + <CardTitle> + <Title + headingLevel="h2" + size="lg" + /> + </CardTitle> + <CardActions + className="" + > <Select aria-label="t(curiosity-graph.dropdownPlaceholder)" ariaLabel="Select option" @@ -110,7 +117,7 @@ exports[`GraphCard Component should render a non-connected component: non-connec </CardHeader> <CardBody> <div - className="" + className="fadein" > <ChartArea chartLegend={[Function]} @@ -175,11 +182,18 @@ Object { exports[`GraphCard Component should render multiple states: error with 403 status 1`] = ` <Card - className="curiosity-usage-graph fadein" + className="curiosity-usage-graph" > <CardHeader> - <CardTitle /> - <CardActions> + <CardTitle> + <Title + headingLevel="h2" + size="lg" + /> + </CardTitle> + <CardActions + className="blur" + > <Select aria-label="t(curiosity-graph.dropdownPlaceholder)" ariaLabel="Select option" @@ -280,11 +294,18 @@ exports[`GraphCard Component should render multiple states: error with 403 statu exports[`GraphCard Component should render multiple states: error with 500 status 1`] = ` <Card - className="curiosity-usage-graph fadein" + className="curiosity-usage-graph" > <CardHeader> - <CardTitle /> - <CardActions> + <CardTitle> + <Title + headingLevel="h2" + size="lg" + /> + </CardTitle> + <CardActions + className="blur" + > <Select aria-label="t(curiosity-graph.dropdownPlaceholder)" ariaLabel="Select option" @@ -385,11 +406,18 @@ exports[`GraphCard Component should render multiple states: error with 500 statu exports[`GraphCard Component should render multiple states: fulfilled 1`] = ` <Card - className="curiosity-usage-graph fadein" + className="curiosity-usage-graph" > <CardHeader> - <CardTitle /> - <CardActions> + <CardTitle> + <Title + headingLevel="h2" + size="lg" + /> + </CardTitle> + <CardActions + className="" + > <Select aria-label="t(curiosity-graph.dropdownPlaceholder)" ariaLabel="Select option" @@ -429,7 +457,7 @@ exports[`GraphCard Component should render multiple states: fulfilled 1`] = ` </CardHeader> <CardBody> <div - className="" + className="fadein" > <ChartArea chartLegend={[Function]} @@ -490,11 +518,18 @@ exports[`GraphCard Component should render multiple states: fulfilled 1`] = ` exports[`GraphCard Component should render multiple states: pending 1`] = ` <Card - className="curiosity-usage-graph fadein" + className="curiosity-usage-graph" > <CardHeader> - <CardTitle /> - <CardActions> + <CardTitle> + <Title + headingLevel="h2" + size="lg" + /> + </CardTitle> + <CardActions + className="" + > <Select aria-label="t(curiosity-graph.dropdownPlaceholder)" ariaLabel="Select option" @@ -534,7 +569,7 @@ exports[`GraphCard Component should render multiple states: pending 1`] = ` </CardHeader> <CardBody> <div - className="" + className="fadein" > <Loader skeletonProps={ diff --git a/src/components/graphCard/graphCard.js b/src/components/graphCard/graphCard.js index 6c2fa2260..ed108f7a6 100644 --- a/src/components/graphCard/graphCard.js +++ b/src/components/graphCard/graphCard.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Card, CardTitle, CardHeader, CardActions, CardBody } from '@patternfly/react-core'; +import { Card, CardTitle, CardHeader, CardActions, CardBody, Title } from '@patternfly/react-core'; import { chart_color_green_300 as chartColorGreenDark } from '@patternfly/react-tokens'; import _isEqual from 'lodash/isEqual'; import { Select } from '../form/select'; @@ -28,7 +28,7 @@ class GraphCard extends React.Component { } componentDidUpdate(prevProps) { - const { query, productId } = this.props; + const { productId, query } = this.props; if (productId !== prevProps.productId || !_isEqual(query, prevProps.query)) { this.onUpdateGraphData(); @@ -41,8 +41,8 @@ class GraphCard extends React.Component { * @event onUpdateGraphData */ onUpdateGraphData = () => { - const { getGraphReportsCapacity, query, isDisabled, productId } = this.props; - const graphGranularity = query && query[rhsmApiTypes.RHSM_API_QUERY_GRANULARITY]; + const { getGraphReportsCapacity, isDisabled, productId, query } = this.props; + const graphGranularity = query?.[rhsmApiTypes.RHSM_API_QUERY_GRANULARITY]; if (!isDisabled && graphGranularity && productId) { const { startDate, endDate } = dateHelpers.getRangedDateTime(graphGranularity); @@ -84,8 +84,8 @@ class GraphCard extends React.Component { * @returns {Node} */ renderChart() { - const { filterGraphData, graphData, query, selectOptionsType, productShortLabel, viewId } = this.props; - const graphGranularity = query && query[rhsmApiTypes.RHSM_API_QUERY_GRANULARITY]; + const { filterGraphData, graphData, selectOptionsType, productShortLabel, query, viewId } = this.props; + const graphGranularity = query?.[rhsmApiTypes.RHSM_API_QUERY_GRANULARITY]; const { selected } = graphCardTypes.getGranularityOptions(selectOptionsType); const updatedGranularity = graphGranularity || selected; @@ -158,20 +158,24 @@ class GraphCard extends React.Component { * @returns {Node} */ render() { - const { cardTitle, children, error, query, isDisabled, selectOptionsType, pending, t } = this.props; + const { cardTitle, children, error, isDisabled, pending, query, selectOptionsType, t } = this.props; if (isDisabled) { return null; } const { options } = graphCardTypes.getGranularityOptions(selectOptionsType); - const graphGranularity = query && query[rhsmApiTypes.RHSM_API_QUERY_GRANULARITY]; + const graphGranularity = query?.[rhsmApiTypes.RHSM_API_QUERY_GRANULARITY]; return ( - <Card className="curiosity-usage-graph fadein"> + <Card className="curiosity-usage-graph"> <CardHeader> - <CardTitle>{cardTitle}</CardTitle> - <CardActions> + <CardTitle> + <Title headingLevel="h2" size="lg"> + {cardTitle} + + + {children} ; @@ -153,7 +172,7 @@ OpenshiftView.propTypes = { query: PropTypes.shape({ [RHSM_API_QUERY_TYPES.GRANULARITY]: PropTypes.oneOf([...Object.values(GRANULARITY_TYPES)]) }), - initialOption: PropTypes.oneOf(['cores', 'sockets']), + initialOption: PropTypes.oneOf(Object.values(RHSM_API_QUERY_UOM_TYPES)), initialGraphFilters: PropTypes.array, initialGuestsFilters: PropTypes.array, initialInventoryFilters: PropTypes.array, @@ -182,9 +201,10 @@ OpenshiftView.defaultProps = { query: { [RHSM_API_QUERY_TYPES.GRANULARITY]: GRANULARITY_TYPES.DAILY, [RHSM_API_QUERY_TYPES.LIMIT]: 10, - [RHSM_API_QUERY_TYPES.OFFSET]: 0 + [RHSM_API_QUERY_TYPES.OFFSET]: 0, + [RHSM_API_QUERY_TYPES.UOM]: RHSM_API_QUERY_UOM_TYPES.CORES }, - initialOption: 'cores', + initialOption: RHSM_API_QUERY_UOM_TYPES.CORES, initialGraphFilters: [ { id: 'cores', diff --git a/src/redux/reducers/__tests__/__snapshots__/viewReducer.test.js.snap b/src/redux/reducers/__tests__/__snapshots__/viewReducer.test.js.snap index b6d8fc975..73b8cfcce 100644 --- a/src/redux/reducers/__tests__/__snapshots__/viewReducer.test.js.snap +++ b/src/redux/reducers/__tests__/__snapshots__/viewReducer.test.js.snap @@ -63,6 +63,19 @@ Object { } `; +exports[`ViewReducer should handle specific defined types: defined type SET_QUERY_RHSM_uom 1`] = ` +Object { + "result": Object { + "query": Object { + "test_id": Object { + "uom": "lorem uom", + }, + }, + }, + "type": "SET_QUERY_RHSM_uom", +} +`; + exports[`ViewReducer should handle specific defined types: defined type SET_QUERY_RHSM_usage 1`] = ` Object { "result": Object { diff --git a/src/redux/reducers/__tests__/viewReducer.test.js b/src/redux/reducers/__tests__/viewReducer.test.js index 12a050b83..c3732af2f 100644 --- a/src/redux/reducers/__tests__/viewReducer.test.js +++ b/src/redux/reducers/__tests__/viewReducer.test.js @@ -14,10 +14,11 @@ describe('ViewReducer', () => { const dispatched = { type: value, [RHSM_API_QUERY_TYPES.GRANULARITY]: 'lorem granularity', - [RHSM_API_QUERY_TYPES.SLA]: 'lorem sla', - [RHSM_API_QUERY_TYPES.USAGE]: 'ipsum usage', [RHSM_API_QUERY_TYPES.LIMIT]: 10, [RHSM_API_QUERY_TYPES.OFFSET]: 10, + [RHSM_API_QUERY_TYPES.SLA]: 'lorem sla', + [RHSM_API_QUERY_TYPES.UOM]: 'lorem uom', + [RHSM_API_QUERY_TYPES.USAGE]: 'ipsum usage', viewId: 'test_id' }; diff --git a/src/redux/reducers/viewReducer.js b/src/redux/reducers/viewReducer.js index 141ebc0fa..e954456fb 100644 --- a/src/redux/reducers/viewReducer.js +++ b/src/redux/reducers/viewReducer.js @@ -49,13 +49,13 @@ const viewReducer = (state = initialState, action) => { reset: false } ); - case reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.SLA]: + case reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.LIMIT]: return reduxHelpers.setStateProp( 'query', { [action.viewId]: { ...state.query[action.viewId], - [RHSM_API_QUERY_TYPES.SLA]: action[RHSM_API_QUERY_TYPES.SLA] + [RHSM_API_QUERY_TYPES.LIMIT]: action[RHSM_API_QUERY_TYPES.LIMIT] } }, { @@ -63,13 +63,13 @@ const viewReducer = (state = initialState, action) => { reset: false } ); - case reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.USAGE]: + case reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.OFFSET]: return reduxHelpers.setStateProp( 'query', { [action.viewId]: { ...state.query[action.viewId], - [RHSM_API_QUERY_TYPES.USAGE]: action[RHSM_API_QUERY_TYPES.USAGE] + [RHSM_API_QUERY_TYPES.OFFSET]: action[RHSM_API_QUERY_TYPES.OFFSET] } }, { @@ -77,13 +77,13 @@ const viewReducer = (state = initialState, action) => { reset: false } ); - case reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.LIMIT]: + case reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.SLA]: return reduxHelpers.setStateProp( 'query', { [action.viewId]: { ...state.query[action.viewId], - [RHSM_API_QUERY_TYPES.LIMIT]: action[RHSM_API_QUERY_TYPES.LIMIT] + [RHSM_API_QUERY_TYPES.SLA]: action[RHSM_API_QUERY_TYPES.SLA] } }, { @@ -91,13 +91,27 @@ const viewReducer = (state = initialState, action) => { reset: false } ); - case reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.OFFSET]: + case reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.UOM]: return reduxHelpers.setStateProp( 'query', { [action.viewId]: { ...state.query[action.viewId], - [RHSM_API_QUERY_TYPES.OFFSET]: action[RHSM_API_QUERY_TYPES.OFFSET] + [RHSM_API_QUERY_TYPES.UOM]: action[RHSM_API_QUERY_TYPES.UOM] + } + }, + { + state, + reset: false + } + ); + case reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.USAGE]: + return reduxHelpers.setStateProp( + 'query', + { + [action.viewId]: { + ...state.query[action.viewId], + [RHSM_API_QUERY_TYPES.USAGE]: action[RHSM_API_QUERY_TYPES.USAGE] } }, { diff --git a/src/redux/types/__tests__/__snapshots__/index.test.js.snap b/src/redux/types/__tests__/__snapshots__/index.test.js.snap index e5b69a703..03884b39d 100644 --- a/src/redux/types/__tests__/__snapshots__/index.test.js.snap +++ b/src/redux/types/__tests__/__snapshots__/index.test.js.snap @@ -30,6 +30,7 @@ Object { "limit": "SET_QUERY_RHSM_limit", "offset": "SET_QUERY_RHSM_offset", "sla": "SET_QUERY_RHSM_sla", + "uom": "SET_QUERY_RHSM_uom", "usage": "SET_QUERY_RHSM_usage", }, }, @@ -72,6 +73,7 @@ Object { "limit": "SET_QUERY_RHSM_limit", "offset": "SET_QUERY_RHSM_offset", "sla": "SET_QUERY_RHSM_sla", + "uom": "SET_QUERY_RHSM_uom", "usage": "SET_QUERY_RHSM_usage", }, }, @@ -99,6 +101,7 @@ Object { "limit": "SET_QUERY_RHSM_limit", "offset": "SET_QUERY_RHSM_offset", "sla": "SET_QUERY_RHSM_sla", + "uom": "SET_QUERY_RHSM_uom", "usage": "SET_QUERY_RHSM_usage", }, }, @@ -169,6 +172,7 @@ Object { "limit": "SET_QUERY_RHSM_limit", "offset": "SET_QUERY_RHSM_offset", "sla": "SET_QUERY_RHSM_sla", + "uom": "SET_QUERY_RHSM_uom", "usage": "SET_QUERY_RHSM_usage", }, }, diff --git a/src/redux/types/queryTypes.js b/src/redux/types/queryTypes.js index 25d25315c..3333225b7 100644 --- a/src/redux/types/queryTypes.js +++ b/src/redux/types/queryTypes.js @@ -4,10 +4,11 @@ const SET_QUERY_CLEAR = 'SET_QUERY_CLEAR'; const SET_QUERY_RHSM_TYPES = { [RHSM_API_QUERY_TYPES.GRANULARITY]: `SET_QUERY_RHSM_${RHSM_API_QUERY_TYPES.GRANULARITY}`, - [RHSM_API_QUERY_TYPES.USAGE]: `SET_QUERY_RHSM_${RHSM_API_QUERY_TYPES.USAGE}`, - [RHSM_API_QUERY_TYPES.SLA]: `SET_QUERY_RHSM_${RHSM_API_QUERY_TYPES.SLA}`, [RHSM_API_QUERY_TYPES.LIMIT]: `SET_QUERY_RHSM_${RHSM_API_QUERY_TYPES.LIMIT}`, - [RHSM_API_QUERY_TYPES.OFFSET]: `SET_QUERY_RHSM_${RHSM_API_QUERY_TYPES.OFFSET}` + [RHSM_API_QUERY_TYPES.OFFSET]: `SET_QUERY_RHSM_${RHSM_API_QUERY_TYPES.OFFSET}`, + [RHSM_API_QUERY_TYPES.SLA]: `SET_QUERY_RHSM_${RHSM_API_QUERY_TYPES.SLA}`, + [RHSM_API_QUERY_TYPES.UOM]: `SET_QUERY_RHSM_${RHSM_API_QUERY_TYPES.UOM}`, + [RHSM_API_QUERY_TYPES.USAGE]: `SET_QUERY_RHSM_${RHSM_API_QUERY_TYPES.USAGE}` }; /** diff --git a/src/types/__tests__/__snapshots__/index.test.js.snap b/src/types/__tests__/__snapshots__/index.test.js.snap index 4a3fbeb4d..fd7b35dc5 100644 --- a/src/types/__tests__/__snapshots__/index.test.js.snap +++ b/src/types/__tests__/__snapshots__/index.test.js.snap @@ -47,6 +47,7 @@ Object { "LIMIT": "limit", "OFFSET": "offset", "SLA": "sla", + "UOM": "uom", "USAGE": "usage", }, "RHSM_API_QUERY_SET_OPTIN_TYPES": Object { @@ -77,8 +78,13 @@ Object { "START_DATE": "beginning", "TALLY_REPORT": "enable_tally_reporting", "TALLY_SYNC": "enable_tally_sync", + "UOM": "uom", "USAGE": "usage", }, + "RHSM_API_QUERY_UOM_TYPES": Object { + "CORES": "cores", + "SOCKETS": "sockets", + }, "RHSM_API_QUERY_USAGE_TYPES": Object { "DEVELOPMENT": "Development/Test", "DISASTER": "Disaster Recovery", @@ -198,6 +204,7 @@ Object { "LIMIT": "limit", "OFFSET": "offset", "SLA": "sla", + "UOM": "uom", "USAGE": "usage", }, "RHSM_API_QUERY_SET_OPTIN_TYPES": Object { @@ -228,8 +235,13 @@ Object { "START_DATE": "beginning", "TALLY_REPORT": "enable_tally_reporting", "TALLY_SYNC": "enable_tally_sync", + "UOM": "uom", "USAGE": "usage", }, + "RHSM_API_QUERY_UOM_TYPES": Object { + "CORES": "cores", + "SOCKETS": "sockets", + }, "RHSM_API_QUERY_USAGE_TYPES": Object { "DEVELOPMENT": "Development/Test", "DISASTER": "Disaster Recovery", @@ -348,6 +360,7 @@ Object { "LIMIT": "limit", "OFFSET": "offset", "SLA": "sla", + "UOM": "uom", "USAGE": "usage", }, "RHSM_API_QUERY_SET_OPTIN_TYPES": Object { @@ -378,8 +391,13 @@ Object { "START_DATE": "beginning", "TALLY_REPORT": "enable_tally_reporting", "TALLY_SYNC": "enable_tally_sync", + "UOM": "uom", "USAGE": "usage", }, + "RHSM_API_QUERY_UOM_TYPES": Object { + "CORES": "cores", + "SOCKETS": "sockets", + }, "RHSM_API_QUERY_USAGE_TYPES": Object { "DEVELOPMENT": "Development/Test", "DISASTER": "Disaster Recovery", @@ -502,6 +520,7 @@ Object { "LIMIT": "limit", "OFFSET": "offset", "SLA": "sla", + "UOM": "uom", "USAGE": "usage", }, "RHSM_API_QUERY_SET_OPTIN_TYPES": Object { @@ -532,8 +551,13 @@ Object { "START_DATE": "beginning", "TALLY_REPORT": "enable_tally_reporting", "TALLY_SYNC": "enable_tally_sync", + "UOM": "uom", "USAGE": "usage", }, + "RHSM_API_QUERY_UOM_TYPES": Object { + "CORES": "cores", + "SOCKETS": "sockets", + }, "RHSM_API_QUERY_USAGE_TYPES": Object { "DEVELOPMENT": "Development/Test", "DISASTER": "Disaster Recovery", diff --git a/src/types/rhsmApiTypes.js b/src/types/rhsmApiTypes.js index 9578d0bc5..5170bb821 100644 --- a/src/types/rhsmApiTypes.js +++ b/src/types/rhsmApiTypes.js @@ -202,6 +202,16 @@ const RHSM_API_QUERY_SLA_TYPES = { NONE: '' }; +/** + * RHSM API query/search parameter UOM type values. + * + * @type {{CORES: string, SOCKETS: string}} + */ +const RHSM_API_QUERY_UOM_TYPES = { + CORES: 'cores', + SOCKETS: 'sockets' +}; + /** * RHSM API query/search parameter USAGE type values. * @@ -214,12 +224,22 @@ const RHSM_API_QUERY_USAGE_TYPES = { UNSPECIFIED: '' }; +/** + * RHSM API query/search parameter OPTIN type values. + * + * @type {{TALLY_SYNC: string, TALLY_REPORT: string, CONDUIT_SYNC: string}} + */ const RHSM_API_QUERY_SET_OPTIN_TYPES = { CONDUIT_SYNC: 'enable_conduit_sync', TALLY_REPORT: 'enable_tally_reporting', TALLY_SYNC: 'enable_tally_sync' }; +/** + * RHSM API query/search parameter CAPACITY type values. + * + * @type {{GRANULARITY: string, USAGE: string, END_DATE: string, SLA: string, START_DATE: string}} + */ const RHSM_API_QUERY_SET_REPORT_CAPACITY_TYPES = { END_DATE: 'ending', GRANULARITY: 'granularity', @@ -228,18 +248,36 @@ const RHSM_API_QUERY_SET_REPORT_CAPACITY_TYPES = { USAGE: 'usage' }; +/** + * RHSM API query/search parameter INVENTORY type values. + * + * @type {{USAGE: string, UOM: string, OFFSET: string, SLA: string, LIMIT: string}} + */ const RHSM_API_QUERY_SET_INVENTORY_TYPES = { LIMIT: 'limit', OFFSET: 'offset', SLA: 'sla', + UOM: 'uom', USAGE: 'usage' }; +/** + * RHSM API query/search parameter GUESTS type values. + * + * @type {{OFFSET: string, LIMIT: string}} + */ const RHSM_API_QUERY_SET_INVENTORY_GUESTS_TYPES = { LIMIT: 'limit', OFFSET: 'offset' }; +/** + * RHSM API query/search parameter values. + * + * @type {{GRANULARITY: string, TALLY_SYNC: string, TALLY_REPORT: string, USAGE: string, + * UOM: string, END_DATE: string, SLA: string, OFFSET: string, START_DATE: string, + * LIMIT: string, CONDUIT_SYNC: string}} + */ const RHSM_API_QUERY_TYPES = { ...RHSM_API_QUERY_SET_OPTIN_TYPES, ...RHSM_API_QUERY_SET_REPORT_CAPACITY_TYPES, @@ -253,27 +291,32 @@ const RHSM_API_QUERY_TYPES = { * @type {{RHSM_API_QUERY_SET_INVENTORY_GUESTS_TYPES: {OFFSET: string, LIMIT: string}, * RHSM_API_RESPONSE_ERROR_DATA_CODE_TYPES: {GENERIC: string, OPTIN: string}, * RHSM_API_RESPONSE_INVENTORY_DATA: string, RHSM_API_RESPONSE_CAPACITY_DATA: string, - * RHSM_API_PATH_ID_TYPES: {RHEL_ARM: string, RHEL_WORKSTATION: string, RHEL_DESKTOP: string, RHEL: string, - * RHEL_SERVER: string, RHEL_IBM_Z: string, RHEL_COMPUTE_NODE: string, RHEL_IBM_POWER: string, - * RHEL_X86: string, OPENSHIFT: string}, RHSM_API_RESPONSE_ERROR_DATA_TYPES: {CODE: string, DETAIL: string}, + * RHSM_API_PATH_ID_TYPES: {RHEL_ARM: string, RHEL_WORKSTATION: string, RHEL_DESKTOP: string, + * RHEL: string, RHEL_SERVER: string, RHEL_IBM_Z: string, RHEL_COMPUTE_NODE: string, + * RHEL_IBM_POWER: string, RHEL_X86: string, OPENSHIFT: string}, + * RHSM_API_RESPONSE_ERROR_DATA_TYPES: {CODE: string, DETAIL: string}, * RHSM_API_QUERY_SET_OPTIN_TYPES: {TALLY_SYNC: string, TALLY_REPORT: string, CONDUIT_SYNC: string}, - * RHSM_API_QUERY_USAGE_TYPES: {UNSPECIFIED: string, DISASTER: string, DEVELOPMENT: string, PRODUCTION: string}, - * RHSM_API_QUERY_SLA_TYPES: {PREMIUM: string, SELF: string, NONE: string, STANDARD: string}, - * RHSM_API_QUERY_SET_INVENTORY_TYPES: {USAGE: string, OFFSET: string, SLA: string, LIMIT: string}, - * RHSM_API_RESPONSE_CAPACITY_DATA_TYPES: {HYPERVISOR_SOCKETS: string, CORES: string, DATE: string, SOCKETS: string, - * PHYSICAL_SOCKETS: string, HYPERVISOR_CORES: string, HAS_INFINITE: string, PHYSICAL_CORES: string}, - * RHSM_API_RESPONSE_META_TYPES: string, RHSM_API_RESPONSE_PRODUCTS_DATA_TYPES: {HYPERVISOR_SOCKETS: string, - * CORES: string, DATE: string, SOCKETS: string, HAS_DATA: string, PHYSICAL_SOCKETS: string, - * HYPERVISOR_CORES: string, PHYSICAL_CORES: string}, RHSM_API_RESPONSE_LINKS_TYPES: string, + * RHSM_API_QUERY_USAGE_TYPES: {UNSPECIFIED: string, DISASTER: string, DEVELOPMENT: string, + * PRODUCTION: string}, RHSM_API_QUERY_SLA_TYPES: {PREMIUM: string, SELF: string, NONE: string, + * STANDARD: string}, RHSM_API_QUERY_SET_INVENTORY_TYPES: {USAGE: string, UOM: string, + * OFFSET: string, SLA: string, LIMIT: string}, + * RHSM_API_RESPONSE_CAPACITY_DATA_TYPES: {HYPERVISOR_SOCKETS: string, CORES: string, DATE: string, + * SOCKETS: string, PHYSICAL_SOCKETS: string, HYPERVISOR_CORES: string, HAS_INFINITE: string, + * PHYSICAL_CORES: string}, RHSM_API_RESPONSE_META_TYPES: string, + * RHSM_API_RESPONSE_PRODUCTS_DATA_TYPES: {HYPERVISOR_SOCKETS: string, CORES: string, DATE: string, + * SOCKETS: string, HAS_DATA: string, PHYSICAL_SOCKETS: string, HYPERVISOR_CORES: string, + * PHYSICAL_CORES: string}, RHSM_API_RESPONSE_LINKS_TYPES: string, * RHSM_API_RESPONSE_INVENTORY_GUESTS_DATA_TYPES: {SUBSCRIPTION_ID: string, ID: string, NAME: string, - * LAST_SEEN: string}, RHSM_API_QUERY_GRANULARITY_TYPES: {WEEKLY: string, QUARTERLY: string, DAILY: string, - * MONTHLY: string}, RHSM_API_RESPONSE_ERROR_DATA: string, RHSM_API_RESPONSE_META: string, - * RHSM_API_RESPONSE_PRODUCTS_DATA: string, RHSM_API_QUERY_TYPES: {GRANULARITY: string, TALLY_SYNC: string, - * TALLY_REPORT: string, USAGE: string, END_DATE: string, SLA: string, OFFSET: string, START_DATE: string, + * LAST_SEEN: string}, RHSM_API_QUERY_GRANULARITY_TYPES: {WEEKLY: string, QUARTERLY: string, + * DAILY: string, MONTHLY: string}, RHSM_API_RESPONSE_ERROR_DATA: string, + * RHSM_API_RESPONSE_META: string, RHSM_API_RESPONSE_PRODUCTS_DATA: string, + * RHSM_API_QUERY_TYPES: {GRANULARITY: string, TALLY_SYNC: string, TALLY_REPORT: string, + * USAGE: string, UOM: string, END_DATE: string, SLA: string, OFFSET: string, START_DATE: string, * LIMIT: string, CONDUIT_SYNC: string}, RHSM_API_RESPONSE_INVENTORY_DATA_TYPES: {CORES: string, * HARDWARE: string, SOCKETS: string, ID: string, NAME: string, LAST_SEEN: string}, - * RHSM_API_QUERY_SET_REPORT_CAPACITY_TYPES: {GRANULARITY: string, USAGE: string, END_DATE: string, SLA: string, - * START_DATE: string}, RHSM_API_RESPONSE_LINKS: string}} + * RHSM_API_QUERY_UOM_TYPES: {CORES: string, SOCKETS: string}, + * RHSM_API_QUERY_SET_REPORT_CAPACITY_TYPES: {GRANULARITY: string, USAGE: string, END_DATE: string, + * SLA: string, START_DATE: string}, RHSM_API_RESPONSE_LINKS: string}} */ const rhsmApiTypes = { RHSM_API_RESPONSE_ERROR_DATA, @@ -293,6 +336,7 @@ const rhsmApiTypes = { RHSM_API_PATH_ID_TYPES, RHSM_API_QUERY_GRANULARITY_TYPES, RHSM_API_QUERY_SLA_TYPES, + RHSM_API_QUERY_UOM_TYPES, RHSM_API_QUERY_USAGE_TYPES, RHSM_API_QUERY_TYPES, RHSM_API_QUERY_SET_OPTIN_TYPES, @@ -321,6 +365,7 @@ export { RHSM_API_PATH_ID_TYPES, RHSM_API_QUERY_GRANULARITY_TYPES, RHSM_API_QUERY_SLA_TYPES, + RHSM_API_QUERY_UOM_TYPES, RHSM_API_QUERY_USAGE_TYPES, RHSM_API_QUERY_TYPES, RHSM_API_QUERY_SET_OPTIN_TYPES, From 66d717ee78ee50eb65e886da16617721266035a0 Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Tue, 8 Sep 2020 03:34:44 -0400 Subject: [PATCH 21/28] fix(pagination,toolbar): issues/400 page reset filter update (#412) * openshiftView, pass productId to toolbar, page reset helper * pagination, productId as primary state update * paginationHelper, centralize page reset * rhelView, pass productId to toolbar * toolbar, page reset helper on filter selection and clear --- .../__snapshots__/openshiftView.test.js.snap | 3 + src/components/openshiftView/openshiftView.js | 12 ++- .../__snapshots__/pagination.test.js.snap | 36 ++++----- .../paginationHelpers.test.js.snap | 26 +++++++ .../__tests__/paginationHelpers.test.js | 23 ++++++ src/components/pagination/pagination.js | 71 ++++++++++------- .../pagination/paginationHelpers.js | 31 ++++++++ .../__snapshots__/rhelView.test.js.snap | 3 + src/components/rhelView/rhelView.js | 7 +- .../__snapshots__/toolbar.test.js.snap | 77 +++++++++++++++++++ .../toolbar/__tests__/toolbar.test.js | 56 +++++++++++++- src/components/toolbar/toolbar.js | 39 ++++++---- 12 files changed, 318 insertions(+), 66 deletions(-) create mode 100644 src/components/pagination/__tests__/__snapshots__/paginationHelpers.test.js.snap create mode 100644 src/components/pagination/__tests__/paginationHelpers.test.js create mode 100644 src/components/pagination/paginationHelpers.js diff --git a/src/components/openshiftView/__tests__/__snapshots__/openshiftView.test.js.snap b/src/components/openshiftView/__tests__/__snapshots__/openshiftView.test.js.snap index 0915049de..19076249b 100644 --- a/src/components/openshiftView/__tests__/__snapshots__/openshiftView.test.js.snap +++ b/src/components/openshiftView/__tests__/__snapshots__/openshiftView.test.js.snap @@ -17,6 +17,7 @@ exports[`OpenshiftView Component should display an alternate graph on query-stri }, ] } + productId="lorem ipsum" query={ Object { "granularity": "daily", @@ -151,6 +152,7 @@ exports[`OpenshiftView Component should have a fallback title: title 1`] = ` }, ] } + productId="lorem ipsum" query={ Object { "granularity": "daily", @@ -467,6 +469,7 @@ exports[`OpenshiftView Component should render a non-connected component: non-co }, ] } + productId="lorem ipsum" query={ Object { "granularity": "daily", diff --git a/src/components/openshiftView/openshiftView.js b/src/components/openshiftView/openshiftView.js index 22c35b353..de9ed99a2 100644 --- a/src/components/openshiftView/openshiftView.js +++ b/src/components/openshiftView/openshiftView.js @@ -17,6 +17,7 @@ import C3GraphCard from '../c3GraphCard/c3GraphCard'; import { Select } from '../form/select'; import Toolbar from '../toolbar/toolbar'; import InventoryList from '../inventoryList/inventoryList'; +import { paginationHelpers } from '../pagination/paginationHelpers'; import { helpers } from '../../common'; import { translate } from '../i18n/i18n'; @@ -46,7 +47,7 @@ class OpenshiftView extends React.Component { */ onSelect = (event = {}) => { const { option } = this.state; - const { initialGraphFilters, initialInventoryFilters, viewId } = this.props; + const { initialGraphFilters, initialInventoryFilters, routeDetail, viewId } = this.props; const { value } = event; if (value !== option) { @@ -67,6 +68,8 @@ class OpenshiftView extends React.Component { inventoryFilters }, () => { + paginationHelpers.resetPage({ productId: routeDetail.pathParameter, viewId }); + store.dispatch({ type: reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.UOM], viewId, @@ -116,7 +119,12 @@ class OpenshiftView extends React.Component { {t(`curiosity-view.title`, { appName: helpers.UI_DISPLAY_NAME, context: viewId })} - + {(isC3 && ( diff --git a/src/components/pagination/__tests__/__snapshots__/pagination.test.js.snap b/src/components/pagination/__tests__/__snapshots__/pagination.test.js.snap index b6036024f..c4ae19a26 100644 --- a/src/components/pagination/__tests__/__snapshots__/pagination.test.js.snap +++ b/src/components/pagination/__tests__/__snapshots__/pagination.test.js.snap @@ -20,11 +20,6 @@ exports[`Pagination Component should handle updating paging for redux state: dis Array [ Array [ Array [ - Object { - "offset": 10, - "type": "SET_QUERY_RHSM_offset", - "viewId": "pagination", - }, Object { "offset": 10, "type": "SET_QUERY_RHSM_offset", @@ -33,22 +28,22 @@ Array [ Object { "limit": 10, "type": "SET_QUERY_RHSM_limit", + "viewId": "lorem", + }, + Object { + "offset": 10, + "type": "SET_QUERY_RHSM_offset", "viewId": "pagination", }, Object { "limit": 10, "type": "SET_QUERY_RHSM_limit", - "viewId": "lorem", + "viewId": "pagination", }, ], ], Array [ Array [ - Object { - "offset": 0, - "type": "SET_QUERY_RHSM_offset", - "viewId": "pagination", - }, Object { "offset": 0, "type": "SET_QUERY_RHSM_offset", @@ -57,12 +52,17 @@ Array [ Object { "limit": 20, "type": "SET_QUERY_RHSM_limit", + "viewId": "lorem", + }, + Object { + "offset": 0, + "type": "SET_QUERY_RHSM_offset", "viewId": "pagination", }, Object { "limit": 20, "type": "SET_QUERY_RHSM_limit", - "viewId": "lorem", + "viewId": "pagination", }, ], ], @@ -136,11 +136,6 @@ exports[`Pagination Component should reset offset/pages when per-page limit is a Array [ Array [ Array [ - Object { - "offset": 0, - "type": "SET_QUERY_RHSM_offset", - "viewId": "pagination", - }, Object { "offset": 0, "type": "SET_QUERY_RHSM_offset", @@ -149,12 +144,17 @@ Array [ Object { "limit": 100, "type": "SET_QUERY_RHSM_limit", + "viewId": "lorem", + }, + Object { + "offset": 0, + "type": "SET_QUERY_RHSM_offset", "viewId": "pagination", }, Object { "limit": 100, "type": "SET_QUERY_RHSM_limit", - "viewId": "lorem", + "viewId": "pagination", }, ], ], diff --git a/src/components/pagination/__tests__/__snapshots__/paginationHelpers.test.js.snap b/src/components/pagination/__tests__/__snapshots__/paginationHelpers.test.js.snap new file mode 100644 index 000000000..aa9e173b2 --- /dev/null +++ b/src/components/pagination/__tests__/__snapshots__/paginationHelpers.test.js.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PaginationHelpers should have specific functions: paginationHelpers 1`] = ` +Object { + "resetPage": [Function], +} +`; + +exports[`PaginationHelpers should reset offset/pages through redux state: dispatch offset 1`] = ` +Array [ + Array [ + Array [ + Object { + "offset": 0, + "type": "SET_QUERY_RHSM_offset", + "viewId": "lorem", + }, + Object { + "offset": 0, + "type": "SET_QUERY_RHSM_offset", + "viewId": "ipsum", + }, + ], + ], +] +`; diff --git a/src/components/pagination/__tests__/paginationHelpers.test.js b/src/components/pagination/__tests__/paginationHelpers.test.js new file mode 100644 index 000000000..52bcbd512 --- /dev/null +++ b/src/components/pagination/__tests__/paginationHelpers.test.js @@ -0,0 +1,23 @@ +import { paginationHelpers } from '../paginationHelpers'; +import { store } from '../../../redux'; + +describe('PaginationHelpers', () => { + let mockDispatch; + + beforeEach(() => { + mockDispatch = jest.spyOn(store, 'dispatch').mockImplementation((type, data) => ({ type, data })); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should have specific functions', () => { + expect(paginationHelpers).toMatchSnapshot('paginationHelpers'); + }); + + it('should reset offset/pages through redux state', () => { + paginationHelpers.resetPage({ productId: 'lorem', viewId: 'ipsum' }); + expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch offset'); + }); +}); diff --git a/src/components/pagination/pagination.js b/src/components/pagination/pagination.js index 8b2758d7c..3db4437f3 100644 --- a/src/components/pagination/pagination.js +++ b/src/components/pagination/pagination.js @@ -4,7 +4,14 @@ import { Pagination as PfPagination } from '@patternfly/react-core'; import { connect, reduxTypes, store } from '../../redux'; import { RHSM_API_QUERY_TYPES } from '../../types/rhsmApiTypes'; -// ToDo: Apply locale/translation to the PF Pagination "titles" prop. +/** + * ToDo: Apply locale/translation to the PF Pagination "titles" prop. + */ +/** + * ToDo: Review removing pagination state updates to both the productId and global viewId + * Applying paging to the global id of viewId is an extra, it's unnecessary and + * was meant to originally help facilitate a refresh across components. + */ /** * Contained pagination. * @@ -25,29 +32,34 @@ class Pagination extends React.Component { const { offsetDefault, perPageDefault, productId, query, viewId } = this.props; const updatedPerPage = query?.[RHSM_API_QUERY_TYPES.LIMIT] || perPageDefault; const offset = updatedPerPage * (page - 1) || offsetDefault; - - store.dispatch([ - { - type: reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.OFFSET], - viewId, - [RHSM_API_QUERY_TYPES.OFFSET]: offset - }, + const updatedActions = [ { type: reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.OFFSET], viewId: productId, [RHSM_API_QUERY_TYPES.OFFSET]: offset }, - { - type: reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.LIMIT], - viewId, - [RHSM_API_QUERY_TYPES.LIMIT]: updatedPerPage - }, { type: reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.LIMIT], viewId: productId, [RHSM_API_QUERY_TYPES.LIMIT]: updatedPerPage } - ]); + ]; + + if (viewId && productId !== viewId) { + updatedActions.push({ + type: reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.OFFSET], + viewId, + [RHSM_API_QUERY_TYPES.OFFSET]: offset + }); + + updatedActions.push({ + type: reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.LIMIT], + viewId, + [RHSM_API_QUERY_TYPES.LIMIT]: updatedPerPage + }); + } + + store.dispatch(updatedActions); }; /** @@ -59,29 +71,34 @@ class Pagination extends React.Component { */ onPerPage = ({ perPage }) => { const { offsetDefault, productId, viewId } = this.props; - - store.dispatch([ - { - type: reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.OFFSET], - viewId, - [RHSM_API_QUERY_TYPES.OFFSET]: offsetDefault - }, + const updatedActions = [ { type: reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.OFFSET], viewId: productId, [RHSM_API_QUERY_TYPES.OFFSET]: offsetDefault }, - { - type: reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.LIMIT], - viewId, - [RHSM_API_QUERY_TYPES.LIMIT]: perPage - }, { type: reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.LIMIT], viewId: productId, [RHSM_API_QUERY_TYPES.LIMIT]: perPage } - ]); + ]; + + if (viewId && productId !== viewId) { + updatedActions.push({ + type: reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.OFFSET], + viewId, + [RHSM_API_QUERY_TYPES.OFFSET]: offsetDefault + }); + + updatedActions.push({ + type: reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.LIMIT], + viewId, + [RHSM_API_QUERY_TYPES.LIMIT]: perPage + }); + } + + store.dispatch(updatedActions); }; // ToDo: Consider using the PfPagination "offset" prop diff --git a/src/components/pagination/paginationHelpers.js b/src/components/pagination/paginationHelpers.js new file mode 100644 index 000000000..3ce22a64b --- /dev/null +++ b/src/components/pagination/paginationHelpers.js @@ -0,0 +1,31 @@ +import { reduxTypes } from '../../redux/types'; +import { RHSM_API_QUERY_TYPES } from '../../types/rhsmApiTypes'; +import { store } from '../../redux'; + +const resetPage = ({ offsetDefault = 0, productId, viewId }) => { + const updatedActions = []; + + if (productId) { + updatedActions.push({ + type: reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.OFFSET], + viewId: productId, + [RHSM_API_QUERY_TYPES.OFFSET]: offsetDefault + }); + } + + if (viewId && productId !== viewId) { + updatedActions.push({ + type: reduxTypes.query.SET_QUERY_RHSM_TYPES[RHSM_API_QUERY_TYPES.OFFSET], + viewId, + [RHSM_API_QUERY_TYPES.OFFSET]: offsetDefault + }); + } + + store.dispatch(updatedActions); +}; + +const paginationHelpers = { + resetPage +}; + +export { paginationHelpers as default, paginationHelpers, resetPage }; diff --git a/src/components/rhelView/__tests__/__snapshots__/rhelView.test.js.snap b/src/components/rhelView/__tests__/__snapshots__/rhelView.test.js.snap index ff0591b0f..3dba0d4b0 100644 --- a/src/components/rhelView/__tests__/__snapshots__/rhelView.test.js.snap +++ b/src/components/rhelView/__tests__/__snapshots__/rhelView.test.js.snap @@ -21,6 +21,7 @@ exports[`RhelView Component should display an alternate graph on query-string up }, ] } + productId="lorem ipsum" query={ Object { "granularity": "daily", @@ -140,6 +141,7 @@ exports[`RhelView Component should have a fallback title: title 1`] = ` }, ] } + productId="lorem ipsum" query={ Object { "granularity": "daily", @@ -432,6 +434,7 @@ exports[`RhelView Component should render a non-connected component: non-connect }, ] } + productId="lorem ipsum" query={ Object { "granularity": "daily", diff --git a/src/components/rhelView/rhelView.js b/src/components/rhelView/rhelView.js index 9ad78eab2..4277aec31 100644 --- a/src/components/rhelView/rhelView.js +++ b/src/components/rhelView/rhelView.js @@ -53,7 +53,12 @@ class RhelView extends React.Component { {t(`curiosity-view.title`, { appName: helpers.UI_DISPLAY_NAME, context: viewId })} - + {(isC3 && ( diff --git a/src/components/toolbar/__tests__/__snapshots__/toolbar.test.js.snap b/src/components/toolbar/__tests__/__snapshots__/toolbar.test.js.snap index 75144e0c0..a6efb2f39 100644 --- a/src/components/toolbar/__tests__/__snapshots__/toolbar.test.js.snap +++ b/src/components/toolbar/__tests__/__snapshots__/toolbar.test.js.snap @@ -1,5 +1,61 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Toolbar Component should dispatch filters towards redux state with paging resets: NO paging state 1`] = ` +Object { + "store": Array [ + Array [ + Array [ + Object { + "lorem": "ipsum", + "type": "lorem ipsum", + "viewId": "toolbar", + }, + ], + ], + ], +} +`; + +exports[`Toolbar Component should dispatch filters towards redux state with paging resets: WITH paging state, NO product id 1`] = ` +Object { + "resetPage": Array [ + Object { + "productId": null, + "viewId": "toolbar", + }, + ], + "store": Array [ + Array [ + Object { + "lorem": "ipsum", + "type": "lorem ipsum", + "viewId": "toolbar", + }, + ], + ], +} +`; + +exports[`Toolbar Component should dispatch filters towards redux state with paging resets: WITH paging state, WITH product id 1`] = ` +Object { + "resetPage": Array [ + Object { + "productId": "lorem", + "viewId": "toolbar", + }, + ], + "store": Array [ + Array [ + Object { + "lorem": "ipsum", + "type": "lorem ipsum", + "viewId": "toolbar", + }, + ], + ], +} +`; + exports[`Toolbar Component should handle adding and clearing filters from redux state: dispatch filter 1`] = ` Array [ Array [ @@ -27,6 +83,7 @@ Array [ "type": "SET_QUERY_RHSM_sla", }, ], + true, ], Array [ Array [ @@ -45,6 +102,7 @@ Array [ "type": "SET_QUERY_RHSM_sla", }, ], + true, ], Array [ Array [ @@ -63,6 +121,7 @@ Array [ "type": "SET_QUERY_CLEAR", }, ], + true, ], Array [ Object { @@ -89,6 +148,7 @@ Array [ "type": "SET_QUERY_RHSM_usage", }, ], + true, ], Array [ Array [ @@ -107,6 +167,7 @@ Array [ "type": "SET_QUERY_RHSM_usage", }, ], + true, ], Array [ Array [ @@ -125,6 +186,7 @@ Array [ "type": "SET_QUERY_CLEAR", }, ], + true, ], Array [ Array [ @@ -144,6 +206,7 @@ Array [ "type": "SET_QUERY_CLEAR", }, ], + true, ], ] `; @@ -175,6 +238,7 @@ Array [ "type": "SET_QUERY_RHSM_sla", }, ], + true, ], Array [ Array [ @@ -193,6 +257,7 @@ Array [ "type": "SET_QUERY_RHSM_sla", }, ], + true, ], Array [ Array [ @@ -211,6 +276,7 @@ Array [ "type": "SET_QUERY_CLEAR", }, ], + true, ], Array [ Object { @@ -237,6 +303,7 @@ Array [ "type": "SET_QUERY_RHSM_usage", }, ], + true, ], Array [ Array [ @@ -255,6 +322,7 @@ Array [ "type": "SET_QUERY_RHSM_usage", }, ], + true, ], Array [ Array [ @@ -273,6 +341,7 @@ Array [ "type": "SET_QUERY_CLEAR", }, ], + true, ], Array [ Array [ @@ -292,6 +361,7 @@ Array [ "type": "SET_QUERY_CLEAR", }, ], + true, ], Array [ Object { @@ -318,6 +388,7 @@ Array [ "type": "SET_QUERY_RHSM_sla", }, ], + true, ], Array [ Array [ @@ -336,6 +407,7 @@ Array [ "type": "SET_QUERY_RHSM_sla", }, ], + true, ], Array [ Array [ @@ -360,6 +432,7 @@ Array [ "type": "SET_FILTER_TYPE", }, ], + true, ], Array [ Object { @@ -386,6 +459,7 @@ Array [ "type": "SET_QUERY_RHSM_usage", }, ], + true, ], Array [ Array [ @@ -404,6 +478,7 @@ Array [ "type": "SET_QUERY_RHSM_usage", }, ], + true, ], Array [ Array [ @@ -428,6 +503,7 @@ Array [ "type": "SET_FILTER_TYPE", }, ], + true, ], Array [ Array [ @@ -453,6 +529,7 @@ Array [ "type": "SET_FILTER_TYPE", }, ], + true, ], ] `; diff --git a/src/components/toolbar/__tests__/toolbar.test.js b/src/components/toolbar/__tests__/toolbar.test.js index c79295484..6fef275af 100644 --- a/src/components/toolbar/__tests__/toolbar.test.js +++ b/src/components/toolbar/__tests__/toolbar.test.js @@ -1,14 +1,16 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; +import { store } from '../../../redux'; import { Toolbar } from '../toolbar'; import { toolbarTypes } from '../toolbarTypes'; import { RHSM_API_QUERY_TYPES } from '../../../types/rhsmApiTypes'; +import { paginationHelpers } from '../../pagination/paginationHelpers'; describe('Toolbar Component', () => { - let mockDispatch; + let mockSetDispatch; beforeEach(() => { - mockDispatch = jest.spyOn(Toolbar.prototype, 'setDispatch').mockImplementation((type, data) => ({ type, data })); + mockSetDispatch = jest.spyOn(Toolbar.prototype, 'setDispatch').mockImplementation((type, data) => ({ type, data })); }); afterEach(() => { @@ -87,10 +89,56 @@ describe('Toolbar Component', () => { }; filterMethods(); - expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch filter'); + expect(mockSetDispatch.mock.calls).toMatchSnapshot('dispatch filter'); component.setProps({ currentFilter: null, hardFilterReset: true }); filterMethods(); - expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch filter, hard reset'); + expect(mockSetDispatch.mock.calls).toMatchSnapshot('dispatch filter, hard reset'); + }); + + it('should dispatch filters towards redux state with paging resets', () => { + // Restore the original setDispatch functionality for testing + mockSetDispatch.mockRestore(); + const mockStoreDispatch = jest.spyOn(store, 'dispatch').mockImplementation((type, data) => ({ type, data })); + const mockResetPage = jest + .spyOn(paginationHelpers, 'resetPage') + .mockImplementation((type, data) => ({ type, data })); + + const props = {}; + const component = shallow(); + const componentInstance = component.instance(); + + componentInstance.setDispatch({ + type: 'lorem ipsum', + data: { lorem: 'ipsum' } + }); + expect({ store: mockStoreDispatch.mock.calls }).toMatchSnapshot('NO paging state'); + + componentInstance.setDispatch( + { + type: 'lorem ipsum', + data: { lorem: 'ipsum' } + }, + true + ); + expect({ + store: mockStoreDispatch.mock.calls[mockStoreDispatch.mock.calls.length - 1], + resetPage: mockResetPage.mock.calls[mockResetPage.mock.calls.length - 1] + }).toMatchSnapshot('WITH paging state, NO product id'); + + component.setProps({ + productId: 'lorem' + }); + componentInstance.setDispatch( + { + type: 'lorem ipsum', + data: { lorem: 'ipsum' } + }, + true + ); + expect({ + store: mockStoreDispatch.mock.calls[mockStoreDispatch.mock.calls.length - 1], + resetPage: mockResetPage.mock.calls[mockResetPage.mock.calls.length - 1] + }).toMatchSnapshot('WITH paging state, WITH product id'); }); }); diff --git a/src/components/toolbar/toolbar.js b/src/components/toolbar/toolbar.js index c990f021b..e766833ba 100644 --- a/src/components/toolbar/toolbar.js +++ b/src/components/toolbar/toolbar.js @@ -13,6 +13,7 @@ import { Select } from '../form/select'; import { connect, reduxTypes, store } from '../../redux'; import { RHSM_API_QUERY_TYPES } from '../../types/rhsmApiTypes'; import { toolbarTypes } from './toolbarTypes'; +import { paginationHelpers } from '../pagination/paginationHelpers'; import { helpers } from '../../common'; import { translate } from '../i18n/i18n'; @@ -50,7 +51,7 @@ class Toolbar extends React.Component { dispatchActions.push({ type: reduxTypes.toolbar.SET_FILTER_TYPE, data: { currentFilter: null } }); } - this.setDispatch(dispatchActions); + this.setDispatch(dispatchActions, true); }; /** @@ -89,7 +90,7 @@ class Toolbar extends React.Component { dispatchActions.push({ type: reduxTypes.toolbar.SET_FILTER_TYPE, data: { currentFilter: updatedCurrentFilter } }); } - this.setDispatch(dispatchActions); + this.setDispatch(dispatchActions, true); }; /** @@ -115,31 +116,39 @@ class Toolbar extends React.Component { const { value } = event; const updatedActiveFilters = new Set(activeFilters).add(field); - this.setDispatch([ - { - type: reduxTypes.toolbar.SET_ACTIVE_FILTERS, - data: { activeFilters: updatedActiveFilters } - }, - { - type: reduxTypes.query.SET_QUERY_RHSM_TYPES[field], - data: { [field]: value } - } - ]); + this.setDispatch( + [ + { + type: reduxTypes.toolbar.SET_ACTIVE_FILTERS, + data: { activeFilters: updatedActiveFilters } + }, + { + type: reduxTypes.query.SET_QUERY_RHSM_TYPES[field], + data: { [field]: value } + } + ], + true + ); }; /** * Dispatch a Redux store type. * * @param {Array|object} actions + * @param {boolean} resetPage */ - setDispatch(actions) { - const { viewId } = this.props; + setDispatch(actions, resetPage = false) { + const { productId, viewId } = this.props; const updatedActions = ((Array.isArray(actions) && actions) || [actions]).map(({ type, data }) => ({ type, viewId, ...data })); + if (resetPage) { + paginationHelpers.resetPage({ productId, viewId }); + } + store.dispatch(updatedActions); } @@ -279,6 +288,7 @@ Toolbar.propTypes = { ), hardFilterReset: PropTypes.bool, isDisabled: PropTypes.bool, + productId: PropTypes.string, t: PropTypes.func, viewId: PropTypes.string }; @@ -306,6 +316,7 @@ Toolbar.defaultProps = { ], hardFilterReset: false, isDisabled: helpers.UI_DISABLED_TOOLBAR, + productId: null, t: translate, viewId: 'toolbar' }; From fc803709894a79ccfaf6fb57f3d4727a2a42363e Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Wed, 9 Sep 2020 15:37:11 -0400 Subject: [PATCH 22/28] fix(guestsList): avoid boolean, remove nullish coalescing op (#413) --- src/components/guestsList/guestsList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/guestsList/guestsList.js b/src/components/guestsList/guestsList.js index 44c783fea..c718663d3 100644 --- a/src/components/guestsList/guestsList.js +++ b/src/components/guestsList/guestsList.js @@ -134,7 +134,7 @@ class GuestsList extends React.Component { // Include the table header let updatedHeight = (numberOfGuests + 1) * 42; - updatedHeight = (updatedHeight < 275 && updatedHeight) ?? 275; + updatedHeight = (updatedHeight < 275 && updatedHeight) || 275; return (
From 8f410d44ca61128636fc8e3931f6da336cfe8684 Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Thu, 10 Sep 2020 14:12:19 -0400 Subject: [PATCH 23/28] chore(build): expand changelog displayed commits (#415) --- package.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 197d0731d..536e28e8e 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,18 @@ "standard-version": { "skip": { "tag": true - } + }, + "types": [{ + "type": "feat", "section": "Features" + },{ + "type": "fix", "section": "Bug Fixes" + },{ + "type": "refactor", "section": "Code Refactoring" + },{ + "type": "perf", "section": "Performance Improvements" + },{ + "type": "style", "section": "Styles" + }] }, "scripts": { "api:dev": "mock -p 5000 -w ./src/services", From 7e8d5ff073f7e045df87d2be772b8abd5e8325f4 Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Tue, 15 Sep 2020 13:09:29 -0400 Subject: [PATCH 24/28] fix(openshiftView,rhelView): issues/421 viewId to productLabel (#423) * openshiftView,rhelView, viewId to productLabel, avoid conflicts * c3graphCard, annotation for viewId to productLabel * graphCard,Tooltip,Legend, product,shortLabel to productLabel * pageHeader, viewId to productLabel --- .../__snapshots__/authentication.test.js.snap | 2 +- src/components/c3GraphCard/c3GraphCard.js | 4 +++ .../__tests__/graphCardChartLegend.test.js | 6 ++-- src/components/graphCard/graphCard.js | 19 ++++++----- .../graphCard/graphCardChartLegend.js | 26 ++++++++------ .../graphCard/graphCardChartTooltip.js | 14 ++++---- .../__tests__/__snapshots__/i18n.test.js.snap | 16 ++++----- .../__snapshots__/loader.test.js.snap | 2 +- .../__snapshots__/messageView.test.js.snap | 6 ++-- .../__snapshots__/openshiftView.test.js.snap | 28 +++++++-------- src/components/openshiftView/openshiftView.js | 34 +++++++++++++------ .../__snapshots__/pageHeader.test.js.snap | 6 ++-- .../__snapshots__/pageLayout.test.js.snap | 2 +- .../pageLayout/__tests__/pageHeader.test.js | 2 +- src/components/pageLayout/pageHeader.js | 20 +++++------ .../__snapshots__/rhelView.test.js.snap | 28 +++++++-------- src/components/rhelView/rhelView.js | 20 ++++++----- 17 files changed, 131 insertions(+), 104 deletions(-) diff --git a/src/components/authentication/__tests__/__snapshots__/authentication.test.js.snap b/src/components/authentication/__tests__/__snapshots__/authentication.test.js.snap index 33fa6a5aa..9bbf0fa36 100644 --- a/src/components/authentication/__tests__/__snapshots__/authentication.test.js.snap +++ b/src/components/authentication/__tests__/__snapshots__/authentication.test.js.snap @@ -67,8 +67,8 @@ exports[`Authentication Component should render a non-connected component error:
{ } ] }, - product: 'test' + productLabel: 'test' }; const component = shallow(); @@ -79,7 +79,7 @@ describe('GraphCardChartLegend Component', () => { legend: { 'test-dolorSit': true }, - product: 'test', + productLabel: 'test', viewId: 'test' }; @@ -119,7 +119,7 @@ describe('GraphCardChartLegend Component', () => { } ] }, - product: 'test' + productLabel: 'test' }; const component = shallow(); diff --git a/src/components/graphCard/graphCard.js b/src/components/graphCard/graphCard.js index a469132e3..a2e46ca2b 100644 --- a/src/components/graphCard/graphCard.js +++ b/src/components/graphCard/graphCard.js @@ -89,7 +89,7 @@ class GraphCard extends React.Component { * @returns {Node} */ renderChart() { - const { filterGraphData, graphData, selectOptionsType, productShortLabel, query, viewId } = this.props; + const { filterGraphData, graphData, selectOptionsType, productLabel, query, viewId } = this.props; const graphGranularity = this.getQueryGranularity(); const { selected } = graphCardTypes.getGranularityOptions(selectOptionsType); const updatedGranularity = graphGranularity || selected; @@ -149,10 +149,10 @@ class GraphCard extends React.Component { {...chartAreaProps} dataSets={filteredGraphData(graphData)} chartLegend={({ chart, datum }) => ( - + )} chartTooltip={({ datum }) => ( - + )} /> ); @@ -206,9 +206,10 @@ class GraphCard extends React.Component { /** * Prop types. * - * @type {{productId: string, pending: boolean, error: boolean, query: object, cardTitle: string, - * filterGraphData: Array, getGraphReportsCapacity: Function, productShortLabel: string, selectOptionsType: string, - * viewId: string, t: Function, children: Node, graphData: object, isDisabled: boolean}} + * @type {{productLabel: string, productId: string, pending: boolean, error: boolean, query: object, + * cardTitle: string, filterGraphData: Array, getGraphReportsCapacity: Function, + * selectOptionsType: string, viewId: string, t: Function, children: Node, graphData: object, + * isDisabled: boolean}} */ GraphCard.propTypes = { cardTitle: PropTypes.string, @@ -229,16 +230,16 @@ GraphCard.propTypes = { isDisabled: PropTypes.bool, pending: PropTypes.bool, productId: PropTypes.string.isRequired, + productLabel: PropTypes.string, selectOptionsType: PropTypes.oneOf(['default']), t: PropTypes.func, - productShortLabel: PropTypes.string, viewId: PropTypes.string }; /** * Default props. * - * @type {{getGraphReportsCapacity: Function, productShortLabel: string, selectOptionsType: string, + * @type {{getGraphReportsCapacity: Function, productLabel: string, selectOptionsType: string, * viewId: string, t: translate, children: null, pending: boolean, graphData: object, * isDisabled: boolean, error: boolean, cardTitle: null, filterGraphData: Array}} */ @@ -251,9 +252,9 @@ GraphCard.defaultProps = { graphData: {}, isDisabled: helpers.UI_DISABLED_GRAPH, pending: false, + productLabel: '', selectOptionsType: 'default', t: translate, - productShortLabel: '', viewId: 'graphCard' }; diff --git a/src/components/graphCard/graphCardChartLegend.js b/src/components/graphCard/graphCardChartLegend.js index b2772673b..7879181bb 100644 --- a/src/components/graphCard/graphCardChartLegend.js +++ b/src/components/graphCard/graphCardChartLegend.js @@ -133,7 +133,7 @@ class GraphCardChartLegend extends React.Component { * @returns {Node} */ render() { - const { datum, product, t } = this.props; + const { datum, productLabel, t } = this.props; return ( @@ -143,16 +143,22 @@ class GraphCardChartLegend extends React.Component { const labelContent = (isThreshold && - t([`curiosity-graph.${id}Label`, `curiosity-graph.thresholdLabel`], { product, context: product })) || - t([`curiosity-graph.${id}Label`, `curiosity-graph.noLabel`], { product, context: product }); + t([`curiosity-graph.${id}Label`, `curiosity-graph.thresholdLabel`], { + product: productLabel, + context: productLabel + })) || + t([`curiosity-graph.${id}Label`, `curiosity-graph.noLabel`], { + product: productLabel, + context: productLabel + }); const tooltipContent = (isThreshold && t([`curiosity-graph.${id}LegendTooltip`, `curiosity-graph.thresholdLegendTooltip`], { - product, - context: product + product: productLabel, + context: productLabel })) || - t(`curiosity-graph.${id}LegendTooltip`, { product, context: product }); + t(`curiosity-graph.${id}LegendTooltip`, { product: productLabel, context: productLabel }); return this.renderLegendItem({ chartId: id, @@ -171,7 +177,7 @@ class GraphCardChartLegend extends React.Component { /** * Prop types. * - * @type {{datum, product: string, t: Function, legend, chart}} + * @type {{datum, productLabel: string, t: Function, legend: object, chart: object}} */ GraphCardChartLegend.propTypes = { chart: PropTypes.shape({ @@ -189,7 +195,7 @@ GraphCardChartLegend.propTypes = { ) }), legend: PropTypes.objectOf(PropTypes.bool), - product: PropTypes.string, + productLabel: PropTypes.string, t: PropTypes.func, viewId: PropTypes.string }; @@ -197,7 +203,7 @@ GraphCardChartLegend.propTypes = { /** * Default props. * - * @type {{datum: {dataSets: Array}, product: string, viewId: string, t: translate, legend: object, + * @type {{datum: {dataSets: Array}, productLabel: string, viewId: string, t: translate, legend: object, * chart: {hide: Function, toggle: Function, isToggled: Function}}} */ GraphCardChartLegend.defaultProps = { @@ -210,7 +216,7 @@ GraphCardChartLegend.defaultProps = { dataSets: [] }, legend: {}, - product: '', + productLabel: '', t: translate, viewId: 'graphCardLegend' }; diff --git a/src/components/graphCard/graphCardChartTooltip.js b/src/components/graphCard/graphCardChartTooltip.js index 3a739035c..9b041b87a 100644 --- a/src/components/graphCard/graphCardChartTooltip.js +++ b/src/components/graphCard/graphCardChartTooltip.js @@ -9,11 +9,11 @@ import { translate } from '../i18n/i18n'; * @param {object} props * @param {object} props.datum * @param {string} props.granularity - * @param {string} props.product + * @param {string} props.productLabel * @param {Function} props.t * @returns {Node} */ -const GraphCardChartTooltip = ({ datum, granularity, product, t }) => { +const GraphCardChartTooltip = ({ datum, granularity, productLabel, t }) => { let header = null; const data = []; const { itemsByKey = {} } = datum || {}; @@ -41,7 +41,7 @@ const GraphCardChartTooltip = ({ datum, granularity, product, t }) => { const dataFactsValue = (itemsByKey[key]?.data.hasData === false && t('curiosity-graph.noDataLabel')) || itemsByKey[key]?.data.y || 0; - tempDataFacet.label = t(`curiosity-graph.${key}Label`, { product }); + tempDataFacet.label = t(`curiosity-graph.${key}Label`, { product: productLabel }); tempDataFacet.value = dataFactsValue; } @@ -88,7 +88,7 @@ const GraphCardChartTooltip = ({ datum, granularity, product, t }) => { /** * Prop types. * - * @type {{datum, product: string, t: Function, granularity: string}} + * @type {{datum, productLabel: string, t: Function, granularity: string}} */ GraphCardChartTooltip.propTypes = { datum: PropTypes.shape({ @@ -105,18 +105,18 @@ GraphCardChartTooltip.propTypes = { ) }), granularity: PropTypes.string.isRequired, - product: PropTypes.string, + productLabel: PropTypes.string, t: PropTypes.func }; /** * Default props. * - * @type {{datum: object, product: string, t: translate}} + * @type {{datum: object, productLabel: string, t: translate}} */ GraphCardChartTooltip.defaultProps = { datum: {}, - product: '', + productLabel: '', t: translate }; diff --git a/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap b/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap index a790d4367..2d0761e75 100644 --- a/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap +++ b/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap @@ -107,19 +107,19 @@ Array [ "keys": Array [ Object { "key": "", - "match": "t([\`curiosity-graph.\${id}Label\`, \`curiosity-graph.thresholdLabel\`], { product, context: product })", + "match": "t([\`curiosity-graph.\${id}Label\`, \`curiosity-graph.thresholdLabel\`], { product: productLabel, context: productLabel })", }, Object { "key": "", - "match": "t([\`curiosity-graph.\${id}Label\`, \`curiosity-graph.noLabel\`], { product, context: product })", + "match": "t([\`curiosity-graph.\${id}Label\`, \`curiosity-graph.noLabel\`], { product: productLabel, context: productLabel })", }, Object { "key": "", - "match": "t([\`curiosity-graph.\${id}LegendTooltip\`, \`curiosity-graph.thresholdLegendTooltip\`], { product, context: product })", + "match": "t([\`curiosity-graph.\${id}LegendTooltip\`, \`curiosity-graph.thresholdLegendTooltip\`], { product: productLabel, context: productLabel })", }, Object { "key": "", - "match": "t(\`curiosity-graph.\${id}LegendTooltip\`, { product, context: product })", + "match": "t(\`curiosity-graph.\${id}LegendTooltip\`, { product: productLabel, context: productLabel })", }, ], }, @@ -144,7 +144,7 @@ Array [ }, Object { "key": "", - "match": "t(\`curiosity-graph.\${key}Label\`, { product })", + "match": "t(\`curiosity-graph.\${key}Label\`, { product: productLabel })", }, Object { "key": "curiosity-graph.tooltipSummary", @@ -199,7 +199,7 @@ Array [ }, Object { "key": "curiosity-view.title", - "match": "t(\`curiosity-view.title\`, { appName: helpers.UI_DISPLAY_NAME, context: viewId })", + "match": "t(\`curiosity-view.title\`, { appName: helpers.UI_DISPLAY_NAME, context: productLabel })", }, Object { "key": "curiosity-graph.cardHeading", @@ -297,7 +297,7 @@ Array [ "keys": Array [ Object { "key": "curiosity-view.subtitle", - "match": "t(\`curiosity-view.subtitle\`, { appName: helpers.UI_DISPLAY_NAME, context: viewId }, [