From 8b9a5607c5bafd6a24900b68250ce121cf00fe75 Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Tue, 16 Nov 2021 19:02:07 -0500 Subject: [PATCH] feat(productView,inventoryList): ent-4367 rhosak inventory (#831) * config, product.rhosak, inventory filters * locale, instances inventory strings * inventoryList.deprecated, hosts based inventory to deprecated * inventoryList, instances based inventory * productView, instances inventory for rhosak * styling, minor margin adjustment --- public/locales/en-US.json | 9 + .../__tests__/__snapshots__/i18n.test.js.snap | 41 ++ .../inventoryList.deprecated.test.js.snap | 561 ++++++++++++++++++ .../__snapshots__/inventoryList.test.js.snap | 342 ++++++++++- .../inventoryList.deprecated.test.js | 152 +++++ .../__tests__/inventoryList.test.js | 81 ++- .../inventoryList/inventoryList.deprecated.js | 379 ++++++++++++ src/components/inventoryList/inventoryList.js | 93 +-- .../inventoryList/inventoryListHelpers.js | 23 +- .../__snapshots__/productView.test.js.snap | 61 ++ .../productView/__tests__/productView.test.js | 15 + src/components/productView/productView.js | 57 +- .../product.openshiftContainer.test.js.snap | 12 +- .../product.openshiftDedicated.test.js.snap | 4 +- .../product.openshiftMetrics.test.js.snap | 4 +- .../__snapshots__/product.rhel.test.js.snap | 12 +- .../__snapshots__/product.rhosak.test.js.snap | 165 ++++++ .../product.satellite.test.js.snap | 4 +- src/config/__tests__/product.rhosak.test.js | 42 ++ src/config/product.rhosak.js | 87 ++- src/styles/_page-layout.scss | 8 + src/styles/_usage-graph.scss | 3 + tests/__snapshots__/code.test.js.snap | 4 +- tests/__snapshots__/dist.test.js.snap | 10 +- 24 files changed, 2077 insertions(+), 92 deletions(-) create mode 100644 src/components/inventoryList/__tests__/__snapshots__/inventoryList.deprecated.test.js.snap create mode 100644 src/components/inventoryList/__tests__/inventoryList.deprecated.test.js create mode 100644 src/components/inventoryList/inventoryList.deprecated.js diff --git a/public/locales/en-US.json b/public/locales/en-US.json index 7014b94c4..1a4852221 100644 --- a/public/locales/en-US.json +++ b/public/locales/en-US.json @@ -115,6 +115,7 @@ "tabHosts_noInstances_OpenShift-dedicated-metrics": "Current instances", "tabHosts_OpenShift-dedicated-metrics": "{{count}} instance", "tabHosts_OpenShift-dedicated-metrics_plural": "{{count}} instances", + "tabInstances": "Current monthly instances", "tabSubscriptions": "Current subscriptions", "tab_disabled": "The {{tabName}} display is currently disabled.", "tableAriaLabel": "{{appName}} systems inventory table.", @@ -142,9 +143,11 @@ "header_cores_OpenShift-dedicated-metrics": "Cores", "header_coreHours": "Core hours", "header_displayName": "Name", + "header_display_name": "Name", "header_guestsDisplayName": "Guest name", "header_hardwareType": "Type", "header_instanceHours": "Instance hours", + "header_Instance-hours": "Monthly instance hours", "header_measurementType": "Type", "header_inventoryId": "UUID", "header_sockets": "Subscribed sockets", @@ -152,13 +155,19 @@ "header_sockets_OpenShift-metrics": "Sockets", "header_sockets_OpenShift-dedicated-metrics": "Sockets", "header_lastSeen": "Last seen", + "header_last_seen": "Last seen", "header_nextEventDate": "Next renewal", "header_productName": "Product", "header_quantity": "Quantity", "header_serviceLevel": "Service level", + "header_Storage-gibibytes": "Monthly data storage", "header_subscriptions": "Sockets", "header_subscriptions_cores": "Cores", "header_subscriptions_sockets": "Sockets", + "header_Transfer-gibibytes": "Monthly data transfer", + "measurement_Instance-hours": "{{total}} GiB hours", + "measurement_Storage-gibibytes": "{{total}} GiB", + "measurement_Transfer-gibibytes": "{{total}} GiB", "measurementType": "{{context}}", "measurementType_cloud": "Public cloud", "measurementType_alibaba": "Public cloud", diff --git a/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap b/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap index 828c9ecad..229f3d3c2 100644 --- a/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap +++ b/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap @@ -161,6 +161,15 @@ Array [ }, ], }, + Object { + "file": "./src/components/inventoryList/inventoryList.deprecated.js", + "keys": Array [ + Object { + "key": "curiosity-inventory.tab", + "match": "t('curiosity-inventory.tab', { context: 'disabled' })", + }, + ], + }, Object { "file": "./src/components/inventoryList/inventoryList.js", "keys": Array [ @@ -306,6 +315,10 @@ Array [ "key": "curiosity-inventory.tabHosts", "match": "t('curiosity-inventory.tabHosts', { context: ['noInstances', productId] })", }, + Object { + "key": "curiosity-inventory.tabInstances", + "match": "t('curiosity-inventory.tabInstances', { context: ['noInstances', productId] })", + }, Object { "key": "curiosity-inventory.tabSubscriptions", "match": "t('curiosity-inventory.tabSubscriptions', { context: productId })", @@ -619,6 +632,18 @@ Array [ "key": "curiosity-graph.label_axisX", "match": "translate('curiosity-graph.label_axisX', { context: GRANULARITY_TYPES.DAILY })", }, + Object { + "key": "curiosity-inventory.measurement", + "match": "translate('curiosity-inventory.measurement', { context: RHSM_API_PATH_METRIC_TYPES.TRANSFER_GIBIBYTES, total: helpers.numberDisplay(total?.value)", + }, + Object { + "key": "curiosity-inventory.measurement", + "match": "translate('curiosity-inventory.measurement', { context: RHSM_API_PATH_METRIC_TYPES.STORAGE_GIBIBYTES, total: helpers.numberDisplay(total?.value)", + }, + Object { + "key": "curiosity-inventory.measurement", + "match": "translate('curiosity-inventory.measurement', { context: RHSM_API_PATH_METRIC_TYPES.INSTANCE_HOURS, total: helpers.numberDisplay(total?.value)", + }, ], }, Object { @@ -672,6 +697,10 @@ Array [ "file": "./src/components/graphCard/graphCardChartTooltip.js", "key": "curiosity-graph.label", }, + Object { + "file": "./src/components/inventoryList/inventoryList.deprecated.js", + "key": "curiosity-inventory.tab", + }, Object { "file": "./src/components/inventoryList/inventoryList.js", "key": "curiosity-inventory.tab", @@ -728,6 +757,18 @@ Array [ "file": "./src/config/product.rhosak.js", "key": "curiosity-graph.label_axisX", }, + Object { + "file": "./src/config/product.rhosak.js", + "key": "curiosity-inventory.measurement", + }, + Object { + "file": "./src/config/product.rhosak.js", + "key": "curiosity-inventory.measurement", + }, + Object { + "file": "./src/config/product.rhosak.js", + "key": "curiosity-inventory.measurement", + }, Object { "file": "./src/config/product.satellite.js", "key": "curiosity-inventory.label", diff --git a/src/components/inventoryList/__tests__/__snapshots__/inventoryList.deprecated.test.js.snap b/src/components/inventoryList/__tests__/__snapshots__/inventoryList.deprecated.test.js.snap new file mode 100644 index 000000000..e3616cc0b --- /dev/null +++ b/src/components/inventoryList/__tests__/__snapshots__/inventoryList.deprecated.test.js.snap @@ -0,0 +1,561 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InventoryList Component should handle expandable guests data: number of guests 1`] = ` + +`; + +exports[`InventoryList Component should handle expandable guests data: number of guests, and id 1`] = ` +
, + }, + ] + } + summary={null} + t={[Function]} + variant="compact" +/> +`; + +exports[`InventoryList Component should handle expandable guests data: number of guests, id, and NO expandable guests display 1`] = ` +
+`; + +exports[`InventoryList Component should handle updating paging through redux state: dispatch onPage 1`] = ` +Array [ + Array [ + Array [ + Object { + "offset": 10, + "type": "SET_QUERY_RHSM_HOSTS_INVENTORY_offset", + "viewId": "lorem", + }, + Object { + "limit": 10, + "type": "SET_QUERY_RHSM_HOSTS_INVENTORY_limit", + "viewId": "lorem", + }, + ], + ], + Array [ + Array [ + Object { + "offset": 20, + "type": "SET_QUERY_RHSM_HOSTS_INVENTORY_offset", + "viewId": "lorem", + }, + Object { + "limit": 10, + "type": "SET_QUERY_RHSM_HOSTS_INVENTORY_limit", + "viewId": "lorem", + }, + ], + ], + Array [ + Array [ + Object { + "offset": 0, + "type": "SET_QUERY_RHSM_HOSTS_INVENTORY_offset", + "viewId": "lorem", + }, + Object { + "limit": 20, + "type": "SET_QUERY_RHSM_HOSTS_INVENTORY_limit", + "viewId": "lorem", + }, + ], + ], + Array [ + Array [ + Object { + "offset": 0, + "type": "SET_QUERY_RHSM_HOSTS_INVENTORY_offset", + "viewId": "lorem", + }, + Object { + "limit": 50, + "type": "SET_QUERY_RHSM_HOSTS_INVENTORY_limit", + "viewId": "lorem", + }, + ], + ], +] +`; + +exports[`InventoryList Component should handle updating sorting through redux state: dispatch filter 1`] = ` +Array [ + Array [ + Array [ + Object { + "dir": "asc", + "type": "SET_QUERY_RHSM_HOSTS_INVENTORY_dir", + "viewId": "lorem", + }, + Object { + "sort": "sockets", + "type": "SET_QUERY_RHSM_HOSTS_INVENTORY_sort", + "viewId": "lorem", + }, + ], + ], + Array [ + Array [ + Object { + "dir": "desc", + "type": "SET_QUERY_RHSM_HOSTS_INVENTORY_dir", + "viewId": "lorem", + }, + Object { + "sort": "sockets", + "type": "SET_QUERY_RHSM_HOSTS_INVENTORY_sort", + "viewId": "lorem", + }, + ], + ], + Array [ + Array [ + Object { + "dir": "asc", + "type": "SET_QUERY_RHSM_HOSTS_INVENTORY_dir", + "viewId": "lorem", + }, + Object { + "sort": "sockets", + "type": "SET_QUERY_RHSM_HOSTS_INVENTORY_sort", + "viewId": "lorem", + }, + ], + ], +] +`; + +exports[`InventoryList Component should handle variations in data: filtered data 1`] = ` + + + + + + + + + + + + + +
+
+ + + + + + + + + + + +`; + +exports[`InventoryList Component should handle variations in data: variable data 1`] = ` + + + + + + + + + + + + + +
+
+ + + + + + + + + + + +`; + +exports[`InventoryList Component should render a non-connected component: non-connected 1`] = ` + + + + + + + + + + + + + +
+
+ + + + + + + + + + + +`; + +exports[`InventoryList Component should return an empty render when disabled: disabled component 1`] = ` + + + + t(curiosity-inventory.tab, {"context":"disabled"}) + + + +`; diff --git a/src/components/inventoryList/__tests__/__snapshots__/inventoryList.test.js.snap b/src/components/inventoryList/__tests__/__snapshots__/inventoryList.test.js.snap index e3616cc0b..7c38ccb92 100644 --- a/src/components/inventoryList/__tests__/__snapshots__/inventoryList.test.js.snap +++ b/src/components/inventoryList/__tests__/__snapshots__/inventoryList.test.js.snap @@ -107,6 +107,336 @@ exports[`InventoryList Component should handle expandable guests data: number of /> `; +exports[`InventoryList Component should handle multiple display states, error, pending, fulfilled: error 1`] = ` + + + + + + + + + + + + + +
+
+ + + + + + + + + + + +`; + +exports[`InventoryList Component should handle multiple display states, error, pending, fulfilled: fulfilled 1`] = ` + + + + + + + + + + + + + +
+
+ + + + + + + + + + + +`; + +exports[`InventoryList Component should handle multiple display states, error, pending, fulfilled: pending 1`] = ` + + + + + + + + + + + + + +
+ +
+
+
+ + + + + + + +
+`; + +exports[`InventoryList Component should handle updating api data when query, or product id is updated: getInstancesInventory 1`] = ` +Array [ + Array [ + "lorem", + Object { + "lorem": "ipsum", + }, + ], + Array [ + "dolor", + Object { + "dolor": "sit", + }, + ], +] +`; + exports[`InventoryList Component should handle updating paging through redux state: dispatch onPage 1`] = ` Array [ Array [ @@ -178,7 +508,7 @@ Array [ "viewId": "lorem", }, Object { - "sort": "sockets", + "sort": "Sockets", "type": "SET_QUERY_RHSM_HOSTS_INVENTORY_sort", "viewId": "lorem", }, @@ -192,7 +522,7 @@ Array [ "viewId": "lorem", }, Object { - "sort": "sockets", + "sort": "Sockets", "type": "SET_QUERY_RHSM_HOSTS_INVENTORY_sort", "viewId": "lorem", }, @@ -206,7 +536,7 @@ Array [ "viewId": "lorem", }, Object { - "sort": "sockets", + "sort": "Sockets", "type": "SET_QUERY_RHSM_HOSTS_INVENTORY_sort", "viewId": "lorem", }, @@ -235,7 +565,7 @@ exports[`InventoryList Component should handle variations in data: filtered data useDispatch={[Function]} useProduct={[Function]} useProductInventoryHostsQuery={[Function]} - viewId="inventoryList" + viewId="inventoryInstancesList" /> { + 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 = { + query: { + [RHSM_API_QUERY_TYPES.LIMIT]: 10, + [RHSM_API_QUERY_TYPES.OFFSET]: 0 + }, + productId: 'lorem' + }; + + const component = shallow(); + expect(component).toMatchSnapshot('non-connected'); + }); + + it('should return an empty render when disabled', () => { + const props = { + query: { + [RHSM_API_QUERY_TYPES.LIMIT]: 10, + [RHSM_API_QUERY_TYPES.OFFSET]: 0 + }, + productId: 'lorem', + listData: [ + { lorem: 'ipsum', dolor: 'sit' }, + { lorem: 'sit', dolor: 'amet' } + ], + itemCount: 2, + isDisabled: true + }; + const component = shallow(); + + expect(component).toMatchSnapshot('disabled component'); + }); + + it('should handle variations in data', () => { + const props = { + query: { + [RHSM_API_QUERY_TYPES.LIMIT]: 10, + [RHSM_API_QUERY_TYPES.OFFSET]: 0 + }, + productId: 'lorem', + listData: [ + { lorem: 'ipsum', dolor: 'sit' }, + { lorem: 'sit', dolor: 'amet' } + ], + itemCount: 2 + }; + + const component = shallow(); + expect(component).toMatchSnapshot('variable data'); + + component.setProps({ + filterInventoryData: [{ id: 'lorem' }] + }); + + expect(component).toMatchSnapshot('filtered data'); + }); + + it('should handle expandable guests data', () => { + const props = { + query: { + [RHSM_API_QUERY_TYPES.LIMIT]: 10, + [RHSM_API_QUERY_TYPES.OFFSET]: 0 + }, + productId: 'lorem', + listData: [{ lorem: 'sit', dolor: 'amet', numberOfGuests: 1 }], + itemCount: 1 + }; + + const component = shallow(); + expect(component.find(Table)).toMatchSnapshot('number of guests'); + + component.setProps({ + ...props, + listData: [{ lorem: 'sit', dolor: 'amet', numberOfGuests: 1, subscriptionManagerId: 'loremIpsum' }] + }); + + expect(component.find(Table)).toMatchSnapshot('number of guests, and id'); + + component.setProps({ + ...props, + listData: [{ lorem: 'sit', dolor: 'amet', numberOfGuests: 2, subscriptionManagerId: 'loremIpsum' }], + settings: { + hasGuests: data => { + const { numberOfGuests = 0, subscriptionManagerId = null } = data; + return numberOfGuests > 2 && subscriptionManagerId; + } + } + }); + + expect(component.find(Table)).toMatchSnapshot('number of guests, id, and NO expandable guests display'); + }); + + it('should handle updating sorting through redux state', () => { + const props = { + query: { + [RHSM_API_QUERY_TYPES.LIMIT]: 10, + [RHSM_API_QUERY_TYPES.OFFSET]: 0 + }, + productId: 'lorem', + listData: [ + { lorem: 'ipsum', dolor: 'sit' }, + { lorem: 'sit', dolor: 'amet' } + ], + itemCount: 2 + }; + + const component = shallow(); + const componentInstance = component.instance(); + + componentInstance.onColumnSort({}, { direction: SortByDirection.asc, id: 'sockets' }); + componentInstance.onColumnSort({}, { direction: SortByDirection.desc, id: 'sockets' }); + componentInstance.onColumnSort({}, { direction: SortByDirection.asc, id: 'loremIpsumBrokenOnPurpose' }); + componentInstance.onColumnSort({}, { direction: SortByDirection.asc, id: 'sockets' }); + + expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch filter'); + }); + + it('should handle updating paging through redux state', () => { + const props = { + query: {}, + productId: 'lorem' + }; + + const component = shallow(); + const componentInstance = component.instance(); + + componentInstance.onPage({ offset: 10, perPage: 10 }); + componentInstance.onPage({ offset: 20, perPage: 10 }); + componentInstance.onPage({ offset: 0, perPage: 20 }); + componentInstance.onPage({ offset: 0, perPage: 50 }); + + expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch onPage'); + }); +}); diff --git a/src/components/inventoryList/__tests__/inventoryList.test.js b/src/components/inventoryList/__tests__/inventoryList.test.js index 5cc3157c1..8c4d15507 100644 --- a/src/components/inventoryList/__tests__/inventoryList.test.js +++ b/src/components/inventoryList/__tests__/inventoryList.test.js @@ -37,11 +37,13 @@ describe('InventoryList Component', () => { [RHSM_API_QUERY_TYPES.OFFSET]: 0 }, productId: 'lorem', - listData: [ + data: [ { lorem: 'ipsum', dolor: 'sit' }, { lorem: 'sit', dolor: 'amet' } ], - itemCount: 2, + meta: { + count: 2 + }, isDisabled: true }; const component = shallow(); @@ -49,6 +51,41 @@ describe('InventoryList Component', () => { expect(component).toMatchSnapshot('disabled component'); }); + it('should handle multiple display states, error, pending, fulfilled', () => { + const props = { + query: { + lorem: 'ipsum' + }, + productId: 'lorem', + pending: true + }; + + const component = shallow(); + expect(component).toMatchSnapshot('pending'); + + component.setProps({ + pending: false, + error: true + }); + + expect(component).toMatchSnapshot('error'); + + component.setProps({ + data: [ + { lorem: 'ipsum', dolor: 'sit' }, + { lorem: 'sit', dolor: 'amet' } + ], + meta: { + count: 2 + }, + pending: false, + error: false, + fulfilled: true + }); + + expect(component).toMatchSnapshot('fulfilled'); + }); + it('should handle variations in data', () => { const props = { query: { @@ -56,11 +93,13 @@ describe('InventoryList Component', () => { [RHSM_API_QUERY_TYPES.OFFSET]: 0 }, productId: 'lorem', - listData: [ + data: [ { lorem: 'ipsum', dolor: 'sit' }, { lorem: 'sit', dolor: 'amet' } ], - itemCount: 2 + meta: { + count: 2 + } }; const component = shallow(); @@ -80,8 +119,10 @@ describe('InventoryList Component', () => { [RHSM_API_QUERY_TYPES.OFFSET]: 0 }, productId: 'lorem', - listData: [{ lorem: 'sit', dolor: 'amet', numberOfGuests: 1 }], - itemCount: 1 + data: [{ lorem: 'sit', dolor: 'amet', numberOfGuests: 1 }], + meta: { + count: 1 + } }; const component = shallow(); @@ -89,14 +130,14 @@ describe('InventoryList Component', () => { component.setProps({ ...props, - listData: [{ lorem: 'sit', dolor: 'amet', numberOfGuests: 1, subscriptionManagerId: 'loremIpsum' }] + data: [{ lorem: 'sit', dolor: 'amet', numberOfGuests: 1, subscriptionManagerId: 'loremIpsum' }] }); expect(component.find(Table)).toMatchSnapshot('number of guests, and id'); component.setProps({ ...props, - listData: [{ lorem: 'sit', dolor: 'amet', numberOfGuests: 2, subscriptionManagerId: 'loremIpsum' }], + data: [{ lorem: 'sit', dolor: 'amet', numberOfGuests: 2, subscriptionManagerId: 'loremIpsum' }], settings: { hasGuests: data => { const { numberOfGuests = 0, subscriptionManagerId = null } = data; @@ -115,11 +156,13 @@ describe('InventoryList Component', () => { [RHSM_API_QUERY_TYPES.OFFSET]: 0 }, productId: 'lorem', - listData: [ + data: [ { lorem: 'ipsum', dolor: 'sit' }, { lorem: 'sit', dolor: 'amet' } ], - itemCount: 2 + meta: { + count: 2 + } }; const component = shallow(); @@ -149,4 +192,22 @@ describe('InventoryList Component', () => { expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch onPage'); }); + + it('should handle updating api data when query, or product id is updated', () => { + const props = { + query: { lorem: 'ipsum' }, + productId: 'lorem', + getInstancesInventory: jest.fn() + }; + + const component = shallow(); + + component.setProps({ + query: { dolor: 'sit' }, + productId: 'dolor' + }); + + expect(props.getInstancesInventory).toHaveBeenCalledTimes(2); + expect(props.getInstancesInventory.mock.calls).toMatchSnapshot('getInstancesInventory'); + }); }); diff --git a/src/components/inventoryList/inventoryList.deprecated.js b/src/components/inventoryList/inventoryList.deprecated.js new file mode 100644 index 000000000..126a5ad9b --- /dev/null +++ b/src/components/inventoryList/inventoryList.deprecated.js @@ -0,0 +1,379 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import _isEqual from 'lodash/isEqual'; +import { SortByDirection, TableVariant } from '@patternfly/react-table'; +import { Bullseye, Card, CardActions, CardBody, CardFooter, CardHeader, CardHeaderMain } from '@patternfly/react-core'; +import { TableToolbar } from '@redhat-cloud-services/frontend-components/TableToolbar'; +import _camelCase from 'lodash/camelCase'; +import { helpers } from '../../common'; +import { connect, reduxActions, reduxSelectors, reduxTypes, store } from '../../redux'; +import Table from '../table/table'; +import { Loader } from '../loader/loader'; +import { MinHeight } from '../minHeight/minHeight'; +import GuestsList from '../guestsList/guestsList'; +import { inventoryListHelpers } from './inventoryListHelpers'; +import Pagination from '../pagination/pagination'; +import { ToolbarFieldDisplayName } from '../toolbar/toolbarFieldDisplayName'; +import { paginationHelpers } from '../pagination/paginationHelpers'; +import { + RHSM_API_QUERY_SORT_DIRECTION_TYPES as SORT_DIRECTION_TYPES, + RHSM_API_QUERY_SORT_TYPES as SORT_TYPES, + RHSM_API_QUERY_TYPES +} from '../../types/rhsmApiTypes'; +import { translate } from '../i18n/i18n'; + +/** + * A hosts system inventory component. + * + * @augments React.Component + * @fires onColumnSort + * @fires onPage + * @fires onUpdateInventoryData + */ +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(); + } + } + + /** + * On column sort update state. + * + * @event onColumnSort + * @param {object} data pass-through inventory data. + * @param {object} sortParams + * @param {string} sortParams.direction + * @param {string} sortParams.id column identifier + */ + onColumnSort = (data, { direction, id }) => { + const { productId } = this.props; + const updatedSortColumn = Object.values(SORT_TYPES).find(value => _camelCase(value) === id); + let updatedDirection; + + if (!updatedSortColumn) { + if (helpers.DEV_MODE || helpers.REVIEW_MODE) { + console.warn(`Sorting can only be performed on select fields, confirm field ${id} is allowed.`); + } + return; + } + + switch (direction) { + case SortByDirection.desc: + updatedDirection = SORT_DIRECTION_TYPES.DESCENDING; + break; + default: + updatedDirection = SORT_DIRECTION_TYPES.ASCENDING; + break; + } + + store.dispatch([ + { + type: reduxTypes.query.SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES[RHSM_API_QUERY_TYPES.DIRECTION], + viewId: productId, + [RHSM_API_QUERY_TYPES.DIRECTION]: updatedDirection + }, + { + type: reduxTypes.query.SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES[RHSM_API_QUERY_TYPES.SORT], + viewId: productId, + [RHSM_API_QUERY_TYPES.SORT]: updatedSortColumn + } + ]); + }; + + /** + * On paging and on perPage events. + * + * @event onPage + * @param {object} params + * @param {number} params.offset + * @param {number} params.perPage + */ + onPage = ({ offset, perPage }) => { + const { productId } = this.props; + + store.dispatch([ + { + type: reduxTypes.query.SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES[RHSM_API_QUERY_TYPES.OFFSET], + viewId: productId, + [RHSM_API_QUERY_TYPES.OFFSET]: offset + }, + { + type: reduxTypes.query.SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES[RHSM_API_QUERY_TYPES.LIMIT], + viewId: productId, + [RHSM_API_QUERY_TYPES.LIMIT]: perPage + } + ]); + }; + + /** + * Call the RHSM APIs, apply filters. + * + * @event onUpdateInventoryData + */ + onUpdateInventoryData = () => { + const { getHostsInventory, isDisabled, productId, query } = this.props; + + if (!isDisabled && productId) { + getHostsInventory(productId, query); + } + }; + + /** + * Render an inventory table. + * + * @returns {Node} + */ + renderTable() { + const { filterGuestsData, filterInventoryData, listData, query, session, settings } = this.props; + let updatedColumnHeaders = []; + + const updatedRows = listData.map(({ ...cellData }) => { + const { columnHeaders, cells } = inventoryListHelpers.parseRowCellsListData({ + filters: inventoryListHelpers.parseInventoryFilters({ + filters: filterInventoryData, + onSort: this.onColumnSort, + query + }), + cellData, + session + }); + + updatedColumnHeaders = columnHeaders; + + const guestsId = cellData?.subscriptionManagerId; + let hasGuests = cellData?.numberOfGuests > 0 && guestsId; + + // Apply hasGuests callback, return boolean + if (typeof settings?.hasGuests === 'function') { + hasGuests = settings.hasGuests({ ...cellData }, { ...session }); + } + + return { + cells, + expandedContent: + (hasGuests && ( + + )) || + undefined + }; + }); + + return ( +
+ ); + } + + /** + * Render an inventory card. + * + * @returns {Node} + */ + render() { + const { + error, + filterInventoryData, + fulfilled, + isDisabled, + itemCount, + listData, + pending, + perPageDefault, + query, + t, + viewId + } = this.props; + + if (isDisabled) { + return ( + + + {t('curiosity-inventory.tab', { context: 'disabled' })} + + + ); + } + + const updatedPerPage = query[RHSM_API_QUERY_TYPES.LIMIT] || perPageDefault; + const updatedOffset = query[RHSM_API_QUERY_TYPES.OFFSET]; + const isLastPage = paginationHelpers.isLastPage(updatedOffset, updatedPerPage, itemCount); + + // Set an updated key to force refresh minHeight + const minHeightContentRefreshKey = + (fulfilled === true && itemCount < updatedPerPage && `bodyMinHeight-${updatedPerPage}-resize`) || + (fulfilled === true && isLastPage && `bodyMinHeight-${updatedPerPage}-resize`) || + (error === true && `bodyMinHeight-${updatedPerPage}-resize`) || + `bodyMinHeight-${updatedPerPage}`; + + return ( + + + + + + + + + + + + + +
+ {pending && ( + cellWidth)) || [], + rowCount: listData?.length || updatedPerPage, + variant: TableVariant.compact + }} + /> + )} + {!pending && this.renderTable()} +
+
+
+ + + + + + + +
+ ); + } +} + +/** + * Prop types. + * + * @type {{settings:object, productId: string, listData: Array, session: object, pending: boolean, + * query: object, fulfilled: boolean, getHostsInventory: Function, error: boolean, + * itemCount: number, viewId: string, t: Function, filterInventoryData: Array, filterGuestsData: Array, + * perPageDefault: number, isDisabled: boolean}} + */ +InventoryList.propTypes = { + error: PropTypes.bool, + fulfilled: PropTypes.bool, + filterGuestsData: PropTypes.array, + filterInventoryData: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + header: PropTypes.oneOfType([ + PropTypes.shape({ + title: PropTypes.node.isRequired + }), + PropTypes.func, + PropTypes.node + ]), + cell: PropTypes.oneOfType([ + PropTypes.shape({ + title: PropTypes.node.isRequired + }), + PropTypes.func, + PropTypes.node + ]) + }).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, + session: PropTypes.object, + settings: PropTypes.shape({ + hasGuests: PropTypes.func + }), + t: PropTypes.func, + viewId: PropTypes.string +}; + +/** + * Default props. + * + * @type {{settings: object, listData: Array, session: object, pending: boolean, fulfilled: boolean, + * getHostsInventory: Function, error: boolean, itemCount: number, viewId: string, t: translate, + * filterInventoryData: Array, filterGuestsData: Array, perPageDefault: number, isDisabled: boolean}} + */ +InventoryList.defaultProps = { + error: false, + fulfilled: false, + filterGuestsData: [], + filterInventoryData: [], + getHostsInventory: helpers.noop, + isDisabled: helpers.UI_DISABLED_TABLE_HOSTS, + itemCount: 0, + listData: [], + pending: false, + perPageDefault: 10, + session: {}, + settings: {}, + t: translate, + 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/inventoryList.js b/src/components/inventoryList/inventoryList.js index 126a5ad9b..be8f279dc 100644 --- a/src/components/inventoryList/inventoryList.js +++ b/src/components/inventoryList/inventoryList.js @@ -16,14 +16,26 @@ import Pagination from '../pagination/pagination'; import { ToolbarFieldDisplayName } from '../toolbar/toolbarFieldDisplayName'; import { paginationHelpers } from '../pagination/paginationHelpers'; import { - RHSM_API_QUERY_SORT_DIRECTION_TYPES as SORT_DIRECTION_TYPES, - RHSM_API_QUERY_SORT_TYPES as SORT_TYPES, - RHSM_API_QUERY_TYPES -} from '../../types/rhsmApiTypes'; + RHSM_API_QUERY_INVENTORY_SORT_DIRECTION_TYPES as SORT_DIRECTION_TYPES, + RHSM_API_QUERY_INVENTORY_SORT_TYPES as SORT_TYPES, + RHSM_API_QUERY_SET_TYPES +} from '../../services/rhsm/rhsmConstants'; import { translate } from '../i18n/i18n'; /** - * A hosts system inventory component. + * ToDo: refactor this component towards hooks. + * This base component was copied from the original hosts component in order to speed + * deliver the RHOSAK product views. This is an interim component and needs to be refactored. + * Refactor will include + * - Auth component, hook conversion. + * - Mock service updates for regular promise/platform calls + * - Mock service transform or hook for RBAC perms + * - Auth context with "useAuthContext" for grabbing session object + * - InventoryCard component + * - Display name toolbar filter, and filter/query context + */ +/** + * An instances interim inventory component. * * @augments React.Component * @fires onColumnSort @@ -54,7 +66,7 @@ class InventoryList extends React.Component { */ onColumnSort = (data, { direction, id }) => { const { productId } = this.props; - const updatedSortColumn = Object.values(SORT_TYPES).find(value => _camelCase(value) === id); + const updatedSortColumn = Object.values(SORT_TYPES).find(value => value === id || _camelCase(value) === id); let updatedDirection; if (!updatedSortColumn) { @@ -75,14 +87,14 @@ class InventoryList extends React.Component { store.dispatch([ { - type: reduxTypes.query.SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES[RHSM_API_QUERY_TYPES.DIRECTION], + type: reduxTypes.query.SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES[RHSM_API_QUERY_SET_TYPES.DIRECTION], viewId: productId, - [RHSM_API_QUERY_TYPES.DIRECTION]: updatedDirection + [RHSM_API_QUERY_SET_TYPES.DIRECTION]: updatedDirection }, { - type: reduxTypes.query.SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES[RHSM_API_QUERY_TYPES.SORT], + type: reduxTypes.query.SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES[RHSM_API_QUERY_SET_TYPES.SORT], viewId: productId, - [RHSM_API_QUERY_TYPES.SORT]: updatedSortColumn + [RHSM_API_QUERY_SET_TYPES.SORT]: updatedSortColumn } ]); }; @@ -100,14 +112,14 @@ class InventoryList extends React.Component { store.dispatch([ { - type: reduxTypes.query.SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES[RHSM_API_QUERY_TYPES.OFFSET], + type: reduxTypes.query.SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES[RHSM_API_QUERY_SET_TYPES.OFFSET], viewId: productId, - [RHSM_API_QUERY_TYPES.OFFSET]: offset + [RHSM_API_QUERY_SET_TYPES.OFFSET]: offset }, { - type: reduxTypes.query.SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES[RHSM_API_QUERY_TYPES.LIMIT], + type: reduxTypes.query.SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES[RHSM_API_QUERY_SET_TYPES.LIMIT], viewId: productId, - [RHSM_API_QUERY_TYPES.LIMIT]: perPage + [RHSM_API_QUERY_SET_TYPES.LIMIT]: perPage } ]); }; @@ -118,10 +130,10 @@ class InventoryList extends React.Component { * @event onUpdateInventoryData */ onUpdateInventoryData = () => { - const { getHostsInventory, isDisabled, productId, query } = this.props; + const { getInstancesInventory, isDisabled, productId, query } = this.props; if (!isDisabled && productId) { - getHostsInventory(productId, query); + getInstancesInventory(productId, query); } }; @@ -131,7 +143,7 @@ class InventoryList extends React.Component { * @returns {Node} */ renderTable() { - const { filterGuestsData, filterInventoryData, listData, query, session, settings } = this.props; + const { filterGuestsData, filterInventoryData, data: listData, query, session, settings } = this.props; let updatedColumnHeaders = []; const updatedRows = listData.map(({ ...cellData }) => { @@ -193,8 +205,8 @@ class InventoryList extends React.Component { filterInventoryData, fulfilled, isDisabled, - itemCount, - listData, + data: listData, + meta, pending, perPageDefault, query, @@ -212,8 +224,9 @@ class InventoryList extends React.Component { ); } - const updatedPerPage = query[RHSM_API_QUERY_TYPES.LIMIT] || perPageDefault; - const updatedOffset = query[RHSM_API_QUERY_TYPES.OFFSET]; + const itemCount = meta?.count; + const updatedPerPage = query[RHSM_API_QUERY_SET_TYPES.LIMIT] || perPageDefault; + const updatedOffset = query[RHSM_API_QUERY_SET_TYPES.OFFSET]; const isLastPage = paginationHelpers.isLastPage(updatedOffset, updatedPerPage, itemCount); // Set an updated key to force refresh minHeight @@ -289,12 +302,13 @@ class InventoryList extends React.Component { /** * Prop types. * - * @type {{settings:object, productId: string, listData: Array, session: object, pending: boolean, - * query: object, fulfilled: boolean, getHostsInventory: Function, error: boolean, - * itemCount: number, viewId: string, t: Function, filterInventoryData: Array, filterGuestsData: Array, - * perPageDefault: number, isDisabled: boolean}} + * @type {{settings: object, data: Array, productId: string, session: object, pending: boolean, query: object, + * fulfilled: boolean, error: boolean, getInstancesInventory: Function, viewId: string, t: Function, + * filterInventoryData: Array, meta: object, filterGuestsData: Array, perPageDefault: number, + * isDisabled: boolean}} */ InventoryList.propTypes = { + data: PropTypes.array, error: PropTypes.bool, fulfilled: PropTypes.bool, filterGuestsData: PropTypes.array, @@ -317,13 +331,12 @@ InventoryList.propTypes = { ]) }).isRequired ), - getHostsInventory: PropTypes.func, + getInstancesInventory: PropTypes.func, isDisabled: PropTypes.bool, - itemCount: PropTypes.number, - listData: PropTypes.array, + meta: PropTypes.shape({ count: PropTypes.number }), pending: PropTypes.bool, - productId: PropTypes.string.isRequired, perPageDefault: PropTypes.number, + productId: PropTypes.string.isRequired, query: PropTypes.object.isRequired, session: PropTypes.object, settings: PropTypes.shape({ @@ -336,25 +349,27 @@ InventoryList.propTypes = { /** * Default props. * - * @type {{settings: object, listData: Array, session: object, pending: boolean, fulfilled: boolean, - * getHostsInventory: Function, error: boolean, itemCount: number, viewId: string, t: translate, - * filterInventoryData: Array, filterGuestsData: Array, perPageDefault: number, isDisabled: boolean}} + * @type {{settings: object, data: Array, session: object, pending: boolean, fulfilled: boolean, error: boolean, + * getInstancesInventory: Function, viewId: string, t: Function, filterInventoryData: Array, meta: object, + * filterGuestsData: Array, perPageDefault: number, isDisabled: boolean}} */ InventoryList.defaultProps = { + data: [], error: false, fulfilled: false, filterGuestsData: [], filterInventoryData: [], - getHostsInventory: helpers.noop, - isDisabled: helpers.UI_DISABLED_TABLE_HOSTS, - itemCount: 0, - listData: [], + getInstancesInventory: helpers.noop, + isDisabled: helpers.UI_DISABLED_TABLE_INSTANCES, + meta: { + count: 0 + }, pending: false, perPageDefault: 10, session: {}, settings: {}, t: translate, - viewId: 'inventoryList' + viewId: 'inventoryInstancesList' }; /** @@ -364,7 +379,7 @@ InventoryList.defaultProps = { * @returns {object} */ const mapDispatchToProps = dispatch => ({ - getHostsInventory: (id, query) => dispatch(reduxActions.rhsm.getHostsInventory(id, query)) + getInstancesInventory: (id, query) => dispatch(reduxActions.rhsm.getInstancesInventory(id, query)) }); /** @@ -372,7 +387,7 @@ const mapDispatchToProps = dispatch => ({ * * @type {Function} */ -const makeMapStateToProps = reduxSelectors.inventoryList.makeInventoryList(); +const makeMapStateToProps = reduxSelectors.instancesList.makeInstancesList(); const ConnectedInventoryList = connect(makeMapStateToProps, mapDispatchToProps)(InventoryList); diff --git a/src/components/inventoryList/inventoryListHelpers.js b/src/components/inventoryList/inventoryListHelpers.js index 4fae3954c..d51b251ba 100644 --- a/src/components/inventoryList/inventoryListHelpers.js +++ b/src/components/inventoryList/inventoryListHelpers.js @@ -6,6 +6,7 @@ import { RHSM_API_QUERY_SORT_DIRECTION_TYPES as SORT_DIRECTION_TYPES, RHSM_API_QUERY_TYPES } from '../../types/rhsmApiTypes'; +import { helpers } from '../../common'; /** * Apply sort filter to filters. @@ -18,7 +19,8 @@ import { * @returns {object} */ const applySortFilters = ({ filter = {}, onSort, query = {} }) => { - const { id } = filter; + const { id, sortId } = filter; + const updatedId = sortId || id; const updatedFilter = { ...filter }; const hasSort = updatedFilter.onSort || onSort; @@ -31,7 +33,7 @@ const applySortFilters = ({ filter = {}, onSort, query = {} }) => { hasSort && typeof updatedFilter.sortActive !== 'boolean' && query?.[RHSM_API_QUERY_TYPES.SORT] && - _camelCase(query?.[RHSM_API_QUERY_TYPES.SORT]) === id + (query?.[RHSM_API_QUERY_TYPES.SORT] === updatedId || _camelCase(query?.[RHSM_API_QUERY_TYPES.SORT]) === updatedId) ) { updatedFilter.sortActive = true; } @@ -130,7 +132,7 @@ const parseRowCellsListData = ({ filters = [], cellData = {}, session = {} }) => updatedColumnHeaders.length = 0; updatedCells.length = 0; - filters.forEach(({ id, cell, cellWidth, header, onSort, sortActive, sortDirection, transforms }) => { + filters.forEach(({ id, cell, cellWidth, header, onSort, sortId, sortActive, sortDirection, transforms }) => { let headerUpdated; let cellUpdated; @@ -172,7 +174,7 @@ const parseRowCellsListData = ({ filters = [], cellData = {}, session = {} }) => if (typeof onSort === 'function') { headerUpdated = { ...headerUpdated, - onSort: obj => onSort({ ...allCells }, { ...obj, id }), + onSort: obj => onSort({ ...allCells }, { ...obj, id: sortId || id }), sortActive, sortDirection }; @@ -180,13 +182,24 @@ const parseRowCellsListData = ({ filters = [], cellData = {}, session = {} }) => // set table row cell filter params if (cell) { - cellUpdated = (typeof cell === 'function' && cell({ ...allCells }, { ...session })) || cell; + cellUpdated = typeof cell === 'function' ? cell({ ...allCells }, { ...session }) : cell; } if (typeof cellUpdated === 'string' || typeof cellUpdated === 'number' || React.isValidElement(cellUpdated)) { cellUpdated = { title: cellUpdated }; + } else if (!cellUpdated?.title) { + if (helpers.DEV_MODE || helpers.REVIEW_MODE) { + console.error( + `PF table throws an error when cell values don't conform to what it is expecting, or align exactly to column headers. + \n\nSee cell ID=${id} with VALUE=${cellUpdated}` + ); + } + + cellUpdated = { + title: '' + }; } updatedColumnHeaders.push(headerUpdated); diff --git a/src/components/productView/__tests__/__snapshots__/productView.test.js.snap b/src/components/productView/__tests__/__snapshots__/productView.test.js.snap index 73843a213..a4b933a0b 100644 --- a/src/components/productView/__tests__/__snapshots__/productView.test.js.snap +++ b/src/components/productView/__tests__/__snapshots__/productView.test.js.snap @@ -522,3 +522,64 @@ exports[`ProductView Component should render nothing if path and product paramet /> `; + +exports[`ProductView Component should use an instances inventory for rhosak: custom inventory, instances table 1`] = ` + + + +`; diff --git a/src/components/productView/__tests__/productView.test.js b/src/components/productView/__tests__/productView.test.js index cebc73e85..9e95458de 100644 --- a/src/components/productView/__tests__/productView.test.js +++ b/src/components/productView/__tests__/productView.test.js @@ -1,5 +1,7 @@ import React from 'react'; import { ProductView } from '../productView'; +import { config as rhosakConfig } from '../../../config/product.rhosak'; +import { InventoryTab } from '../../inventoryTabs/inventoryTab'; describe('ProductView Component', () => { it('should render a basic component', async () => { @@ -67,4 +69,17 @@ describe('ProductView Component', () => { const component = await shallowHookComponent(); expect(component).toMatchSnapshot('custom tabs, subscriptions table'); }); + + it('should use an instances inventory for rhosak', async () => { + const props = { + useRouteDetail: () => ({ + pathParameter: rhosakConfig.productId, + productParameter: rhosakConfig.productGroup, + productConfig: [rhosakConfig] + }) + }; + + const component = await shallowHookComponent(); + expect(component.find(InventoryTab).first()).toMatchSnapshot('custom inventory, instances table'); + }); }); diff --git a/src/components/productView/productView.js b/src/components/productView/productView.js index 8cc321759..6abc3b7f8 100644 --- a/src/components/productView/productView.js +++ b/src/components/productView/productView.js @@ -9,6 +9,7 @@ import { apiQueries } from '../../redux'; import { ConnectedGraphCard as ConnectedGraphCardDeprecated } from '../graphCard/graphCard.deprecated'; import { GraphCard } from '../graphCard/graphCard'; import { Toolbar } from '../toolbar/toolbar'; +import { ConnectedInventoryList as ConnectedInventoryListDeprecated } from '../inventoryList/inventoryList.deprecated'; import { ConnectedInventoryList } from '../inventoryList/inventoryList'; import { helpers } from '../../common'; import BannerMessages from '../bannerMessages/bannerMessages'; @@ -113,7 +114,9 @@ const ProductView = ({ t, toolbarGraph, toolbarGraphDescription, useRouteDetail: )} {productId === RHSM_API_PATH_PRODUCT_TYPES.RHOSAK && } - + - {!helpers.UI_DISABLED_TABLE_HOSTS && initialInventoryFilters && ( - - - - )} + {!helpers.UI_DISABLED_TABLE_HOSTS && + productId !== RHSM_API_PATH_PRODUCT_TYPES.RHOSAK && + initialInventoryFilters && ( + + + + )} + {!helpers.UI_DISABLED_TABLE_INSTANCES && + productId === RHSM_API_PATH_PRODUCT_TYPES.RHOSAK && + initialInventoryFilters && ( + + + + )} {!helpers.UI_DISABLED_TABLE_SUBSCRIPTIONS && initialSubscriptionsInventoryFilters && ( , + }, + ], + "columnHeaders": Array [ + Object { + "title": "t(curiosity-inventory.header, {\\"context\\":\\"display_name\\"})", + "transforms": Array [], + }, + Object { + "title": "t(curiosity-inventory.header, {\\"context\\":\\"Transfer-gibibytes\\"})", + "transforms": Array [ + [Function], + ], + }, + Object { + "title": "t(curiosity-inventory.header, {\\"context\\":\\"Storage-gibibytes\\"})", + "transforms": Array [ + [Function], + ], + }, + Object { + "title": "t(curiosity-inventory.header, {\\"context\\":\\"Instance-hours\\"})", + "transforms": Array [ + [Function], + ], + }, + Object { + "title": "t(curiosity-inventory.header, {\\"context\\":\\"last_seen\\"})", + "transforms": Array [ + [Function], + ], + }, + ], + "data": Object { + "Instance-hours": Object { + "title": "t(curiosity-inventory.header, {\\"context\\":\\"Instance-hours\\"})", + "value": 200, + }, + "Storage-gibibytes": Object { + "title": "t(curiosity-inventory.header, {\\"context\\":\\"Storage-gibibytes\\"})", + "value": 1000.00123, + }, + "Transfer-gibibytes": Object { + "title": "t(curiosity-inventory.header, {\\"context\\":\\"Transfer-gibibytes\\"})", + "value": 0.0003456, + }, + "display_name": Object { + "title": "t(curiosity-inventory.header, {\\"context\\":\\"display_name\\"})", + "value": "lorem ipsum", + }, + "last_seen": Object { + "title": "t(curiosity-inventory.header, {\\"context\\":\\"last_seen\\"})", + "value": "2020-04-02T00:00:00Z", + }, + }, +} +`; + +exports[`Product RHOSAK config should apply an instances inventory configuration under hosts: filtered, authorized 1`] = ` +Object { + "cells": Array [ + Object { + "title": + + , + }, + Object { + "title": "t(curiosity-inventory.measurement, {\\"context\\":\\"Transfer-gibibytes\\",\\"total\\":\\"0.00035\\"})", + }, + Object { + "title": "t(curiosity-inventory.measurement, {\\"context\\":\\"Storage-gibibytes\\",\\"total\\":\\"1000.00123\\"})", + }, + Object { + "title": "t(curiosity-inventory.measurement, {\\"context\\":\\"Instance-hours\\",\\"total\\":\\"200\\"})", + }, + Object { + "title": , + }, + ], + "columnHeaders": Array [ + Object { + "title": "t(curiosity-inventory.header, {\\"context\\":\\"display_name\\"})", + "transforms": Array [], + }, + Object { + "title": "t(curiosity-inventory.header, {\\"context\\":\\"Transfer-gibibytes\\"})", + "transforms": Array [ + [Function], + ], + }, + Object { + "title": "t(curiosity-inventory.header, {\\"context\\":\\"Storage-gibibytes\\"})", + "transforms": Array [ + [Function], + ], + }, + Object { + "title": "t(curiosity-inventory.header, {\\"context\\":\\"Instance-hours\\"})", + "transforms": Array [ + [Function], + ], + }, + Object { + "title": "t(curiosity-inventory.header, {\\"context\\":\\"last_seen\\"})", + "transforms": Array [ + [Function], + ], + }, + ], + "data": Object { + "Instance-hours": Object { + "title": "t(curiosity-inventory.header, {\\"context\\":\\"Instance-hours\\"})", + "value": 200, + }, + "Storage-gibibytes": Object { + "title": "t(curiosity-inventory.header, {\\"context\\":\\"Storage-gibibytes\\"})", + "value": 1000.00123, + }, + "Transfer-gibibytes": Object { + "title": "t(curiosity-inventory.header, {\\"context\\":\\"Transfer-gibibytes\\"})", + "value": 0.0003456, + }, + "display_name": Object { + "title": "t(curiosity-inventory.header, {\\"context\\":\\"display_name\\"})", + "value": "lorem ipsum", + }, + "inventory_id": Object { + "title": "t(curiosity-inventory.header, {\\"context\\":\\"inventory_id\\"})", + "value": "XXXX-XXXX-XXXXX-XXXXX", + }, + "last_seen": Object { + "title": "t(curiosity-inventory.header, {\\"context\\":\\"last_seen\\"})", + "value": "2020-04-02T00:00:00Z", + }, + }, +} +`; + exports[`Product RHOSAK config should apply graph configuration: filters 1`] = ` Object { "groupedFilters": Array [], diff --git a/src/config/__tests__/__snapshots__/product.satellite.test.js.snap b/src/config/__tests__/__snapshots__/product.satellite.test.js.snap index 6db436ef0..af776a071 100644 --- a/src/config/__tests__/__snapshots__/product.satellite.test.js.snap +++ b/src/config/__tests__/__snapshots__/product.satellite.test.js.snap @@ -234,7 +234,9 @@ Object { Object { "title": 10, }, - [Function], + Object { + "title": "", + }, ], "columnHeaders": Array [ Object { diff --git a/src/config/__tests__/product.rhosak.test.js b/src/config/__tests__/product.rhosak.test.js index b68dff146..6d513fde6 100644 --- a/src/config/__tests__/product.rhosak.test.js +++ b/src/config/__tests__/product.rhosak.test.js @@ -1,5 +1,12 @@ import { config } from '../product.rhosak'; import { generateChartSettings } from '../../components/graphCard/graphCardHelpers'; +import { parseRowCellsListData } from '../../components/inventoryList/inventoryListHelpers'; +import { + RHSM_API_QUERY_INVENTORY_SORT_DIRECTION_TYPES as SORT_DIRECTION_TYPES, + RHSM_API_RESPONSE_INSTANCES_DATA_TYPES as INVENTORY_TYPES, + RHSM_API_QUERY_SET_TYPES, + RHSM_API_PATH_METRIC_TYPES +} from '../../services/rhsm/rhsmConstants'; describe('Product RHOSAK config', () => { it('should apply graph configuration', () => { @@ -39,4 +46,39 @@ describe('Product RHOSAK config', () => { expect(generateTicks()).toMatchSnapshot('yAxisTickFormat'); }); + + /** + * FixMe: this test needs to be updated as part of the refactor towards instances vs hosts + */ + it('should apply an instances inventory configuration under hosts', () => { + const { initialInventoryFilters: initialFilters, inventoryHostsQuery: inventoryQuery } = config; + + const inventoryData = { + [INVENTORY_TYPES.DISPLAY_NAME]: 'lorem ipsum', + [RHSM_API_PATH_METRIC_TYPES.TRANSFER_GIBIBYTES]: 0.0003456, + [RHSM_API_PATH_METRIC_TYPES.STORAGE_GIBIBYTES]: 1000.00123, + [RHSM_API_PATH_METRIC_TYPES.INSTANCE_HOURS]: 200, + [INVENTORY_TYPES.LAST_SEEN]: '2020-04-02T00:00:00Z' + }; + + const filteredInventoryData = parseRowCellsListData({ + filters: initialFilters, + cellData: inventoryData + }); + + expect(filteredInventoryData).toMatchSnapshot('filtered'); + + const filteredInventoryDataAuthorized = parseRowCellsListData({ + filters: initialFilters, + cellData: { + ...inventoryData, + [INVENTORY_TYPES.INVENTORY_ID]: 'XXXX-XXXX-XXXXX-XXXXX' + }, + session: { authorized: { inventory: true } } + }); + + expect(filteredInventoryDataAuthorized).toMatchSnapshot('filtered, authorized'); + + expect(inventoryQuery[RHSM_API_QUERY_SET_TYPES.DIRECTION] === SORT_DIRECTION_TYPES.DESCENDING).toBe(true); + }); }); diff --git a/src/config/product.rhosak.js b/src/config/product.rhosak.js index a2349be6c..a2e78d2ac 100644 --- a/src/config/product.rhosak.js +++ b/src/config/product.rhosak.js @@ -1,3 +1,4 @@ +import React from 'react'; import { chart_color_blue_100 as chartColorBlueLight, chart_color_blue_300 as chartColorBlueDark, @@ -6,7 +7,12 @@ import { chart_color_purple_100 as chartColorPurpleLight, chart_color_purple_300 as chartColorPurpleDark } from '@patternfly/react-tokens'; +import { Button } from '@patternfly/react-core'; +import { DateFormat } from '@redhat-cloud-services/frontend-components/DateFormat'; import { + RHSM_API_QUERY_INVENTORY_SORT_DIRECTION_TYPES as SORT_DIRECTION_TYPES, + RHSM_API_QUERY_INVENTORY_SORT_TYPES, + RHSM_API_RESPONSE_INSTANCES_DATA_TYPES as INVENTORY_TYPES, RHSM_API_QUERY_GRANULARITY_TYPES as GRANULARITY_TYPES, RHSM_API_QUERY_SET_TYPES, RHSM_API_PATH_PRODUCT_TYPES, @@ -38,7 +44,12 @@ const config = { graphTallyQuery: { [RHSM_API_QUERY_SET_TYPES.GRANULARITY]: GRANULARITY_TYPES.DAILY }, - inventoryHostsQuery: {}, + inventoryHostsQuery: { + [RHSM_API_QUERY_SET_TYPES.SORT]: RHSM_API_QUERY_INVENTORY_SORT_TYPES.LAST_SEEN, + [RHSM_API_QUERY_SET_TYPES.DIRECTION]: SORT_DIRECTION_TYPES.DESCENDING, + [RHSM_API_QUERY_SET_TYPES.LIMIT]: 100, + [RHSM_API_QUERY_SET_TYPES.OFFSET]: 0 + }, inventorySubscriptionsQuery: {}, initialGraphFilters: [ { @@ -88,6 +99,80 @@ const config = { ?.toUpperCase(); } }, + initialInventoryFilters: [ + { + id: INVENTORY_TYPES.DISPLAY_NAME, + cell: ( + { [INVENTORY_TYPES.DISPLAY_NAME]: displayName = {}, [INVENTORY_TYPES.INVENTORY_ID]: inventoryId = {} }, + session + ) => { + const { inventory: authorized } = session?.authorized || {}; + + if (!inventoryId.value) { + return displayName.value; + } + + let updatedDisplayName = displayName.value || inventoryId.value; + + if (authorized) { + updatedDisplayName = ( + + ); + } + + return {updatedDisplayName}; + }, + isSortable: true + }, + { + id: RHSM_API_PATH_METRIC_TYPES.TRANSFER_GIBIBYTES, + cell: ({ [RHSM_API_PATH_METRIC_TYPES.TRANSFER_GIBIBYTES]: total }) => + translate('curiosity-inventory.measurement', { + context: RHSM_API_PATH_METRIC_TYPES.TRANSFER_GIBIBYTES, + total: helpers.numberDisplay(total?.value)?.format({ mantissa: 5, trimMantissa: true }) || 0 + }), + isSortable: true, + isWrappable: true, + cellWidth: 15 + }, + { + id: RHSM_API_PATH_METRIC_TYPES.STORAGE_GIBIBYTES, + cell: ({ [RHSM_API_PATH_METRIC_TYPES.STORAGE_GIBIBYTES]: total }) => + translate('curiosity-inventory.measurement', { + context: RHSM_API_PATH_METRIC_TYPES.STORAGE_GIBIBYTES, + total: helpers.numberDisplay(total?.value)?.format({ mantissa: 5, trimMantissa: true }) || 0 + }), + isSortable: true, + isWrappable: true, + cellWidth: 15 + }, + { + id: RHSM_API_PATH_METRIC_TYPES.INSTANCE_HOURS, + cell: ({ [RHSM_API_PATH_METRIC_TYPES.INSTANCE_HOURS]: total }) => + translate('curiosity-inventory.measurement', { + context: RHSM_API_PATH_METRIC_TYPES.INSTANCE_HOURS, + total: helpers.numberDisplay(total?.value)?.format({ mantissa: 5, trimMantissa: true }) || 0 + }), + isSortable: true, + isWrappable: true, + cellWidth: 15 + }, + { + id: INVENTORY_TYPES.LAST_SEEN, + cell: ({ [INVENTORY_TYPES.LAST_SEEN]: lastSeen }) => + (lastSeen?.value && ) || '', + isSortable: true, + isWrappable: true, + cellWidth: 15 + } + ], initialToolbarFilters: [ { id: 'rangedMonthly' diff --git a/src/styles/_page-layout.scss b/src/styles/_page-layout.scss index c5f43e178..cdbae6b9b 100644 --- a/src/styles/_page-layout.scss +++ b/src/styles/_page-layout.scss @@ -10,6 +10,14 @@ position: relative; } +.curiosity-page-section { + &__tabs { + margin-bottom: var(--pf-global--spacer--md); + margin-left: var(--pf-global--spacer--md); + margin-right: var(--pf-global--spacer--md); + } +} + .curiosity-page-columns > .curiosity-page-columns-column { margin-left: 0; margin-right: 0; diff --git a/src/styles/_usage-graph.scss b/src/styles/_usage-graph.scss index ead5f4983..fdc14f53a 100644 --- a/src/styles/_usage-graph.scss +++ b/src/styles/_usage-graph.scss @@ -76,6 +76,9 @@ } &__totals { + /** + * ToDo: consider relocating margin into page-layout once margin is restored to all product layouts + */ margin-left: var(--pf-global--spacer--md); margin-right: var(--pf-global--spacer--md); margin-top: var(--pf-global--spacer--md); diff --git a/tests/__snapshots__/code.test.js.snap b/tests/__snapshots__/code.test.js.snap index 176305d3e..f3ab44738 100644 --- a/tests/__snapshots__/code.test.js.snap +++ b/tests/__snapshots__/code.test.js.snap @@ -2,7 +2,9 @@ exports[`General code checks should only have specific console.[warn|log|info|error] methods: console methods 1`] = ` Array [ - "components/inventoryList/inventoryList.js:62: console.warn(\`Sorting can only be performed on select fields, confirm field \${id} is allowed.\`);", + "components/inventoryList/inventoryList.deprecated.js:62: console.warn(\`Sorting can only be performed on select fields, confirm field \${id} is allowed.\`);", + "components/inventoryList/inventoryList.js:74: console.warn(\`Sorting can only be performed on select fields, confirm field \${id} is allowed.\`);", + "components/inventoryList/inventoryListHelpers.js:194: console.error(", "components/inventorySubscriptions/inventorySubscriptions.js:60: console.warn(\`Sorting can only be performed on select fields, confirm field \${id} is allowed.\`);", "redux/common/reduxHelpers.js:282: console.error(\`Error: Property \${prop} does not exist within the passed state.\`, state);", "redux/common/reduxHelpers.js:286: console.warn(\`Warning: Property \${prop} does not exist within the passed initialState.\`, initialState);", diff --git a/tests/__snapshots__/dist.test.js.snap b/tests/__snapshots__/dist.test.js.snap index 05bb49639..9342c043e 100644 --- a/tests/__snapshots__/dist.test.js.snap +++ b/tests/__snapshots__/dist.test.js.snap @@ -16,6 +16,7 @@ Array [ "./dist/fonts/pfbg_768@2x.jpg", "./dist/fonts/pfbg_992@2x.jpg", "./dist/index.html", + "./dist/js/1001*js", "./dist/js/1014*js", "./dist/js/1026*js", "./dist/js/1138*js", @@ -39,13 +40,14 @@ Array [ "./dist/js/4097*js", "./dist/js/4418*js", "./dist/js/4467*js", + "./dist/js/4623*js", "./dist/js/4822*js", + "./dist/js/4944*js", "./dist/js/5020*js", "./dist/js/5076*js", "./dist/js/5394*js", "./dist/js/5876*js", "./dist/js/608*js", - "./dist/js/6304*js", "./dist/js/6395*js", "./dist/js/6491*js", "./dist/js/6876*js", @@ -70,7 +72,6 @@ Array [ "./dist/js/9844*js", "./dist/js/9877*js", "./dist/js/9928*js", - "./dist/js/9977*js", "./dist/js/App*js", "./dist/js/pfVendor*js", "./dist/js/pfVendor*js*txt", @@ -82,6 +83,7 @@ Array [ "./dist/locales/en-US.json", "./dist/locales/en.json", "./dist/locales/locales.json", + "./dist/sourcemaps/1001*js.map", "./dist/sourcemaps/1014*js.map", "./dist/sourcemaps/1026*js.map", "./dist/sourcemaps/1138*js.map", @@ -105,13 +107,14 @@ Array [ "./dist/sourcemaps/4097*js.map", "./dist/sourcemaps/4418*js.map", "./dist/sourcemaps/4467*js.map", + "./dist/sourcemaps/4623*js.map", "./dist/sourcemaps/4822*js.map", + "./dist/sourcemaps/4944*js.map", "./dist/sourcemaps/5020*js.map", "./dist/sourcemaps/5076*js.map", "./dist/sourcemaps/5394*js.map", "./dist/sourcemaps/5876*js.map", "./dist/sourcemaps/608*js.map", - "./dist/sourcemaps/6304*js.map", "./dist/sourcemaps/6395*js.map", "./dist/sourcemaps/6491*js.map", "./dist/sourcemaps/6876*js.map", @@ -136,7 +139,6 @@ Array [ "./dist/sourcemaps/9844*js.map", "./dist/sourcemaps/9877*js.map", "./dist/sourcemaps/9928*js.map", - "./dist/sourcemaps/9977*js.map", "./dist/sourcemaps/App*js.map", "./dist/sourcemaps/pfVendor*js.map", "./dist/sourcemaps/reactVendor*js.map",