From 273ca429d5400c24ca48c6dccc2759b9e6181b47 Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Tue, 14 Dec 2021 11:14:11 -0500 Subject: [PATCH] refactor(inventoryCard): ent-4572 class, function conversion (#857) * locale, disabled tab string * guestsList, class to func, deprecated guests * guestsListContext, hooks for selectors, action * inventoryCard, class to func * inventoryCardContext, hooks for selectors, action * inventorySubscriptions, class to func, alias inventoryCard * inventorySubscriptionsContext, hooks for selectors, action * productViews, component prop drill, config clean up * productViewContext, activate guests config * useReactRedux, expand base redux hooks * viewReducer, guests query and types * selectors, remove unused instances, subscriptions * rhsmConstants, expand for guests, number_of_guests * rhsmSchemas, guests, subscriptions added * rhsmTransformers, instances number_of_guests added --- public/locales/en-US.json | 2 +- .../guestsList.deprecated.test.js.snap | 267 ++++++++++ .../__snapshots__/guestsList.test.js.snap | 112 ++--- .../guestsListContext.test.js.snap | 60 +++ .../__tests__/guestsList.deprecated.test.js | 83 ++++ .../guestsList/__tests__/guestsList.test.js | 85 ++-- .../__tests__/guestsListContext.test.js | 76 +++ .../guestsList/guestsList.deprecated.js | 267 ++++++++++ src/components/guestsList/guestsList.js | 248 ++++------ .../guestsList/guestsListContext.js | 140 ++++++ .../__tests__/__snapshots__/i18n.test.js.snap | 13 - .../__snapshots__/inventoryCard.test.js.snap | 194 ++------ .../inventoryCardContext.test.js.snap | 106 ++++ .../__tests__/inventoryCard.test.js | 251 +++++----- .../__tests__/inventoryCardContext.test.js | 104 ++++ src/components/inventoryList/inventoryCard.js | 457 ++++++------------ .../inventoryList/inventoryCardContext.js | 162 +++++++ .../inventoryList/inventoryList.deprecated.js | 6 +- .../inventorySubscriptions.test.js.snap | 430 +--------------- ...inventorySubscriptionsContext.test.js.snap | 106 ++++ .../__tests__/inventorySubscriptions.test.js | 110 +---- .../inventorySubscriptionsContext.test.js | 104 ++++ .../inventorySubscriptions.js | 370 ++------------ .../inventorySubscriptionsContext.js | 168 +++++++ .../__snapshots__/productView.test.js.snap | 66 +-- .../productViewContext.test.js.snap | 16 + ...productViewOpenShiftContainer.test.js.snap | 57 +-- .../__tests__/productViewContext.test.js | 10 + src/components/productView/productView.js | 29 +- .../productView/productViewContext.js | 32 +- .../productViewOpenShiftContainer.js | 17 +- .../product.openshiftContainer.test.js.snap | 2 +- .../__snapshots__/product.rhel.test.js.snap | 2 +- src/config/product.openshiftContainer.js | 6 +- src/config/product.rhel.js | 6 +- .../__snapshots__/useReactRedux.test.js.snap | 110 +++++ .../hooks/__tests__/useReactRedux.test.js | 62 +++ src/redux/hooks/useReactRedux.js | 78 ++- .../__snapshots__/viewReducer.test.js.snap | 196 +++++++- .../reducers/__tests__/viewReducer.test.js | 10 +- src/redux/reducers/viewReducer.js | 55 +++ .../instancesListSelectors.test.js.snap | 32 -- .../subscriptionsListSelectors.test.js.snap | 275 ----------- .../__tests__/instancesListSelectors.test.js | 53 -- .../subscriptionsListSelectors.test.js | 259 ---------- src/redux/selectors/index.js | 4 - src/redux/selectors/instancesListSelectors.js | 70 --- .../selectors/subscriptionsListSelectors.js | 162 ------- .../__snapshots__/index.test.js.snap | 20 + src/redux/types/queryTypes.js | 15 +- .../__snapshots__/rhsmConstants.test.js.snap | 36 ++ .../__snapshots__/rhsmSchemas.test.js.snap | 20 +- .../rhsmTransformers.test.js.snap | 2 + .../rhsm/__tests__/rhsmSchemas.test.js | 4 +- src/services/rhsm/rhsmConstants.js | 22 +- src/services/rhsm/rhsmSchemas.js | 75 +++ src/services/rhsm/rhsmServices.js | 39 +- src/services/rhsm/rhsmTranformers.js | 25 +- .../__snapshots__/index.test.js.snap | 104 ++-- src/types/rhsmApiTypes.js | 138 +++--- tests/__snapshots__/code.test.js.snap | 4 +- tests/__snapshots__/dist.test.js.snap | 16 +- 62 files changed, 3092 insertions(+), 2958 deletions(-) create mode 100644 src/components/guestsList/__tests__/__snapshots__/guestsList.deprecated.test.js.snap create mode 100644 src/components/guestsList/__tests__/__snapshots__/guestsListContext.test.js.snap create mode 100644 src/components/guestsList/__tests__/guestsList.deprecated.test.js create mode 100644 src/components/guestsList/__tests__/guestsListContext.test.js create mode 100644 src/components/guestsList/guestsList.deprecated.js create mode 100644 src/components/guestsList/guestsListContext.js create mode 100644 src/components/inventoryList/__tests__/__snapshots__/inventoryCardContext.test.js.snap create mode 100644 src/components/inventoryList/__tests__/inventoryCardContext.test.js create mode 100644 src/components/inventoryList/inventoryCardContext.js create mode 100644 src/components/inventorySubscriptions/__tests__/__snapshots__/inventorySubscriptionsContext.test.js.snap create mode 100644 src/components/inventorySubscriptions/__tests__/inventorySubscriptionsContext.test.js create mode 100644 src/components/inventorySubscriptions/inventorySubscriptionsContext.js delete mode 100644 src/redux/selectors/__tests__/__snapshots__/instancesListSelectors.test.js.snap delete mode 100644 src/redux/selectors/__tests__/__snapshots__/subscriptionsListSelectors.test.js.snap delete mode 100644 src/redux/selectors/__tests__/instancesListSelectors.test.js delete mode 100644 src/redux/selectors/__tests__/subscriptionsListSelectors.test.js delete mode 100644 src/redux/selectors/instancesListSelectors.js delete mode 100644 src/redux/selectors/subscriptionsListSelectors.js diff --git a/public/locales/en-US.json b/public/locales/en-US.json index 1d1849c20..fa8060cc3 100644 --- a/public/locales/en-US.json +++ b/public/locales/en-US.json @@ -100,7 +100,7 @@ "tabHosts_OpenShift-dedicated-metrics_other": "{{count}} instances", "tabInstances": "Current monthly instances", "tabSubscriptions": "Current subscriptions", - "tab_disabled": "The {{tabName}} display is currently disabled.", + "tab_disabled": "The inventory display is currently disabled.", "tableAriaLabel": "{{appName}} systems inventory table.", "tableSummary": "A generated table with one level of column headers.", "tableEmptyInventoryTitle": "No results found", diff --git a/src/components/guestsList/__tests__/__snapshots__/guestsList.deprecated.test.js.snap b/src/components/guestsList/__tests__/__snapshots__/guestsList.deprecated.test.js.snap new file mode 100644 index 000000000..2a87d26bf --- /dev/null +++ b/src/components/guestsList/__tests__/__snapshots__/guestsList.deprecated.test.js.snap @@ -0,0 +1,267 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GuestsList Component should handle multiple display states: fulfilled 1`] = ` +
+
+
+ + + + +`; + +exports[`GuestsList Component should handle multiple display states: initial pending 1`] = ` +
+ +
+`; + +exports[`GuestsList Component should handle multiple display states: paged pending 1`] = ` +
+
+
+
+ +
+
+
+
+`; + +exports[`GuestsList Component should handle updating paging state: state 1`] = ` +Object { + "initialState": Object { + "currentPage": 0, + "limit": 100, + "previousData": Array [], + }, + "scrollComplete": Object { + "currentPage": 1, + "limit": 100, + "previousData": Array [ + Object { + "dolor": "sit", + "lorem": "ipsum", + }, + ], + }, + "scrollProgress": Object { + "currentPage": 0, + "limit": 100, + "previousData": Array [], + }, +} +`; + +exports[`GuestsList Component should handle variations in data: filtered data 1`] = ` +
+
+
+
+ + + +`; + +exports[`GuestsList Component should handle variations in data: variable data 1`] = ` +
+
+
+
+ + + +`; + +exports[`GuestsList Component should render a non-connected component: non-connected 1`] = ` +
+
+
+
+
+`; diff --git a/src/components/guestsList/__tests__/__snapshots__/guestsList.test.js.snap b/src/components/guestsList/__tests__/__snapshots__/guestsList.test.js.snap index 2a87d26bf..8ae00987d 100644 --- a/src/components/guestsList/__tests__/__snapshots__/guestsList.test.js.snap +++ b/src/components/guestsList/__tests__/__snapshots__/guestsList.test.js.snap @@ -1,5 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`GuestsList Component should handle an onScroll event: scroll event 1`] = ` +Array [ + Array [ + Object { + "target": Object { + "clientHeight": 100, + "scrollHeight": 200, + "scrollTop": 100, + }, + }, + ], +] +`; + exports[`GuestsList Component should handle multiple display states: fulfilled 1`] = `
`; -exports[`GuestsList Component should handle multiple display states: paged pending 1`] = ` -
-
-
-
- -
-
-
-
-`; - -exports[`GuestsList Component should handle updating paging state: state 1`] = ` -Object { - "initialState": Object { - "currentPage": 0, - "limit": 100, - "previousData": Array [], - }, - "scrollComplete": Object { - "currentPage": 1, - "limit": 100, - "previousData": Array [ - Object { - "dolor": "sit", - "lorem": "ipsum", - }, - ], - }, - "scrollProgress": Object { - "currentPage": 0, - "limit": 100, - "previousData": Array [], - }, -} -`; - exports[`GuestsList Component should handle variations in data: filtered data 1`] = `
`; -exports[`GuestsList Component should render a non-connected component: non-connected 1`] = ` +exports[`GuestsList Component should render a basic component: basic render 1`] = `
-
-
-
+ tableProps={ + Object { + "borders": false, + "className": "curiosity-guests-list", + "colCount": 1, + "colWidth": Array [], + "rowCount": 0, + "variant": "compact", + } + } + variant="table" + />
`; diff --git a/src/components/guestsList/__tests__/__snapshots__/guestsListContext.test.js.snap b/src/components/guestsList/__tests__/__snapshots__/guestsListContext.test.js.snap new file mode 100644 index 000000000..92214d8f0 --- /dev/null +++ b/src/components/guestsList/__tests__/__snapshots__/guestsListContext.test.js.snap @@ -0,0 +1,60 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InventorySubscriptionsContext should handle an onScroll event: onPage event, dispatch 1`] = ` +Array [ + Array [ + Array [ + Object { + "offset": 1, + "type": "SET_QUERY_RHSM_GUESTS_INVENTORY_offset", + "viewId": "1234567890", + }, + Object { + "limit": 100, + "type": "SET_QUERY_RHSM_GUESTS_INVENTORY_limit", + "viewId": "1234567890", + }, + ], + ], + Array [ + Array [ + Object { + "type": "SET_QUERY_CLEAR_INVENTORY_GUESTS_LIST", + "viewId": "1234567890", + }, + ], + ], +] +`; + +exports[`InventorySubscriptionsContext should handle instances inventory API responses: inventory, cancelled 1`] = ` +Object { + "cancelled": true, +} +`; + +exports[`InventorySubscriptionsContext should handle instances inventory API responses: inventory, error 1`] = ` +Object { + "error": true, +} +`; + +exports[`InventorySubscriptionsContext should handle instances inventory API responses: inventory, fulfilled 1`] = ` +Object { + "fulfilled": true, +} +`; + +exports[`InventorySubscriptionsContext should handle instances inventory API responses: inventory, pending 1`] = ` +Object { + "pending": true, +} +`; + +exports[`InventorySubscriptionsContext should return specific properties: specific properties 1`] = ` +Object { + "useGetGuestsInventory": [Function], + "useOnScroll": [Function], + "useSelectorsGuestsInventory": [Function], +} +`; diff --git a/src/components/guestsList/__tests__/guestsList.deprecated.test.js b/src/components/guestsList/__tests__/guestsList.deprecated.test.js new file mode 100644 index 000000000..c775a3c82 --- /dev/null +++ b/src/components/guestsList/__tests__/guestsList.deprecated.test.js @@ -0,0 +1,83 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { GuestsList } from '../guestsList.deprecated'; + +describe('GuestsList Component', () => { + it('should render a non-connected component', () => { + const props = { + id: 'lorem', + numberOfGuests: 0 + }; + + const component = shallow(); + expect(component).toMatchSnapshot('non-connected'); + }); + + it('should handle variations in data', () => { + const props = { + id: 'lorem', + numberOfGuests: 2, + listData: [ + { lorem: 'ipsum', dolor: 'sit' }, + { lorem: 'amet', dolor: 'amet' } + ] + }; + + const component = shallow(); + expect(component).toMatchSnapshot('variable data'); + + component.setProps({ + filterGuestsData: [{ id: 'lorem' }] + }); + + expect(component).toMatchSnapshot('filtered data'); + }); + + it('should handle multiple display states', () => { + const props = { + id: 'lorem', + numberOfGuests: 1, + pending: true + }; + + const component = shallow(); + expect(component).toMatchSnapshot('initial pending'); + + component.setProps({ + pending: false, + listData: [{ lorem: 'ipsum', dolor: 'sit' }] + }); + + expect(component).toMatchSnapshot('fulfilled'); + + component.setState({ currentPage: 1 }); + + component.setProps({ + pending: true, + listData: [] + }); + + expect(component).toMatchSnapshot('paged pending'); + }); + + it('should handle updating paging state', () => { + const props = { + id: 'lorem', + numberOfGuests: 111, + listData: [{ lorem: 'ipsum', dolor: 'sit' }] + }; + + const component = shallow(); + const componentInstance = component.instance(); + + const initialState = component.state(); + + componentInstance.onScroll({ target: { scrollHeight: 100, scrollTop: 20, clientHeight: 100 } }); + const scrollProgress = component.state(); + + componentInstance.onScroll({ target: { scrollHeight: 100, scrollTop: 0, clientHeight: 100 } }); + const scrollComplete = component.state(); + + expect({ initialState, scrollProgress, scrollComplete }).toMatchSnapshot('state'); + }); +}); diff --git a/src/components/guestsList/__tests__/guestsList.test.js b/src/components/guestsList/__tests__/guestsList.test.js index abdef74f0..91f20a5e6 100644 --- a/src/components/guestsList/__tests__/guestsList.test.js +++ b/src/components/guestsList/__tests__/guestsList.test.js @@ -1,83 +1,92 @@ import React from 'react'; -import { shallow } from 'enzyme'; import { GuestsList } from '../guestsList'; describe('GuestsList Component', () => { - it('should render a non-connected component', () => { + it('should render a basic component', async () => { const props = { id: 'lorem', numberOfGuests: 0 }; - const component = shallow(); - expect(component).toMatchSnapshot('non-connected'); + const component = await shallowHookComponent(); + expect(component).toMatchSnapshot('basic render'); }); - it('should handle variations in data', () => { + it('should handle variations in data', async () => { const props = { id: 'lorem', numberOfGuests: 2, - listData: [ - { lorem: 'ipsum', dolor: 'sit' }, - { lorem: 'amet', dolor: 'amet' } - ] + useGetGuestsInventory: () => ({ + data: { + data: [ + { lorem: 'ipsum', dolor: 'sit' }, + { lorem: 'amet', dolor: 'amet' } + ] + } + }) }; - const component = shallow(); + const component = await shallowHookComponent(); expect(component).toMatchSnapshot('variable data'); component.setProps({ - filterGuestsData: [{ id: 'lorem' }] + useProductInventoryGuestsConfig: () => ({ + filters: [{ id: 'lorem', cellWidth: 20 }] + }) }); expect(component).toMatchSnapshot('filtered data'); }); - it('should handle multiple display states', () => { + it('should handle multiple display states', async () => { const props = { id: 'lorem', numberOfGuests: 1, - pending: true + useGetGuestsInventory: () => ({ + pending: true + }) }; - const component = shallow(); + const component = await shallowHookComponent(); expect(component).toMatchSnapshot('initial pending'); component.setProps({ - pending: false, - listData: [{ lorem: 'ipsum', dolor: 'sit' }] + useGetGuestsInventory: () => ({ + fulfilled: true, + data: { + data: [{ lorem: 'ipsum', dolor: 'sit' }] + } + }) }); expect(component).toMatchSnapshot('fulfilled'); - - component.setState({ currentPage: 1 }); - - component.setProps({ - pending: true, - listData: [] - }); - - expect(component).toMatchSnapshot('paged pending'); }); - it('should handle updating paging state', () => { + it('should handle an onScroll event', async () => { + const mockOnScroll = jest.fn(); const props = { id: 'lorem', - numberOfGuests: 111, - listData: [{ lorem: 'ipsum', dolor: 'sit' }] + numberOfGuests: 200, + useOnScroll: () => mockOnScroll, + useGetGuestsInventory: () => ({ + fulfilled: true, + data: { + data: [{ lorem: 'ipsum', dolor: 'sit' }] + } + }) }; - const component = shallow(); - const componentInstance = component.instance(); + const component = await shallowHookComponent(); + component + .find('.curiosity-table-scroll-list') + .simulate('scroll', { target: { scrollHeight: 200, scrollTop: 100, clientHeight: 100 } }); - const initialState = component.state(); - - componentInstance.onScroll({ target: { scrollHeight: 100, scrollTop: 20, clientHeight: 100 } }); - const scrollProgress = component.state(); - - componentInstance.onScroll({ target: { scrollHeight: 100, scrollTop: 0, clientHeight: 100 } }); - const scrollComplete = component.state(); + component.setProps({ + useGetGuestsInventory: () => ({ + pending: true + }) + }); - expect({ initialState, scrollProgress, scrollComplete }).toMatchSnapshot('state'); + expect(mockOnScroll.mock.calls).toMatchSnapshot('scroll event'); }); }); diff --git a/src/components/guestsList/__tests__/guestsListContext.test.js b/src/components/guestsList/__tests__/guestsListContext.test.js new file mode 100644 index 000000000..d659a0915 --- /dev/null +++ b/src/components/guestsList/__tests__/guestsListContext.test.js @@ -0,0 +1,76 @@ +import { context, useGetGuestsInventory, useOnScroll } from '../guestsListContext'; +import { RHSM_API_QUERY_SET_TYPES } from '../../../services/rhsm/rhsmConstants'; + +describe('InventorySubscriptionsContext', () => { + it('should return specific properties', () => { + expect(context).toMatchSnapshot('specific properties'); + }); + + it('should handle instances inventory API responses', () => { + const { result: errorResponse } = shallowHook(() => + useGetGuestsInventory('1234567890', { + useDispatch: () => {}, + useProductInventoryQuery: () => ({}), + useSelectorsInventory: () => ({ error: true }) + }) + ); + + expect(errorResponse).toMatchSnapshot('inventory, error'); + + const { result: pendingResponse } = shallowHook(() => + useGetGuestsInventory('1234567890', { + useDispatch: () => {}, + useProductInventoryQuery: () => ({}), + useSelectorsInventory: () => ({ pending: true }) + }) + ); + + expect(pendingResponse).toMatchSnapshot('inventory, pending'); + + const { result: cancelledResponse } = shallowHook(() => + useGetGuestsInventory('1234567890', { + useDispatch: () => {}, + useProductInventoryQuery: () => ({}), + useSelectorsInventory: () => ({ cancelled: true }) + }) + ); + + expect(cancelledResponse).toMatchSnapshot('inventory, cancelled'); + + const { result: fulfilledResponse } = shallowHook(() => + useGetGuestsInventory('1234567890', { + useDispatch: () => {}, + useProductInventoryQuery: () => ({}), + useSelectorsInventory: () => ({ fulfilled: true }) + }) + ); + + expect(fulfilledResponse).toMatchSnapshot('inventory, fulfilled'); + }); + + it('should handle an onScroll event', async () => { + const mockDispatch = jest.fn(); + const mockSuccessCallback = jest.fn(); + + const { unmount } = await mountHook(() => { + const onScroll = useOnScroll('1234567890', mockSuccessCallback, { + useDispatch: () => mockDispatch, + useProductInventoryQuery: () => ({ + [RHSM_API_QUERY_SET_TYPES.OFFSET]: 0, + [RHSM_API_QUERY_SET_TYPES.LIMIT]: 100 + }), + useSelectorsInventory: () => ({ pending: false, data: { meta: { count: 200 } } }) + }); + + onScroll({ target: { scrollHeight: 200, scrollTop: 100, clientHeight: 100 } }); + }); + + await unmount(); + + expect(mockDispatch.mock.calls).toMatchSnapshot('onPage event, dispatch'); + expect(mockSuccessCallback).toHaveBeenCalledTimes(1); + + mockDispatch.mockClear(); + mockSuccessCallback.mockClear(); + }); +}); diff --git a/src/components/guestsList/guestsList.deprecated.js b/src/components/guestsList/guestsList.deprecated.js new file mode 100644 index 000000000..812139d73 --- /dev/null +++ b/src/components/guestsList/guestsList.deprecated.js @@ -0,0 +1,267 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { TableVariant } from '@patternfly/react-table'; +import { helpers } from '../../common'; +import { apiQueries, connect, reduxActions, reduxSelectors } from '../../redux'; +import { Loader } from '../loader/loader'; +import { inventoryCardHelpers } from '../inventoryList/inventoryCardHelpers'; +import { RHSM_API_QUERY_TYPES } from '../../types/rhsmApiTypes'; +import { Table } from '../table/table'; + +/** + * ToDo: Consider removing the query prop entirely. + * The current API doesn't allow setting more than "offset" and "limit" + */ +/** + * ToDo: Review moving the "onScroll" layout into a standalone component. + */ +/** + * A system inventory guests component. + * + * @augments React.Component + * @fires onUpdateGuestsData + * @fires onScroll + */ +class GuestsList extends React.Component { + state = { currentPage: 0, limit: 100, previousData: [] }; + + componentDidMount() { + this.onUpdateGuestsData(); + } + + componentDidUpdate(prevProps, prevState) { + const { currentPage } = this.state; + + if (currentPage !== prevState.currentPage) { + this.onUpdateGuestsData(); + } + } + + /** + * Call the RHSM APIs, apply filters. + * + * @event onUpdateGuestsData + */ + onUpdateGuestsData = () => { + const { currentPage, limit } = this.state; + const { getHostsInventoryGuests, query, id } = this.props; + + if (id) { + const updatedQuery = { + ...query, + [RHSM_API_QUERY_TYPES.LIMIT]: limit, + [RHSM_API_QUERY_TYPES.OFFSET]: currentPage * limit || 0 + }; + + const { inventoryGuestsQuery } = apiQueries.parseRhsmQuery(updatedQuery); + getHostsInventoryGuests(id, inventoryGuestsQuery); + } + }; + + /** + * Update page state. + * + * @event onScroll + * @param {object} event + */ + onScroll = event => { + const { target } = event; + const { currentPage, limit, previousData } = this.state; + const { numberOfGuests, pending, listData } = this.props; + + const bottom = target.scrollHeight - target.scrollTop === target.clientHeight; + + if (numberOfGuests > (currentPage + 1) * limit && bottom && !pending) { + const newPage = currentPage + 1; + const updatedData = [...previousData, ...(listData || [])]; + + this.setState({ + previousData: updatedData, + currentPage: newPage + }); + } + }; + + renderLoader() { + const { currentPage } = this.state; + const { filterGuestsData, listData, pending } = this.props; + + if (currentPage > 0 && pending) { + const scrollLoader = ( + cellWidth)) || [], + rowCount: 0, + variant: TableVariant.compact + }} + /> + ); + + return
{scrollLoader}
; + } + + return null; + } + + /** + * ToDo: Consider moving the "meaning of life" into the default props on iteration. + * For everyone else... move the 42 into default props, possibly the 275. + */ + /** + * Render a guests table. + * + * @returns {Node} + */ + renderTable() { + const { previousData } = this.state; + const { filterGuestsData, listData, numberOfGuests, session } = this.props; + let updatedColumnHeaders = []; + + const updatedRows = [...previousData, ...(listData || [])].map(({ ...cellData }) => { + const { columnHeaders, cells } = inventoryCardHelpers.parseRowCellsListData({ + filters: filterGuestsData, + cellData, + session + }); + + updatedColumnHeaders = columnHeaders; + + return { + cells + }; + }); + + // Include the table header + let updatedHeight = (numberOfGuests + 1) * 42; + updatedHeight = (updatedHeight < 275 && updatedHeight) || 275; + + return ( +
+
+ {this.renderLoader()} + {(updatedRows.length && ( +
+ )) || + null} + + + ); + } + + /** + * Render a guest list table. + * + * @returns {Node} + */ + render() { + const { currentPage } = this.state; + const { error, filterGuestsData, listData, numberOfGuests, pending, perPageDefault } = this.props; + + return ( +
+ {pending && currentPage === 0 && ( + cellWidth)) || [], + rowCount: numberOfGuests < perPageDefault ? numberOfGuests : perPageDefault, + variant: TableVariant.compact + }} + /> + )} + {((!pending && currentPage === 0) || currentPage > 0) && this.renderTable()} +
+ ); + } +} + +/** + * Prop types. + * + * @type {{listData: Array, getHostsInventoryGuests: Function, session: object, filterGuestsData: object, + * pending: boolean, query: object, numberOfGuests: number, perPageDefault: number, id: string, + * error: boolean}} + */ +GuestsList.propTypes = { + error: PropTypes.bool, + filterGuestsData: 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 + ), + getHostsInventoryGuests: PropTypes.func, + listData: PropTypes.array, + id: PropTypes.string.isRequired, + numberOfGuests: PropTypes.number.isRequired, + pending: PropTypes.bool, + perPageDefault: PropTypes.number, + query: PropTypes.object, + session: PropTypes.object +}; + +/** + * Default props. + * + * @type {{listData: Array, getHostsInventoryGuests: Function, session: object, filterGuestsData: Array, + * pending: boolean, query: object, perPageDefault: number, error: boolean}} + */ +GuestsList.defaultProps = { + error: false, + filterGuestsData: [], + getHostsInventoryGuests: helpers.noop, + listData: [], + pending: false, + perPageDefault: 5, + query: {}, + session: {} +}; + +/** + * Apply actions to props. + * + * @param {Function} dispatch + * @returns {object} + */ +const mapDispatchToProps = dispatch => ({ + getHostsInventoryGuests: (id, query) => dispatch(reduxActions.rhsm.getHostsInventoryGuests(id, query)) +}); + +/** + * Create a selector from applied state, props. + * + * @type {Function} + */ +const makeMapStateToProps = reduxSelectors.guestsList.makeGuestsList(); + +const ConnectedGuestsList = connect(makeMapStateToProps, mapDispatchToProps)(GuestsList); + +export { ConnectedGuestsList as default, ConnectedGuestsList, GuestsList }; diff --git a/src/components/guestsList/guestsList.js b/src/components/guestsList/guestsList.js index 812139d73..30eebbd49 100644 --- a/src/components/guestsList/guestsList.js +++ b/src/components/guestsList/guestsList.js @@ -1,91 +1,59 @@ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { TableVariant } from '@patternfly/react-table'; -import { helpers } from '../../common'; -import { apiQueries, connect, reduxActions, reduxSelectors } from '../../redux'; +import { useProductInventoryGuestsConfig, useProductInventoryGuestsQuery } from '../productView/productViewContext'; +import { connect, reduxSelectors } from '../../redux'; import { Loader } from '../loader/loader'; import { inventoryCardHelpers } from '../inventoryList/inventoryCardHelpers'; -import { RHSM_API_QUERY_TYPES } from '../../types/rhsmApiTypes'; +import { RHSM_API_QUERY_SET_TYPES } from '../../services/rhsm/rhsmConstants'; import { Table } from '../table/table'; +import { useGetGuestsInventory, useOnScroll } from './guestsListContext'; -/** - * ToDo: Consider removing the query prop entirely. - * The current API doesn't allow setting more than "offset" and "limit" - */ -/** - * ToDo: Review moving the "onScroll" layout into a standalone component. - */ /** * A system inventory guests component. * - * @augments React.Component - * @fires onUpdateGuestsData + * @param {object} props + * @param {number} props.defaultPerPage + * @param {string} props.id + * @param {number} props.numberOfGuests + * @param {object} props.session + * @param {Function} props.useGetGuestsInventory + * @param {Function} props.useOnScroll + * @param {Function} props.useProductInventoryGuestsQuery + * @param {Function} props.useProductInventoryGuestsConfig * @fires onScroll + * @returns {Node} */ -class GuestsList extends React.Component { - state = { currentPage: 0, limit: 100, previousData: [] }; - - componentDidMount() { - this.onUpdateGuestsData(); - } - - componentDidUpdate(prevProps, prevState) { - const { currentPage } = this.state; - - if (currentPage !== prevState.currentPage) { - this.onUpdateGuestsData(); - } - } - - /** - * Call the RHSM APIs, apply filters. - * - * @event onUpdateGuestsData - */ - onUpdateGuestsData = () => { - const { currentPage, limit } = this.state; - const { getHostsInventoryGuests, query, id } = this.props; - - if (id) { - const updatedQuery = { - ...query, - [RHSM_API_QUERY_TYPES.LIMIT]: limit, - [RHSM_API_QUERY_TYPES.OFFSET]: currentPage * limit || 0 - }; - - const { inventoryGuestsQuery } = apiQueries.parseRhsmQuery(updatedQuery); - getHostsInventoryGuests(id, inventoryGuestsQuery); - } - }; +const GuestsList = ({ + defaultPerPage, + id, + numberOfGuests, + session, + useGetGuestsInventory: useAliasGetGuestsInventory, + useOnScroll: useAliasOnScroll, + useProductInventoryGuestsQuery: useAliasProductInventoryGuestsQuery, + useProductInventoryGuestsConfig: useAliasProductInventoryGuestsConfig +}) => { + const [previousData, setPreviousData] = useState([]); + const { filters: filterGuestsData } = useAliasProductInventoryGuestsConfig(); + + const query = useAliasProductInventoryGuestsQuery({ options: { overrideId: id } }); + const { [RHSM_API_QUERY_SET_TYPES.OFFSET]: currentPage } = query; + + const { error, pending, data = {} } = useAliasGetGuestsInventory(id); + const { data: listData = [] } = data; + + const onScroll = useAliasOnScroll(id, () => { + const updatedData = [...previousData, ...(listData || [])]; + setPreviousData(updatedData); + }); /** - * Update page state. + * Render a scroll table loader. * - * @event onScroll - * @param {object} event + * @returns {Node} */ - onScroll = event => { - const { target } = event; - const { currentPage, limit, previousData } = this.state; - const { numberOfGuests, pending, listData } = this.props; - - const bottom = target.scrollHeight - target.scrollTop === target.clientHeight; - - if (numberOfGuests > (currentPage + 1) * limit && bottom && !pending) { - const newPage = currentPage + 1; - const updatedData = [...previousData, ...(listData || [])]; - - this.setState({ - previousData: updatedData, - currentPage: newPage - }); - } - }; - - renderLoader() { - const { currentPage } = this.state; - const { filterGuestsData, listData, pending } = this.props; - + const renderLoader = () => { if (currentPage > 0 && pending) { const scrollLoader = ( { let updatedColumnHeaders = []; const updatedRows = [...previousData, ...(listData || [])].map(({ ...cellData }) => { @@ -134,6 +96,7 @@ class GuestsList extends React.Component { }; }); + // ToDo: Review having the height be a calc value // Include the table header let updatedHeight = (numberOfGuests + 1) * 42; updatedHeight = (updatedHeight < 275 && updatedHeight) || 275; @@ -142,9 +105,9 @@ class GuestsList extends React.Component {
- {this.renderLoader()} + {renderLoader()} {(updatedRows.length && (
); - } - - /** - * Render a guest list table. - * - * @returns {Node} - */ - render() { - const { currentPage } = this.state; - const { error, filterGuestsData, listData, numberOfGuests, pending, perPageDefault } = this.props; + }; - return ( -
- {pending && currentPage === 0 && ( - cellWidth)) || [], - rowCount: numberOfGuests < perPageDefault ? numberOfGuests : perPageDefault, - variant: TableVariant.compact - }} - /> - )} - {((!pending && currentPage === 0) || currentPage > 0) && this.renderTable()} -
- ); - } -} + return ( +
+ {pending && currentPage === 0 && ( + cellWidth)) || [], + rowCount: numberOfGuests < defaultPerPage ? numberOfGuests : defaultPerPage, + variant: TableVariant.compact + }} + /> + )} + {((!pending && currentPage === 0) || currentPage > 0) && renderTable()} +
+ ); +}; /** * Prop types. * - * @type {{listData: Array, getHostsInventoryGuests: Function, session: object, filterGuestsData: object, - * pending: boolean, query: object, numberOfGuests: number, perPageDefault: number, id: string, - * error: boolean}} + * @type {{useProductInventoryGuestsConfig: Function, session: object, useOnScroll: Function, numberOfGuests: number, + * id: string, useGetGuestsInventory: Function, useProductInventoryGuestsQuery: Function, defaultPerPage: number}} */ GuestsList.propTypes = { - error: PropTypes.bool, - filterGuestsData: 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 - ), - getHostsInventoryGuests: PropTypes.func, - listData: PropTypes.array, + defaultPerPage: PropTypes.number, id: PropTypes.string.isRequired, numberOfGuests: PropTypes.number.isRequired, - pending: PropTypes.bool, - perPageDefault: PropTypes.number, - query: PropTypes.object, - session: PropTypes.object + session: PropTypes.object, + useGetGuestsInventory: PropTypes.func, + useOnScroll: PropTypes.func, + useProductInventoryGuestsConfig: PropTypes.func, + useProductInventoryGuestsQuery: PropTypes.func }; /** * Default props. * - * @type {{listData: Array, getHostsInventoryGuests: Function, session: object, filterGuestsData: Array, - * pending: boolean, query: object, perPageDefault: number, error: boolean}} + * @type {{useProductInventoryGuestsConfig: Function, session: object, useOnScroll: Function, + * useGetGuestsInventory: Function, useProductInventoryGuestsQuery: Function, defaultPerPage: number}} */ GuestsList.defaultProps = { - error: false, - filterGuestsData: [], - getHostsInventoryGuests: helpers.noop, - listData: [], - pending: false, - perPageDefault: 5, - query: {}, - session: {} + defaultPerPage: 5, + session: {}, + useGetGuestsInventory, + useOnScroll, + useProductInventoryGuestsConfig, + useProductInventoryGuestsQuery }; -/** - * Apply actions to props. - * - * @param {Function} dispatch - * @returns {object} - */ -const mapDispatchToProps = dispatch => ({ - getHostsInventoryGuests: (id, query) => dispatch(reduxActions.rhsm.getHostsInventoryGuests(id, query)) -}); - /** * Create a selector from applied state, props. * * @type {Function} */ -const makeMapStateToProps = reduxSelectors.guestsList.makeGuestsList(); +const makeMapStateToProps = reduxSelectors.user.makeUserSession(); -const ConnectedGuestsList = connect(makeMapStateToProps, mapDispatchToProps)(GuestsList); +const ConnectedGuestsList = connect(makeMapStateToProps)(GuestsList); export { ConnectedGuestsList as default, ConnectedGuestsList, GuestsList }; diff --git a/src/components/guestsList/guestsListContext.js b/src/components/guestsList/guestsListContext.js new file mode 100644 index 000000000..583e1109d --- /dev/null +++ b/src/components/guestsList/guestsListContext.js @@ -0,0 +1,140 @@ +import { useUnmount, useShallowCompareEffect } from 'react-use'; +import { reduxActions, reduxTypes, storeHooks } from '../../redux'; +import { useProductInventoryGuestsQuery } from '../productView/productViewContext'; +import { RHSM_API_QUERY_SET_TYPES } from '../../services/rhsm/rhsmConstants'; + +/** + * Guests inventory selector response. + * + * @param {string} id + * @param {object} options + * @param {Function} options.useSelectorsResponse + * @returns {{data: (*|{}), pending: (*|boolean), fulfilled, error}} + */ +const useSelectorsGuestsInventory = ( + id, + { useSelectorsResponse: useAliasSelectorsResponse = storeHooks.reactRedux.useSelectorsResponse } = {} +) => { + const { error, cancelled, fulfilled, pending, data } = useAliasSelectorsResponse( + ({ inventory }) => inventory?.hostsGuests?.[id] + ); + + return { + error, + fulfilled, + pending: pending || cancelled || false, + data: (data?.length === 1 && data[0]) || data || {} + }; +}; + +/** + * Combined Redux RHSM Actions, getHostsInventoryGuests, and inventory selector response. + * + * @param {string} id + * @param {object} options + * @param {Function} options.getInventory + * @param {Function} options.useDispatch + * @param {Function} options.useProductInventoryQuery + * @param {Function} options.useSelectorsInventory + * @returns {Function} + */ +const useGetGuestsInventory = ( + id, + { + getInventory = reduxActions.rhsm.getHostsInventoryGuests, + useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, + useProductInventoryQuery: useAliasProductInventoryQuery = useProductInventoryGuestsQuery, + useSelectorsInventory: useAliasSelectorsInventory = useSelectorsGuestsInventory + } = {} +) => { + const query = useAliasProductInventoryQuery({ options: { overrideId: id } }); + const dispatch = useAliasDispatch(); + const response = useAliasSelectorsInventory(id); + + useShallowCompareEffect(() => { + getInventory(id, query)(dispatch); + }, [dispatch, id, query]); + + return { + ...response + }; +}; + +/** + * Use paging as onScroll event for guests inventory. + * + * @param {string} id + * @param {Function} successCallback + * @param {object} options + * @param {Function} options.useDispatch + * @param {Function} options.useSelectorsInventory + * @param {Function} options.useProductInventoryQuery + * @returns {Function} + */ +const useOnScroll = ( + id, + successCallback, + { + useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, + useSelectorsInventory: useAliasSelectorsInventory = useSelectorsGuestsInventory, + useProductInventoryQuery: useAliasProductInventoryQuery = useProductInventoryGuestsQuery + } = {} +) => { + const dispatch = useAliasDispatch(); + const { pending, data = {} } = useAliasSelectorsInventory(id); + const { count: numberOfGuests } = data?.meta || {}; + + const query = useAliasProductInventoryQuery({ options: { overrideId: id } }); + const { [RHSM_API_QUERY_SET_TYPES.LIMIT]: limit, [RHSM_API_QUERY_SET_TYPES.OFFSET]: currentPage } = query; + + /** + * Reset paging in scenarios where inventory is filtered, or guests is collapsed. + */ + useUnmount(() => { + dispatch([ + { + type: reduxTypes.query.SET_QUERY_CLEAR_INVENTORY_GUESTS_LIST, + viewId: id + } + ]); + }); + + /** + * On scroll, dispatch type. + * + * @event onScroll + * @param {object} event + * @returns {void} + */ + return event => { + const { target } = event; + const bottom = target.scrollHeight - target.scrollTop === target.clientHeight; + + if (numberOfGuests > (currentPage + 1) * limit && bottom && !pending) { + if (typeof successCallback === 'function') { + successCallback(event); + } + + dispatch([ + { + type: reduxTypes.query.SET_QUERY_RHSM_GUESTS_INVENTORY_TYPES[RHSM_API_QUERY_SET_TYPES.OFFSET], + viewId: id, + [RHSM_API_QUERY_SET_TYPES.OFFSET]: currentPage + 1 + }, + { + type: reduxTypes.query.SET_QUERY_RHSM_GUESTS_INVENTORY_TYPES[RHSM_API_QUERY_SET_TYPES.LIMIT], + viewId: id, + [RHSM_API_QUERY_SET_TYPES.LIMIT]: limit + } + ]); + } + }; +}; + +const context = { + useGetGuestsInventory, + useOnScroll, + useSelectorsGuestsInventory +}; + +export { context as default, context, useGetGuestsInventory, useOnScroll, useSelectorsGuestsInventory }; diff --git a/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap b/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap index 16e04eb7a..7dd70598a 100644 --- a/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap +++ b/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap @@ -192,15 +192,6 @@ Array [ }, ], }, - Object { - "file": "./src/components/inventorySubscriptions/inventorySubscriptions.js", - "keys": Array [ - Object { - "key": "curiosity-inventory.tab", - "match": "t('curiosity-inventory.tab', { context: 'disabled' })", - }, - ], - }, Object { "file": "./src/components/inventoryTabs/inventoryTabs.js", "keys": Array [ @@ -709,10 +700,6 @@ Array [ "file": "./src/components/inventoryList/inventoryList.deprecated.js", "key": "curiosity-inventory.tab", }, - Object { - "file": "./src/components/inventorySubscriptions/inventorySubscriptions.js", - "key": "curiosity-inventory.tab", - }, Object { "file": "./src/components/inventoryTabs/inventoryTabs.js", "key": "curiosity-inventory.tabHeading", diff --git a/src/components/inventoryList/__tests__/__snapshots__/inventoryCard.test.js.snap b/src/components/inventoryList/__tests__/__snapshots__/inventoryCard.test.js.snap index 7c38ccb92..0346ca7a9 100644 --- a/src/components/inventoryList/__tests__/__snapshots__/inventoryCard.test.js.snap +++ b/src/components/inventoryList/__tests__/__snapshots__/inventoryCard.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`InventoryList Component should handle expandable guests data: number of guests 1`] = ` +exports[`InventoryCard Component should handle expandable guests data: number of guests 1`] = `
`; -exports[`InventoryList Component should handle expandable guests data: number of guests, and id 1`] = ` +exports[`InventoryCard Component should handle expandable guests data: number of guests, and id 1`] = `
, }, ] @@ -74,7 +67,7 @@ exports[`InventoryList Component should handle expandable guests data: number of /> `; -exports[`InventoryList Component should handle expandable guests data: number of guests, id, and NO expandable guests display 1`] = ` +exports[`InventoryCard Component should handle expandable guests data: number of guests, id, and NO expandable guests display 1`] = `
`; -exports[`InventoryList Component should handle multiple display states, error, pending, fulfilled: error 1`] = ` +exports[`InventoryCard Component should handle multiple display states, error, pending, fulfilled: error 1`] = ` @@ -127,7 +120,6 @@ exports[`InventoryList Component should handle multiple display states, error, p useDispatch={[Function]} useProduct={[Function]} useProductInventoryHostsQuery={[Function]} - viewId="inventoryInstancesList" /> `; -exports[`InventoryList Component should handle multiple display states, error, pending, fulfilled: fulfilled 1`] = ` +exports[`InventoryCard Component should handle multiple display states, error, pending, fulfilled: fulfilled 1`] = ` @@ -222,7 +214,6 @@ exports[`InventoryList Component should handle multiple display states, error, p useDispatch={[Function]} useProduct={[Function]} useProductInventoryHostsQuery={[Function]} - viewId="inventoryInstancesList" /> `; -exports[`InventoryList Component should handle multiple display states, error, pending, fulfilled: pending 1`] = ` +exports[`InventoryCard Component should handle multiple display states, error, pending, fulfilled: pending 1`] = ` @@ -339,7 +330,6 @@ exports[`InventoryList Component should handle multiple display states, error, p useDispatch={[Function]} useProduct={[Function]} useProductInventoryHostsQuery={[Function]} - viewId="inventoryInstancesList" /> `; -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 [ - 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[`InventoryCard Component should handle variations in data: filtered data 1`] = ` @@ -565,7 +430,6 @@ exports[`InventoryList Component should handle variations in data: filtered data useDispatch={[Function]} useProduct={[Function]} useProductInventoryHostsQuery={[Function]} - viewId="inventoryInstancesList" /> `; -exports[`InventoryList Component should handle variations in data: variable data 1`] = ` +exports[`InventoryCard Component should handle variations in data: variable data 1`] = ` @@ -686,7 +550,6 @@ exports[`InventoryList Component should handle variations in data: variable data useDispatch={[Function]} useProduct={[Function]} useProductInventoryHostsQuery={[Function]} - viewId="inventoryInstancesList" /> `; -exports[`InventoryList Component should render a non-connected component: non-connected 1`] = ` +exports[`InventoryCard Component should render a basic component: basic render 1`] = ` @@ -803,7 +666,6 @@ exports[`InventoryList Component should render a non-connected component: non-co useDispatch={[Function]} useProduct={[Function]} useProductInventoryHostsQuery={[Function]} - viewId="inventoryInstancesList" />
-
@@ -864,7 +732,7 @@ exports[`InventoryList Component should render a non-connected component: non-co `; -exports[`InventoryList Component should return an empty render when disabled: disabled component 1`] = ` +exports[`InventoryCard Component should return an empty render when disabled: disabled component 1`] = ` diff --git a/src/components/inventoryList/__tests__/__snapshots__/inventoryCardContext.test.js.snap b/src/components/inventoryList/__tests__/__snapshots__/inventoryCardContext.test.js.snap new file mode 100644 index 000000000..2b116eaee --- /dev/null +++ b/src/components/inventoryList/__tests__/__snapshots__/inventoryCardContext.test.js.snap @@ -0,0 +1,106 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InventoryCardContext should handle an onColumnSort event: onColumnSort event, dispatch 1`] = ` +Array [ + Array [ + Array [ + Object { + "dir": "desc", + "type": "SET_QUERY_RHSM_HOSTS_INVENTORY_dir", + "viewId": "lorem", + }, + Object { + "sort": "loremIpsumColumnOne", + "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": "loremIpsumColumnOne", + "type": "SET_QUERY_RHSM_HOSTS_INVENTORY_sort", + "viewId": "lorem", + }, + ], + ], +] +`; + +exports[`InventoryCardContext should handle an onPage event: onPage event, dispatch 1`] = ` +Array [ + Array [ + Array [ + Object { + "offset": 1, + "type": "SET_QUERY_RHSM_HOSTS_INVENTORY_offset", + "viewId": "lorem", + }, + Object { + "limit": 5, + "type": "SET_QUERY_RHSM_HOSTS_INVENTORY_limit", + "viewId": "lorem", + }, + ], + ], +] +`; + +exports[`InventoryCardContext should handle instances inventory API responses: inventory, cancelled 1`] = ` +Object { + "data": Object {}, + "error": undefined, + "fulfilled": undefined, + "pending": true, +} +`; + +exports[`InventoryCardContext should handle instances inventory API responses: inventory, disabled 1`] = ` +Object { + "data": Object {}, + "error": undefined, + "fulfilled": undefined, + "pending": false, +} +`; + +exports[`InventoryCardContext should handle instances inventory API responses: inventory, error 1`] = ` +Object { + "data": Object {}, + "error": true, + "fulfilled": undefined, + "pending": false, +} +`; + +exports[`InventoryCardContext should handle instances inventory API responses: inventory, fulfilled 1`] = ` +Object { + "data": Object {}, + "error": undefined, + "fulfilled": true, + "pending": false, +} +`; + +exports[`InventoryCardContext should handle instances inventory API responses: inventory, pending 1`] = ` +Object { + "data": Object {}, + "error": undefined, + "fulfilled": undefined, + "pending": true, +} +`; + +exports[`InventoryCardContext should return specific properties: specific properties 1`] = ` +Object { + "useGetInstancesInventory": [Function], + "useOnColumnSortInstances": [Function], + "useOnPageInstances": [Function], +} +`; diff --git a/src/components/inventoryList/__tests__/inventoryCard.test.js b/src/components/inventoryList/__tests__/inventoryCard.test.js index b882a1875..473b7be3a 100644 --- a/src/components/inventoryList/__tests__/inventoryCard.test.js +++ b/src/components/inventoryList/__tests__/inventoryCard.test.js @@ -1,213 +1,178 @@ import React from 'react'; -import { shallow } from 'enzyme'; -import { SortByDirection } from '@patternfly/react-table'; import Table from '../../table/table'; -import { InventoryList } from '../inventoryCard'; -import { store } from '../../../redux'; +import { InventoryCard } from '../inventoryCard'; import { RHSM_API_QUERY_TYPES } from '../../../types/rhsmApiTypes'; -describe('InventoryList Component', () => { - let mockDispatch; - - beforeEach(() => { - mockDispatch = jest.spyOn(store, 'dispatch').mockImplementation((type, data) => ({ type, data })); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should render a non-connected component', () => { +describe('InventoryCard Component', () => { + it('should render a basic component', async () => { const props = { - query: { + useProduct: () => ({ productId: 'lorem' }), + useProductInventoryQuery: () => ({ [RHSM_API_QUERY_TYPES.LIMIT]: 10, [RHSM_API_QUERY_TYPES.OFFSET]: 0 - }, - productId: 'lorem' + }) }; - const component = shallow(); - expect(component).toMatchSnapshot('non-connected'); + const component = await shallowHookComponent(); + expect(component).toMatchSnapshot('basic render'); }); - it('should return an empty render when disabled', () => { + it('should return an empty render when disabled', async () => { const props = { - query: { + isDisabled: true, + useProduct: () => ({ productId: 'lorem' }), + useProductInventoryQuery: () => ({ [RHSM_API_QUERY_TYPES.LIMIT]: 10, [RHSM_API_QUERY_TYPES.OFFSET]: 0 - }, - productId: 'lorem', - data: [ - { lorem: 'ipsum', dolor: 'sit' }, - { lorem: 'sit', dolor: 'amet' } - ], - meta: { - count: 2 - }, - isDisabled: true + }), + useGetInventory: () => ({ + data: { + data: [ + { lorem: 'ipsum', dolor: 'sit' }, + { lorem: 'sit', dolor: 'amet' } + ], + meta: { + count: 2 + } + } + }) }; - const component = shallow(); + const component = await shallowHookComponent(); expect(component).toMatchSnapshot('disabled component'); }); - it('should handle multiple display states, error, pending, fulfilled', () => { + it('should handle multiple display states, error, pending, fulfilled', async () => { const props = { - query: { + useProduct: () => ({ productId: 'lorem' }), + useProductInventoryQuery: () => ({ lorem: 'ipsum' - }, - productId: 'lorem', - pending: true + }), + useGetInventory: () => ({ + pending: true + }) }; - const component = shallow(); + const component = await shallowHookComponent(); expect(component).toMatchSnapshot('pending'); component.setProps({ - pending: false, - error: true + useGetInventory: () => ({ + 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 + useGetInventory: () => ({ + pending: false, + error: false, + fulfilled: true, + data: { + data: [ + { lorem: 'ipsum', dolor: 'sit' }, + { lorem: 'sit', dolor: 'amet' } + ], + meta: { + count: 2 + } + } + }) }); expect(component).toMatchSnapshot('fulfilled'); }); - it('should handle variations in data', () => { + it('should handle variations in data', async () => { const props = { - query: { + useProduct: () => ({ productId: 'lorem' }), + useProductInventoryQuery: () => ({ [RHSM_API_QUERY_TYPES.LIMIT]: 10, [RHSM_API_QUERY_TYPES.OFFSET]: 0 - }, - productId: 'lorem', - data: [ - { lorem: 'ipsum', dolor: 'sit' }, - { lorem: 'sit', dolor: 'amet' } - ], - meta: { - count: 2 - } + }), + useGetInventory: () => ({ + fulfilled: true, + data: { + data: [ + { lorem: 'ipsum', dolor: 'sit' }, + { lorem: 'sit', dolor: 'amet' } + ], + meta: { + count: 2 + } + } + }) }; - const component = shallow(); + const component = await shallowHookComponent(); expect(component).toMatchSnapshot('variable data'); component.setProps({ - filterInventoryData: [{ id: 'lorem' }] + useProductInventoryConfig: () => ({ filters: [{ id: 'lorem' }] }) }); expect(component).toMatchSnapshot('filtered data'); }); - it('should handle expandable guests data', () => { + it('should handle expandable guests data', async () => { const props = { - query: { + useProduct: () => ({ productId: 'lorem' }), + useProductInventoryQuery: () => ({ [RHSM_API_QUERY_TYPES.LIMIT]: 10, [RHSM_API_QUERY_TYPES.OFFSET]: 0 - }, - productId: 'lorem', - data: [{ lorem: 'sit', dolor: 'amet', numberOfGuests: 1 }], - meta: { - count: 1 - } + }), + useGetInventory: () => ({ + fulfilled: true, + data: { + data: [{ lorem: 'sit', dolor: 'amet', numberOfGuests: 1 }], + meta: { + count: 1 + } + } + }) }; - const component = shallow(); + const component = await shallowHookComponent(); expect(component.find(Table)).toMatchSnapshot('number of guests'); component.setProps({ ...props, - data: [{ lorem: 'sit', dolor: 'amet', numberOfGuests: 1, subscriptionManagerId: 'loremIpsum' }] + useGetInventory: () => ({ + fulfilled: true, + data: { + data: [{ lorem: 'sit', dolor: 'amet', numberOfGuests: 1, subscriptionManagerId: 'loremIpsum' }], + meta: { + count: 1 + } + } + }) }); expect(component.find(Table)).toMatchSnapshot('number of guests, and id'); component.setProps({ ...props, - data: [{ lorem: 'sit', dolor: 'amet', numberOfGuests: 2, subscriptionManagerId: 'loremIpsum' }], - settings: { - hasGuests: data => { - const { numberOfGuests = 0, subscriptionManagerId = null } = data; - return numberOfGuests > 2 && subscriptionManagerId; + useGetInventory: () => ({ + fulfilled: true, + data: { + data: [{ lorem: 'sit', dolor: 'amet', numberOfGuests: 2, subscriptionManagerId: 'loremIpsum' }], + meta: { + count: 1 + } + } + }), + useProductInventoryConfig: () => ({ + settings: { + hasSubTable: 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', - data: [ - { lorem: 'ipsum', dolor: 'sit' }, - { lorem: 'sit', dolor: 'amet' } - ], - meta: { - count: 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'); - }); - - 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/__tests__/inventoryCardContext.test.js b/src/components/inventoryList/__tests__/inventoryCardContext.test.js new file mode 100644 index 000000000..5ce26fddb --- /dev/null +++ b/src/components/inventoryList/__tests__/inventoryCardContext.test.js @@ -0,0 +1,104 @@ +import { + context, + useGetInstancesInventory, + useOnPageInstances, + useOnColumnSortInstances +} from '../inventoryCardContext'; +import { RHSM_API_QUERY_INVENTORY_SORT_DIRECTION_TYPES as SORT_DIRECTION_TYPES } from '../../../services/rhsm/rhsmConstants'; + +describe('InventoryCardContext', () => { + it('should return specific properties', () => { + expect(context).toMatchSnapshot('specific properties'); + }); + + it('should handle instances inventory API responses', async () => { + const { result: errorResponse } = shallowHook(() => + useGetInstancesInventory({ + useProduct: () => ({ productId: 'lorem' }), + useDispatch: () => {}, + useProductInventoryQuery: () => ({}), + useSelectorsResponse: () => ({ error: true }) + }) + ); + + expect(errorResponse).toMatchSnapshot('inventory, error'); + + const { result: pendingResponse } = shallowHook(() => + useGetInstancesInventory({ + useProduct: () => ({ productId: 'lorem' }), + useDispatch: () => {}, + useProductInventoryQuery: () => ({}), + useSelectorsResponse: () => ({ pending: true }) + }) + ); + + expect(pendingResponse).toMatchSnapshot('inventory, pending'); + + const { result: cancelledResponse } = shallowHook(() => + useGetInstancesInventory({ + useProduct: () => ({ productId: 'lorem' }), + useDispatch: () => {}, + useProductInventoryQuery: () => ({}), + useSelectorsResponse: () => ({ cancelled: true }) + }) + ); + + expect(cancelledResponse).toMatchSnapshot('inventory, cancelled'); + + const mockFulfilledGetInventory = jest.fn(); + const { result: fulfilledResponse } = await mountHook(() => + useGetInstancesInventory({ + getInventory: () => mockFulfilledGetInventory, + useProduct: () => ({ productId: 'lorem' }), + useDispatch: () => {}, + useProductInventoryQuery: () => ({}), + useSelectorsResponse: () => ({ fulfilled: true }) + }) + ); + + expect(mockFulfilledGetInventory).toHaveBeenCalledTimes(1); + expect(fulfilledResponse).toMatchSnapshot('inventory, fulfilled'); + + const mockDisabledGetInventory = jest.fn(); + const { result: disabledResponse } = await mountHook(() => + useGetInstancesInventory({ + isDisabled: true, + getInventory: () => mockDisabledGetInventory, + useProduct: () => ({ productId: 'lorem' }), + useDispatch: () => {}, + useProductInventoryQuery: () => ({}), + useSelectorsResponse: () => ({}) + }) + ); + + expect(mockDisabledGetInventory).toHaveBeenCalledTimes(0); + expect(disabledResponse).toMatchSnapshot('inventory, disabled'); + }); + + it('should handle an onPage event', () => { + const mockDispatch = jest.fn(); + const onPage = useOnPageInstances({ + useDispatch: () => mockDispatch, + useProduct: () => ({ productId: 'lorem' }) + }); + + onPage({ offset: 1, perPage: 5 }); + expect(mockDispatch.mock.calls).toMatchSnapshot('onPage event, dispatch'); + mockDispatch.mockClear(); + }); + + it('should handle an onColumnSort event', () => { + const mockDispatch = jest.fn(); + const onColumnSort = useOnColumnSortInstances({ + sortColumns: { LOREM_IPSUM_COLUMN_ONE: 'loremIpsumColumnOne' }, + useDispatch: () => mockDispatch, + useProduct: () => ({ productId: 'lorem' }) + }); + + onColumnSort(null, { direction: SORT_DIRECTION_TYPES.DESCENDING, id: 'loremIpsumColumnOne' }); + onColumnSort(null, { direction: SORT_DIRECTION_TYPES.ASCENDING, id: 'loremIpsumColumnOne' }); + + expect(mockDispatch.mock.calls).toMatchSnapshot('onColumnSort event, dispatch'); + mockDispatch.mockClear(); + }); +}); diff --git a/src/components/inventoryList/inventoryCard.js b/src/components/inventoryList/inventoryCard.js index 02569b5a1..4c13e4fe5 100644 --- a/src/components/inventoryList/inventoryCard.js +++ b/src/components/inventoryList/inventoryCard.js @@ -1,12 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -import _isEqual from 'lodash/isEqual'; -import { SortByDirection, TableVariant } from '@patternfly/react-table'; +import { 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 { useProductInventoryHostsConfig, useProductInventoryHostsQuery } from '../productView/productViewContext'; import { helpers } from '../../common'; -import { connect, reduxActions, reduxSelectors, reduxTypes, store } from '../../redux'; +import { connect, reduxSelectors } from '../../redux'; import Table from '../table/table'; import { Loader } from '../loader/loader'; import { MinHeight } from '../minHeight/minHeight'; @@ -15,142 +14,83 @@ import { inventoryCardHelpers } from './inventoryCardHelpers'; import Pagination from '../pagination/pagination'; import { ToolbarFieldDisplayName } from '../toolbar/toolbarFieldDisplayName'; import { paginationHelpers } from '../pagination/paginationHelpers'; -import { - 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 { RHSM_API_QUERY_SET_TYPES } from '../../services/rhsm/rhsmConstants'; import { translate } from '../i18n/i18n'; +import { useGetInstancesInventory, useOnPageInstances, useOnColumnSortInstances } from './inventoryCardContext'; /** - * 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. + * Set up inventory cards. Expand filters with base settings. * - * @augments React.Component + * @param {object} props + * @param {Node} props.cardActions + * @param {boolean} props.isDisabled + * @param {number} props.perPageDefault + * @param {Function} props.t + * @param {object} props.session + * @param {Function} props.useGetInventory + * @param {Function} props.useOnPage + * @param {Function} props.useOnColumnSort + * @param {Function} props.useProductInventoryConfig + * @param {Function} props.useProductInventoryQuery * @fires onColumnSort * @fires onPage * @fires onUpdateInventoryData + * @returns {Node} */ -class InventoryList extends React.Component { - componentDidMount() { - this.onUpdateInventoryData(); +const InventoryCard = ({ + cardActions, + isDisabled, + perPageDefault, + t, + session, + useGetInventory: useAliasGetInventory, + useOnPage: useAliasOnPage, + useOnColumnSort: useAliasOnColumnSort, + useProductInventoryConfig: useAliasProductInventoryConfig, + useProductInventoryQuery: useAliasProductInventoryQuery +}) => { + const query = useAliasProductInventoryQuery(); + const onPage = useAliasOnPage(); + const onColumnSort = useAliasOnColumnSort(); + const { filters: filterInventoryData, settings } = useAliasProductInventoryConfig(); + const { error, fulfilled, pending, data = {} } = useAliasGetInventory({ isDisabled }); + const { data: listData = [], meta = {} } = data; + + if (isDisabled) { + return ( + + + {t('curiosity-inventory.tab', { context: 'disabled' })} + + + ); } - componentDidUpdate(prevProps) { - const { productId, query } = this.props; - - if (productId !== prevProps.productId || !_isEqual(query, prevProps.query)) { - this.onUpdateInventoryData(); - } - } + 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); - /** - * 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 => value === id || _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_SET_TYPES.DIRECTION], - viewId: productId, - [RHSM_API_QUERY_SET_TYPES.DIRECTION]: updatedDirection - }, - { - type: reduxTypes.query.SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES[RHSM_API_QUERY_SET_TYPES.SORT], - viewId: productId, - [RHSM_API_QUERY_SET_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_SET_TYPES.OFFSET], - viewId: productId, - [RHSM_API_QUERY_SET_TYPES.OFFSET]: offset - }, - { - type: reduxTypes.query.SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES[RHSM_API_QUERY_SET_TYPES.LIMIT], - viewId: productId, - [RHSM_API_QUERY_SET_TYPES.LIMIT]: perPage - } - ]); - }; - - /** - * Call the RHSM APIs, apply filters. - * - * @event onUpdateInventoryData - */ - onUpdateInventoryData = () => { - const { getInstancesInventory, isDisabled, productId, query } = this.props; - - if (!isDisabled && productId) { - getInstancesInventory(productId, query); - } - }; + // 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}`; /** * Render an inventory table. * * @returns {Node} */ - renderTable() { - const { filterGuestsData, filterInventoryData, data: listData, query, session, settings } = this.props; + const renderTable = () => { let updatedColumnHeaders = []; const updatedRows = listData.map(({ ...cellData }) => { const { columnHeaders, cells } = inventoryCardHelpers.parseRowCellsListData({ filters: inventoryCardHelpers.parseInventoryFilters({ filters: filterInventoryData, - onSort: this.onColumnSort, + onSort: onColumnSort, query }), cellData, @@ -158,28 +98,21 @@ class InventoryList extends React.Component { }); 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 }); + const subscriptionManagerId = cellData?.subscriptionManagerId; + const numberOfGuests = cellData?.numberOfGuests; + let isSubTable; + + // Is there a subTable, callback, or attempt to determine, return boolean + if (typeof settings?.hasSubTable === 'function') { + isSubTable = settings.hasSubTable({ ...cellData }, { ...session }); + } else { + isSubTable = numberOfGuests > 0 && subscriptionManagerId; } return { cells, expandedContent: - (hasGuests && ( - - )) || - undefined + (isSubTable && ) || undefined }; }); @@ -192,203 +125,119 @@ class InventoryList extends React.Component { rows={updatedRows} /> ); - } - - /** - * Render an inventory card. - * - * @returns {Node} - */ - render() { - const { - error, - filterInventoryData, - fulfilled, - isDisabled, - data: listData, - meta, - pending, - perPageDefault, - query, - t, - viewId - } = this.props; - - if (isDisabled) { - return ( - - - {t('curiosity-inventory.tab', { context: 'disabled' })} - - - ); - } - - 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 - 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()} -
-
-
- - - - + + + {cardActions} + + + + + + + +
+ {pending && ( + cellWidth)) || [], + rowCount: listData?.length || updatedPerPage, + variant: TableVariant.compact + }} /> - - - - - ); - } -} + )} + {!pending && renderTable()} +
+
+
+ + + + + + + +
+ ); +}; /** * Prop types. * - * @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}} + * @type {{useOnPage: Function, t: Function, session: object, perPageDefault: number, isDisabled: boolean, + * useProductInventoryConfig: Function, useGetInventory: Function, useOnColumnSort: Function, + * useProductInventoryQuery: Function}} */ -InventoryList.propTypes = { - data: PropTypes.array, - 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 - ), - getInstancesInventory: PropTypes.func, +InventoryCard.propTypes = { + cardActions: PropTypes.node, isDisabled: PropTypes.bool, - meta: PropTypes.shape({ count: PropTypes.number }), - pending: PropTypes.bool, perPageDefault: PropTypes.number, - productId: PropTypes.string.isRequired, - query: PropTypes.object.isRequired, session: PropTypes.object, - settings: PropTypes.shape({ - hasGuests: PropTypes.func - }), t: PropTypes.func, - viewId: PropTypes.string + useGetInventory: PropTypes.func, + useOnPage: PropTypes.func, + useOnColumnSort: PropTypes.func, + useProductInventoryConfig: PropTypes.func, + useProductInventoryQuery: PropTypes.func }; /** * Default props. * - * @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}} + * @type {{useOnPage: Function, t: Function, session: object, perPageDefault: number, isDisabled: boolean, + * useProductInventoryConfig: Function, useGetInventory: Function, useOnColumnSort: Function, + * useProductInventoryQuery: Function}} */ -InventoryList.defaultProps = { - data: [], - error: false, - fulfilled: false, - filterGuestsData: [], - filterInventoryData: [], - getInstancesInventory: helpers.noop, +InventoryCard.defaultProps = { + cardActions: ( + + + + ), isDisabled: helpers.UI_DISABLED_TABLE_INSTANCES, - meta: { - count: 0 - }, - pending: false, perPageDefault: 10, session: {}, - settings: {}, t: translate, - viewId: 'inventoryInstancesList' + useGetInventory: useGetInstancesInventory, + useOnPage: useOnPageInstances, + useOnColumnSort: useOnColumnSortInstances, + useProductInventoryConfig: useProductInventoryHostsConfig, + useProductInventoryQuery: useProductInventoryHostsQuery }; -/** - * Apply actions to props. - * - * @param {Function} dispatch - * @returns {object} - */ -const mapDispatchToProps = dispatch => ({ - getInstancesInventory: (id, query) => dispatch(reduxActions.rhsm.getInstancesInventory(id, query)) -}); - /** * Create a selector from applied state, props. * * @type {Function} */ -const makeMapStateToProps = reduxSelectors.instancesList.makeInstancesList(); +const makeMapStateToProps = reduxSelectors.user.makeUserSession(); -const ConnectedInventoryList = connect(makeMapStateToProps, mapDispatchToProps)(InventoryList); +const ConnectedInventoryCard = connect(makeMapStateToProps)(InventoryCard); -export { ConnectedInventoryList as default, ConnectedInventoryList, InventoryList }; +export { ConnectedInventoryCard as default, ConnectedInventoryCard, InventoryCard }; diff --git a/src/components/inventoryList/inventoryCardContext.js b/src/components/inventoryList/inventoryCardContext.js new file mode 100644 index 000000000..2655cdc55 --- /dev/null +++ b/src/components/inventoryList/inventoryCardContext.js @@ -0,0 +1,162 @@ +import { useShallowCompareEffect } from 'react-use'; +import _camelCase from 'lodash/camelCase'; +import { SortByDirection } from '@patternfly/react-table'; +import { reduxActions, reduxTypes, storeHooks } from '../../redux'; +import { useProduct, useProductInventoryHostsQuery } from '../productView/productViewContext'; +import { + 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 { helpers } from '../../common'; + +/** + * Combined Redux RHSM Actions, getInstancesInventory, and inventory selector response. + * + * @param {object} options + * @param {boolean} options.isDisabled + * @param {Function} options.getInventory + * @param {Function} options.useDispatch + * @param {Function} options.useProduct + * @param {Function} options.useProductInventoryQuery + * @param {Function} options.useSelectorsResponse + * @returns {Function} + */ +const useGetInstancesInventory = ({ + isDisabled = false, + getInventory = reduxActions.rhsm.getInstancesInventory, + useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, + useProduct: useAliasProduct = useProduct, + useProductInventoryQuery: useAliasProductInventoryQuery = useProductInventoryHostsQuery, + useSelectorsResponse: useAliasSelectorsResponse = storeHooks.reactRedux.useSelectorsResponse +} = {}) => { + const { productId } = useAliasProduct(); + const query = useAliasProductInventoryQuery(); + const dispatch = useAliasDispatch(); + const { error, cancelled, fulfilled, pending, data } = useAliasSelectorsResponse( + ({ inventory }) => inventory?.instancesInventory?.[productId] + ); + + useShallowCompareEffect(() => { + if (!isDisabled) { + getInventory(productId, query)(dispatch); + } + }, [dispatch, isDisabled, productId, query]); + + return { + error, + fulfilled, + pending: pending || cancelled || false, + data: (data?.length === 1 && data[0]) || data || {} + }; +}; + +/** + * An onPage callback for instances inventory. + * + * @param {object} options + * @param {Function} options.useDispatch + * @param {Function} options.useProduct + * @returns {Function} + */ +const useOnPageInstances = ({ + useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, + useProduct: useAliasProduct = useProduct +} = {}) => { + const { productId } = useAliasProduct(); + const dispatch = useAliasDispatch(); + + /** + * On event update state for instances inventory. + * + * @event onPage + * @param {object} params + * @param {number} params.offset + * @param {number} params.perPage + * @returns {void} + */ + return ({ offset, perPage }) => { + dispatch([ + { + type: reduxTypes.query.SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES[RHSM_API_QUERY_SET_TYPES.OFFSET], + viewId: productId, + [RHSM_API_QUERY_SET_TYPES.OFFSET]: offset + }, + { + type: reduxTypes.query.SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES[RHSM_API_QUERY_SET_TYPES.LIMIT], + viewId: productId, + [RHSM_API_QUERY_SET_TYPES.LIMIT]: perPage + } + ]); + }; +}; + +/** + * An onColumnSort callback for instances inventory. + * + * @param {object} options + * @param {object} options.sortColumns + * @param {Function} options.useDispatch + * @param {Function} options.useProduct + * @returns {Function} + */ +const useOnColumnSortInstances = ({ + sortColumns = SORT_TYPES, + useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, + useProduct: useAliasProduct = useProduct +} = {}) => { + const { productId } = useAliasProduct(); + const dispatch = useAliasDispatch(); + + /** + * On event update state for instances inventory. + * + * @event onColumnSort + * @param {*} _data + * @param {object} params + * @param {string} params.direction + * @param {string} params.id + * @returns {void} + */ + return (_data, { direction, id }) => { + const updatedSortColumn = Object.values(sortColumns).find(value => value === id || _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; + } + + dispatch([ + { + type: reduxTypes.query.SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES[RHSM_API_QUERY_SET_TYPES.DIRECTION], + viewId: productId, + [RHSM_API_QUERY_SET_TYPES.DIRECTION]: updatedDirection + }, + { + type: reduxTypes.query.SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES[RHSM_API_QUERY_SET_TYPES.SORT], + viewId: productId, + [RHSM_API_QUERY_SET_TYPES.SORT]: updatedSortColumn + } + ]); + }; +}; + +const context = { + useGetInstancesInventory, + useOnPageInstances, + useOnColumnSortInstances +}; + +export { context as default, context, useGetInstancesInventory, useOnPageInstances, useOnColumnSortInstances }; diff --git a/src/components/inventoryList/inventoryList.deprecated.js b/src/components/inventoryList/inventoryList.deprecated.js index 869bc8319..6098aedc7 100644 --- a/src/components/inventoryList/inventoryList.deprecated.js +++ b/src/components/inventoryList/inventoryList.deprecated.js @@ -10,7 +10,7 @@ import { connect, reduxActions, reduxSelectors, reduxTypes, store } from '../../ import Table from '../table/table'; import { Loader } from '../loader/loader'; import { MinHeight } from '../minHeight/minHeight'; -import GuestsList from '../guestsList/guestsList'; +import GuestsList from '../guestsList/guestsList.deprecated'; import { inventoryCardHelpers } from './inventoryCardHelpers'; import Pagination from '../pagination/pagination'; import { ToolbarFieldDisplayName } from '../toolbar/toolbarFieldDisplayName'; @@ -131,7 +131,7 @@ class InventoryList extends React.Component { * @returns {Node} */ renderTable() { - const { filterGuestsData, filterInventoryData, listData, query, session, settings } = this.props; + const { filterGuestsData, filterInventoryData, listData, productId, query, session, settings } = this.props; let updatedColumnHeaders = []; const updatedRows = listData.map(({ ...cellData }) => { @@ -160,7 +160,7 @@ class InventoryList extends React.Component { expandedContent: (hasGuests && ( - - - - - - - - - -
-
- - - - - - - - - - - -`; - -exports[`InventorySubscriptions Component should handle variations in data: variable data 1`] = ` - - - - - - - - - - -
-
- - - - - - - - - - - -`; - -exports[`InventorySubscriptions Component should render a non-connected component: non-connected 1`] = ` - - - - - - - - - - -
-
- - - - - - - - - - - -`; - -exports[`InventorySubscriptions Component should return an empty render when disabled: disabled component 1`] = ` - - - - t(curiosity-inventory.tab, {"context":"disabled"}) - - - +exports[`InventorySubscriptions Component should render a basic component: basic render 1`] = ` + `; diff --git a/src/components/inventorySubscriptions/__tests__/__snapshots__/inventorySubscriptionsContext.test.js.snap b/src/components/inventorySubscriptions/__tests__/__snapshots__/inventorySubscriptionsContext.test.js.snap new file mode 100644 index 000000000..1647b6531 --- /dev/null +++ b/src/components/inventorySubscriptions/__tests__/__snapshots__/inventorySubscriptionsContext.test.js.snap @@ -0,0 +1,106 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InventorySubscriptionsContext should handle an onColumnSort event: onColumnSort event, dispatch 1`] = ` +Array [ + Array [ + Array [ + Object { + "dir": "desc", + "type": "SET_QUERY_RHSM_SUBSCRIPTIONS_INVENTORY_dir", + "viewId": "lorem", + }, + Object { + "sort": "loremIpsumColumnOne", + "type": "SET_QUERY_RHSM_SUBSCRIPTIONS_INVENTORY_sort", + "viewId": "lorem", + }, + ], + ], + Array [ + Array [ + Object { + "dir": "asc", + "type": "SET_QUERY_RHSM_SUBSCRIPTIONS_INVENTORY_dir", + "viewId": "lorem", + }, + Object { + "sort": "loremIpsumColumnOne", + "type": "SET_QUERY_RHSM_SUBSCRIPTIONS_INVENTORY_sort", + "viewId": "lorem", + }, + ], + ], +] +`; + +exports[`InventorySubscriptionsContext should handle an onPage event: onPage event, dispatch 1`] = ` +Array [ + Array [ + Array [ + Object { + "offset": 1, + "type": "SET_QUERY_RHSM_SUBSCRIPTIONS_INVENTORY_offset", + "viewId": "lorem", + }, + Object { + "limit": 5, + "type": "SET_QUERY_RHSM_SUBSCRIPTIONS_INVENTORY_limit", + "viewId": "lorem", + }, + ], + ], +] +`; + +exports[`InventorySubscriptionsContext should handle instances inventory API responses: inventory, cancelled 1`] = ` +Object { + "data": Object {}, + "error": undefined, + "fulfilled": undefined, + "pending": true, +} +`; + +exports[`InventorySubscriptionsContext should handle instances inventory API responses: inventory, disabled 1`] = ` +Object { + "data": Object {}, + "error": undefined, + "fulfilled": undefined, + "pending": false, +} +`; + +exports[`InventorySubscriptionsContext should handle instances inventory API responses: inventory, error 1`] = ` +Object { + "data": Object {}, + "error": true, + "fulfilled": undefined, + "pending": false, +} +`; + +exports[`InventorySubscriptionsContext should handle instances inventory API responses: inventory, fulfilled 1`] = ` +Object { + "data": Object {}, + "error": undefined, + "fulfilled": true, + "pending": false, +} +`; + +exports[`InventorySubscriptionsContext should handle instances inventory API responses: inventory, pending 1`] = ` +Object { + "data": Object {}, + "error": undefined, + "fulfilled": undefined, + "pending": true, +} +`; + +exports[`InventorySubscriptionsContext should return specific properties: specific properties 1`] = ` +Object { + "useGetSubscriptionsInventory": [Function], + "useOnColumnSortSubscriptions": [Function], + "useOnPageSubscriptions": [Function], +} +`; diff --git a/src/components/inventorySubscriptions/__tests__/inventorySubscriptions.test.js b/src/components/inventorySubscriptions/__tests__/inventorySubscriptions.test.js index 08fd88fde..5a5b87056 100644 --- a/src/components/inventorySubscriptions/__tests__/inventorySubscriptions.test.js +++ b/src/components/inventorySubscriptions/__tests__/inventorySubscriptions.test.js @@ -1,116 +1,18 @@ import React from 'react'; -import { shallow } from 'enzyme'; -import { SortByDirection } from '@patternfly/react-table'; import { InventorySubscriptions } from '../inventorySubscriptions'; -import { store } from '../../../redux'; import { RHSM_API_QUERY_TYPES } from '../../../types/rhsmApiTypes'; describe('InventorySubscriptions 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 = { - 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', () => { + it('should render a basic component', async () => { const props = { - query: { + useProduct: () => ({ productId: 'lorem' }), + useProductInventoryQuery: () => ({ [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 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: 'nextEventDate' }); - componentInstance.onColumnSort({}, { direction: SortByDirection.desc, id: 'nextEventDate' }); - componentInstance.onColumnSort({}, { direction: SortByDirection.asc, id: 'loremIpsumBrokenOnPurpose' }); - componentInstance.onColumnSort({}, { direction: SortByDirection.asc, id: 'nextEventDate' }); - - 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'); + const component = await shallowHookComponent(); + expect(component).toMatchSnapshot('basic render'); }); }); diff --git a/src/components/inventorySubscriptions/__tests__/inventorySubscriptionsContext.test.js b/src/components/inventorySubscriptions/__tests__/inventorySubscriptionsContext.test.js new file mode 100644 index 000000000..3df30b82f --- /dev/null +++ b/src/components/inventorySubscriptions/__tests__/inventorySubscriptionsContext.test.js @@ -0,0 +1,104 @@ +import { + context, + useGetSubscriptionsInventory, + useOnPageSubscriptions, + useOnColumnSortSubscriptions +} from '../inventorySubscriptionsContext'; +import { RHSM_API_QUERY_INVENTORY_SORT_DIRECTION_TYPES as SORT_DIRECTION_TYPES } from '../../../services/rhsm/rhsmConstants'; + +describe('InventorySubscriptionsContext', () => { + it('should return specific properties', () => { + expect(context).toMatchSnapshot('specific properties'); + }); + + it('should handle instances inventory API responses', async () => { + const { result: errorResponse } = shallowHook(() => + useGetSubscriptionsInventory({ + useProduct: () => ({ productId: 'lorem' }), + useDispatch: () => {}, + useProductInventoryQuery: () => ({}), + useSelectorsResponse: () => ({ error: true }) + }) + ); + + expect(errorResponse).toMatchSnapshot('inventory, error'); + + const { result: pendingResponse } = shallowHook(() => + useGetSubscriptionsInventory({ + useProduct: () => ({ productId: 'lorem' }), + useDispatch: () => {}, + useProductInventoryQuery: () => ({}), + useSelectorsResponse: () => ({ pending: true }) + }) + ); + + expect(pendingResponse).toMatchSnapshot('inventory, pending'); + + const { result: cancelledResponse } = shallowHook(() => + useGetSubscriptionsInventory({ + useProduct: () => ({ productId: 'lorem' }), + useDispatch: () => {}, + useProductInventoryQuery: () => ({}), + useSelectorsResponse: () => ({ cancelled: true }) + }) + ); + + expect(cancelledResponse).toMatchSnapshot('inventory, cancelled'); + + const mockFulfilledGetInventory = jest.fn(); + const { result: fulfilledResponse } = await mountHook(() => + useGetSubscriptionsInventory({ + getInventory: () => mockFulfilledGetInventory, + useProduct: () => ({ productId: 'lorem' }), + useDispatch: () => {}, + useProductInventoryQuery: () => ({}), + useSelectorsResponse: () => ({ fulfilled: true }) + }) + ); + + expect(mockFulfilledGetInventory).toHaveBeenCalledTimes(1); + expect(fulfilledResponse).toMatchSnapshot('inventory, fulfilled'); + + const mockDisabledGetInventory = jest.fn(); + const { result: disabledResponse } = await mountHook(() => + useGetSubscriptionsInventory({ + isDisabled: true, + getInventory: () => mockDisabledGetInventory, + useProduct: () => ({ productId: 'lorem' }), + useDispatch: () => {}, + useProductInventoryQuery: () => ({}), + useSelectorsResponse: () => ({}) + }) + ); + + expect(mockDisabledGetInventory).toHaveBeenCalledTimes(0); + expect(disabledResponse).toMatchSnapshot('inventory, disabled'); + }); + + it('should handle an onPage event', () => { + const mockDispatch = jest.fn(); + const onPage = useOnPageSubscriptions({ + useDispatch: () => mockDispatch, + useProduct: () => ({ productId: 'lorem' }) + }); + + onPage({ offset: 1, perPage: 5 }); + expect(mockDispatch.mock.calls).toMatchSnapshot('onPage event, dispatch'); + mockDispatch.mockClear(); + }); + + it('should handle an onColumnSort event', () => { + const mockDispatch = jest.fn(); + const onColumnSort = useOnColumnSortSubscriptions({ + sortColumns: { LOREM_IPSUM_COLUMN_ONE: 'loremIpsumColumnOne' }, + useDispatch: () => mockDispatch, + useProduct: () => ({ productId: 'lorem' }) + }); + + onColumnSort(null, { direction: SORT_DIRECTION_TYPES.DESCENDING, id: 'loremIpsumColumnOne' }); + onColumnSort(null, { direction: SORT_DIRECTION_TYPES.ASCENDING, id: 'loremIpsumColumnOne' }); + + expect(mockDispatch.mock.calls).toMatchSnapshot('onColumnSort event, dispatch'); + mockDispatch.mockClear(); + }); +}); diff --git a/src/components/inventorySubscriptions/inventorySubscriptions.js b/src/components/inventorySubscriptions/inventorySubscriptions.js index daa22bbe2..08f2759f9 100644 --- a/src/components/inventorySubscriptions/inventorySubscriptions.js +++ b/src/components/inventorySubscriptions/inventorySubscriptions.js @@ -1,345 +1,79 @@ 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 } 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 { inventoryCardHelpers } from '../inventoryList/inventoryCardHelpers'; -import Pagination from '../pagination/pagination'; -import { paginationHelpers } from '../pagination/paginationHelpers'; import { - RHSM_API_QUERY_SORT_DIRECTION_TYPES as SORT_DIRECTION_TYPES, - RHSM_API_QUERY_SUBSCRIPTIONS_SORT_TYPES as SORT_TYPES, - RHSM_API_QUERY_TYPES -} from '../../types/rhsmApiTypes'; -import { translate } from '../i18n/i18n'; + useProductInventorySubscriptionsConfig, + useProductInventorySubscriptionsQuery +} from '../productView/productViewContext'; +import { + useGetSubscriptionsInventory, + useOnPageSubscriptions, + useOnColumnSortSubscriptions +} from './inventorySubscriptionsContext'; +import InventoryCard from '../inventoryList/inventoryCard'; +import { helpers } from '../../common'; /** - * A subscriptions system inventory component. + * A subscriptions' system inventory component. * - * @augments React.Component + * @param {object} props + * @param {boolean} props.isDisabled + * @param {Function} props.useGetInventory + * @param {Function} props.useOnPage + * @param {Function} props.useOnColumnSort + * @param {Function} props.useProductInventoryConfig + * @param {Function} props.useProductInventoryQuery * @fires onColumnSort * @fires onPage * @fires onUpdateInventoryData + * @returns {Node} */ -class InventorySubscriptions 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_SUBSCRIPTIONS_INVENTORY_TYPES[RHSM_API_QUERY_TYPES.DIRECTION], - viewId: productId, - [RHSM_API_QUERY_TYPES.DIRECTION]: updatedDirection - }, - { - type: reduxTypes.query.SET_QUERY_RHSM_SUBSCRIPTIONS_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_SUBSCRIPTIONS_INVENTORY_TYPES[RHSM_API_QUERY_TYPES.OFFSET], - viewId: productId, - [RHSM_API_QUERY_TYPES.OFFSET]: offset - }, - { - type: reduxTypes.query.SET_QUERY_RHSM_SUBSCRIPTIONS_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 { getSubscriptionsInventory, isDisabled, productId, query } = this.props; - - if (!isDisabled && productId) { - getSubscriptionsInventory(productId, query); - } - }; - - /** - * Render an inventory table. - * - * @returns {Node} - */ - renderTable() { - const { filterInventoryData, listData, query, session } = this.props; - let updatedColumnHeaders = []; - - const updatedRows = listData.map(({ ...cellData }) => { - const { columnHeaders, cells } = inventoryCardHelpers.parseRowCellsListData({ - filters: inventoryCardHelpers.parseInventoryFilters({ - filters: filterInventoryData, - onSort: this.onColumnSort, - query - }), - cellData, - session - }); - - updatedColumnHeaders = columnHeaders; - - return { - cells - }; - }); - - return ( -
- ); - } - - /** - * Render an inventory card. - * - * @returns {Node} - */ - render() { - const { - error, - filterInventoryData, - fulfilled, - isDisabled, - itemCount, - listData, - pending, - perPageDefault, - query, - t - } = 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()} -
-
-
- - - - - - - -
- ); - } -} +const InventorySubscriptions = ({ + isDisabled, + useGetInventory: useAliasGetInventory, + useOnPage: useAliasOnPage, + useOnColumnSort: useAliasOnColumnSort, + useProductInventoryConfig: useAliasProductInventoryConfig, + useProductInventoryQuery: useAliasProductInventoryQuery +}) => ( + +); /** * Prop types. * - * @type {{productId: string, listData: Array, session: object, pending: boolean, query: object, - * fulfilled: boolean, error: boolean, getSubscriptionsInventory: Function, itemCount: number, - * t: Function, filterInventoryData: Array, perPageDefault: number, isDisabled: boolean}} + * @type {{useOnPage: Function, isDisabled: boolean, useProductInventoryConfig: Function, useGetInventory: Function, + * useOnColumnSort: Function, useProductInventoryQuery: Function}} */ InventorySubscriptions.propTypes = { - error: PropTypes.bool, - fulfilled: PropTypes.bool, - 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 - ), - getSubscriptionsInventory: 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, - t: PropTypes.func + useGetInventory: PropTypes.func, + useOnPage: PropTypes.func, + useOnColumnSort: PropTypes.func, + useProductInventoryConfig: PropTypes.func, + useProductInventoryQuery: PropTypes.func }; /** * Default props. * - * @type {{t: translate, filterInventoryData: Array, listData: Array, session: object, pending: boolean, - * fulfilled: boolean, perPageDefault: number, isDisabled: boolean, error: boolean, - * getSubscriptionsInventory: Function, itemCount: number}} + * @type {{useOnPage: Function, isDisabled: boolean, useProductInventoryConfig: Function, useGetInventory: Function, + * useOnColumnSort: Function, useProductInventoryQuery: Function}} */ InventorySubscriptions.defaultProps = { - error: false, - fulfilled: false, - filterInventoryData: [], - getSubscriptionsInventory: helpers.noop, isDisabled: helpers.UI_DISABLED_TABLE_SUBSCRIPTIONS, - itemCount: 0, - listData: [], - pending: false, - perPageDefault: 10, - session: {}, - t: translate + useGetInventory: useGetSubscriptionsInventory, + useOnPage: useOnPageSubscriptions, + useOnColumnSort: useOnColumnSortSubscriptions, + useProductInventoryConfig: useProductInventorySubscriptionsConfig, + useProductInventoryQuery: useProductInventorySubscriptionsQuery }; -/** - * Apply actions to props. - * - * @param {Function} dispatch - * @returns {object} - */ -const mapDispatchToProps = dispatch => ({ - getSubscriptionsInventory: (id, query) => dispatch(reduxActions.rhsm.getSubscriptionsInventory(id, query)) -}); - -/** - * Create a selector from applied state, props. - * - * @type {Function} - */ -const makeMapStateToProps = reduxSelectors.subscriptionsList.makeSubscriptionsList(); - -const ConnectedInventorySubscriptions = connect(makeMapStateToProps, mapDispatchToProps)(InventorySubscriptions); - -export { ConnectedInventorySubscriptions as default, ConnectedInventorySubscriptions, InventorySubscriptions }; +export { InventorySubscriptions as default, InventorySubscriptions }; diff --git a/src/components/inventorySubscriptions/inventorySubscriptionsContext.js b/src/components/inventorySubscriptions/inventorySubscriptionsContext.js new file mode 100644 index 000000000..093fe174d --- /dev/null +++ b/src/components/inventorySubscriptions/inventorySubscriptionsContext.js @@ -0,0 +1,168 @@ +import { useShallowCompareEffect } from 'react-use'; +import _camelCase from 'lodash/camelCase'; +import { SortByDirection } from '@patternfly/react-table'; +import { reduxActions, reduxTypes, storeHooks } from '../../redux'; +import { useProduct, useProductInventorySubscriptionsQuery } from '../productView/productViewContext'; +import { + 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 { helpers } from '../../common'; + +/** + * Combined Redux RHSM Actions, getSubscriptionsInventory, and inventory selector response. + * + * @param {object} options + * @param {boolean} options.isDisabled + * @param {Function} options.getInventory + * @param {Function} options.useDispatch + * @param {Function} options.useProduct + * @param {Function} options.useProductInventoryQuery + * @param {Function} options.useSelectorsResponse + * @returns {Function} + */ +const useGetSubscriptionsInventory = ({ + isDisabled = false, + getInventory = reduxActions.rhsm.getSubscriptionsInventory, + useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, + useProduct: useAliasProduct = useProduct, + useProductInventoryQuery: useAliasProductInventoryQuery = useProductInventorySubscriptionsQuery, + useSelectorsResponse: useAliasSelectorsResponse = storeHooks.reactRedux.useSelectorsResponse +} = {}) => { + const { productId } = useAliasProduct(); + const query = useAliasProductInventoryQuery(); + const dispatch = useAliasDispatch(); + const { error, cancelled, fulfilled, pending, data } = useAliasSelectorsResponse( + ({ inventory }) => inventory?.subscriptionsInventory?.[productId] + ); + + useShallowCompareEffect(() => { + if (!isDisabled) { + getInventory(productId, query)(dispatch); + } + }, [dispatch, isDisabled, productId, query]); + + return { + error, + fulfilled, + pending: pending || cancelled || false, + data: (data?.length === 1 && data[0]) || data || {} + }; +}; + +/** + * An onPage callback for subscriptions inventory. + * + * @param {object} options + * @param {Function} options.useDispatch + * @param {Function} options.useProduct + * @returns {Function} + */ +const useOnPageSubscriptions = ({ + useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, + useProduct: useAliasProduct = useProduct +} = {}) => { + const { productId } = useAliasProduct(); + const dispatch = useAliasDispatch(); + + /** + * On event update state for subscriptions inventory. + * + * @event onPage + * @param {object} params + * @param {number} params.offset + * @param {number} params.perPage + * @returns {void} + */ + return ({ offset, perPage }) => { + dispatch([ + { + type: reduxTypes.query.SET_QUERY_RHSM_SUBSCRIPTIONS_INVENTORY_TYPES[RHSM_API_QUERY_SET_TYPES.OFFSET], + viewId: productId, + [RHSM_API_QUERY_SET_TYPES.OFFSET]: offset + }, + { + type: reduxTypes.query.SET_QUERY_RHSM_SUBSCRIPTIONS_INVENTORY_TYPES[RHSM_API_QUERY_SET_TYPES.LIMIT], + viewId: productId, + [RHSM_API_QUERY_SET_TYPES.LIMIT]: perPage + } + ]); + }; +}; + +/** + * An onColumnSort callback for subscriptions inventory. + * + * @param {object} options + * @param {object} options.sortColumns + * @param {Function} options.useDispatch + * @param {Function} options.useProduct + * @returns {Function} + */ +const useOnColumnSortSubscriptions = ({ + sortColumns = SORT_TYPES, + useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, + useProduct: useAliasProduct = useProduct +} = {}) => { + const { productId } = useAliasProduct(); + const dispatch = useAliasDispatch(); + + /** + * On event update state for subscriptions inventory. + * + * @event onColumnSort + * @param {*} _data + * @param {object} params + * @param {string} params.direction + * @param {string} params.id + * @returns {void} + */ + return (_data, { direction, id }) => { + const updatedSortColumn = Object.values(sortColumns).find(value => value === id || _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; + } + + dispatch([ + { + type: reduxTypes.query.SET_QUERY_RHSM_SUBSCRIPTIONS_INVENTORY_TYPES[RHSM_API_QUERY_SET_TYPES.DIRECTION], + viewId: productId, + [RHSM_API_QUERY_SET_TYPES.DIRECTION]: updatedDirection + }, + { + type: reduxTypes.query.SET_QUERY_RHSM_SUBSCRIPTIONS_INVENTORY_TYPES[RHSM_API_QUERY_SET_TYPES.SORT], + viewId: productId, + [RHSM_API_QUERY_SET_TYPES.SORT]: updatedSortColumn + } + ]); + }; +}; + +const context = { + useGetSubscriptionsInventory, + useOnPageSubscriptions, + useOnColumnSortSubscriptions +}; + +export { + context as default, + context, + useGetSubscriptionsInventory, + useOnPageSubscriptions, + useOnColumnSortSubscriptions +}; diff --git a/src/components/productView/__tests__/__snapshots__/productView.test.js.snap b/src/components/productView/__tests__/__snapshots__/productView.test.js.snap index a4b933a0b..8394a938b 100644 --- a/src/components/productView/__tests__/__snapshots__/productView.test.js.snap +++ b/src/components/productView/__tests__/__snapshots__/productView.test.js.snap @@ -137,12 +137,13 @@ exports[`ProductView Component should allow custom inventory displays via config key="inventory_subs_lorem" title="t(curiosity-inventory.tabSubscriptions, {\\"context\\":\\"lorem\\"})" > - @@ -529,57 +530,6 @@ exports[`ProductView Component should use an instances inventory for rhosak: cus key="inventory_instances_rhosak" title="t(curiosity-inventory.tabInstances, {\\"context\\":\\"noInstances_rhosak\\"})" > - + `; diff --git a/src/components/productView/__tests__/__snapshots__/productViewContext.test.js.snap b/src/components/productView/__tests__/__snapshots__/productViewContext.test.js.snap index 40aca4ec9..8e38d1be0 100644 --- a/src/components/productView/__tests__/__snapshots__/productViewContext.test.js.snap +++ b/src/components/productView/__tests__/__snapshots__/productViewContext.test.js.snap @@ -274,6 +274,7 @@ Object { exports[`ProductViewContext should apply hooks for retrieving specific api queries: inventoryGuestsQuery 1`] = ` Object { + "limit": 100, "offset": "testOffset", } `; @@ -318,6 +319,20 @@ Object { } `; +exports[`ProductViewContext should apply hooks for retrieving specific config filters and settings: productInventoryGuestsConfig 1`] = ` +Object { + "filters": Array [ + Object { + "sit": "dolor", + }, + ], + "initialQuery": Object {}, + "settings": Object { + "sit": "dolor", + }, +} +`; + exports[`ProductViewContext should apply hooks for retrieving specific config filters and settings: productInventoryHostsConfig 1`] = ` Object { "filters": Array [ @@ -389,6 +404,7 @@ Object { }, "useGraphConfig": [Function], "useGraphTallyQuery": [Function], + "useInventoryGuestsConfig": [Function], "useInventoryGuestsQuery": [Function], "useInventoryHostsConfig": [Function], "useInventoryHostsQuery": [Function], diff --git a/src/components/productView/__tests__/__snapshots__/productViewOpenShiftContainer.test.js.snap b/src/components/productView/__tests__/__snapshots__/productViewOpenShiftContainer.test.js.snap index c1ebb08c6..8ec697925 100644 --- a/src/components/productView/__tests__/__snapshots__/productViewOpenShiftContainer.test.js.snap +++ b/src/components/productView/__tests__/__snapshots__/productViewOpenShiftContainer.test.js.snap @@ -345,56 +345,13 @@ exports[`ProductViewOpenShiftContainer Component should render a basic component key="inventory_subs_OpenShift Container Platform" title="t(curiosity-inventory.tabSubscriptions, {\\"context\\":\\"OpenShift Container Platform\\"})" > - diff --git a/src/components/productView/__tests__/productViewContext.test.js b/src/components/productView/__tests__/productViewContext.test.js index ccdf21fa1..31f392d34 100644 --- a/src/components/productView/__tests__/productViewContext.test.js +++ b/src/components/productView/__tests__/productViewContext.test.js @@ -9,6 +9,7 @@ import { useProductContext, useProduct, useProductGraphConfig, + useProductInventoryGuestsConfig, useProductInventoryHostsConfig, useProductInventorySubscriptionsConfig, useProductToolbarConfig @@ -116,6 +117,10 @@ describe('ProductViewContext', () => { initialGraphSettings: { lorem: 'ipsum' }, + initialGuestsFilters: [{ sit: 'dolor' }], + initialGuestsSettings: { + sit: 'dolor' + }, initialInventoryFilters: [{ dolor: 'sit' }], initialInventorySettings: { dolor: 'sit' @@ -138,6 +143,11 @@ describe('ProductViewContext', () => { ); expect(productGraphConfig).toMatchSnapshot('productGraphConfig'); + const { result: productInventoryGuestsConfig } = shallowHook(() => + useProductInventoryGuestsConfig({ useProductContext: () => mockContextValue }) + ); + expect(productInventoryGuestsConfig).toMatchSnapshot('productInventoryGuestsConfig'); + const { result: productInventoryHostsConfig } = shallowHook(() => useProductInventoryHostsConfig({ useProductContext: () => mockContextValue }) ); diff --git a/src/components/productView/productView.js b/src/components/productView/productView.js index e6275d2b7..07d63123f 100644 --- a/src/components/productView/productView.js +++ b/src/components/productView/productView.js @@ -10,13 +10,13 @@ import { ConnectedGraphCard as ConnectedGraphCardDeprecated } from '../graphCard import { GraphCard } from '../graphCard/graphCard'; import { Toolbar } from '../toolbar/toolbar'; import { ConnectedInventoryList as ConnectedInventoryListDeprecated } from '../inventoryList/inventoryList.deprecated'; -import { ConnectedInventoryList } from '../inventoryList/inventoryCard'; +import { ConnectedInventoryCard } from '../inventoryList/inventoryCard'; import { helpers } from '../../common'; import BannerMessages from '../bannerMessages/bannerMessages'; import { SelectPosition } from '../form/select'; import { ToolbarFieldGranularity } from '../toolbar/toolbarFieldGranularity'; import InventoryTabs, { InventoryTab } from '../inventoryTabs/inventoryTabs'; -import { ConnectedInventorySubscriptions } from '../inventorySubscriptions/inventorySubscriptions'; +import { InventorySubscriptions } from '../inventorySubscriptions/inventorySubscriptions'; import { RHSM_API_PATH_PRODUCT_TYPES } from '../../services/rhsm/rhsmConstants'; import { translate } from '../i18n/i18n'; @@ -62,11 +62,8 @@ const ProductView = ({ t, toolbarGraph, toolbarGraphDescription, useRouteDetail: return null; } - const { - graphTallyQuery: initialGraphTallyQuery, - inventoryHostsQuery: initialInventoryHostsQuery, - inventorySubscriptionsQuery: initialInventorySubscriptionsQuery - } = apiQueries.parseRhsmQuery(query, { graphTallyQuery, inventoryHostsQuery, inventorySubscriptionsQuery }); + const { graphTallyQuery: initialGraphTallyQuery, inventoryHostsQuery: initialInventoryHostsQuery } = + apiQueries.parseRhsmQuery(query, { graphTallyQuery, inventoryHostsQuery, inventorySubscriptionsQuery }); let graphCardTooltip = null; @@ -149,15 +146,7 @@ const ProductView = ({ t, toolbarGraph, toolbarGraphDescription, useRouteDetail: key={`inventory_instances_${productId}`} title={t('curiosity-inventory.tabInstances', { context: ['noInstances', productId] })} > - + )} {!helpers.UI_DISABLED_TABLE_SUBSCRIPTIONS && initialSubscriptionsInventoryFilters && ( @@ -165,13 +154,7 @@ const ProductView = ({ t, toolbarGraph, toolbarGraphDescription, useRouteDetail: key={`inventory_subs_${productId}`} title={t('curiosity-inventory.tabSubscriptions', { context: productId })} > - + )} diff --git a/src/components/productView/productViewContext.js b/src/components/productView/productViewContext.js index 7d5680a20..1f4706853 100644 --- a/src/components/productView/productViewContext.js +++ b/src/components/productView/productViewContext.js @@ -2,6 +2,7 @@ import React, { useCallback, useContext } from 'react'; import { reduxHelpers } from '../../redux/common'; import { storeHooks } from '../../redux/hooks'; import { RHSM_API_QUERY_TYPES, rhsmApiTypes } from '../../types/rhsmApiTypes'; +import { RHSM_API_QUERY_SET_TYPES } from '../../services/rhsm/rhsmConstants'; import { helpers } from '../../common/helpers'; /** @@ -25,19 +26,22 @@ const useProductViewContext = () => useContext(ProductViewContext); * * @param {string} queryType An identifier used to pull from both config and Redux, they should named the same. * @param {object} options + * @param {string} options.overrideId A custom identifier, used for scenarios like the Guest inventory IDs * @param {object} options.useProductViewContext * @returns {object} */ const useProductQueryFactory = ( queryType, - { useProductViewContext: useAliasProductViewContext = useProductViewContext } = {} + { overrideId, useProductViewContext: useAliasProductViewContext = useProductViewContext } = {} ) => { const { [queryType]: initialQuery, productId, viewId } = useAliasProductViewContext(); + const queryOverride = storeHooks.reactRedux.useSelector(({ view }) => view?.[queryType]?.[overrideId], undefined); const queryProduct = storeHooks.reactRedux.useSelector(({ view }) => view?.[queryType]?.[productId], undefined); const queryView = storeHooks.reactRedux.useSelector(({ view }) => view?.[queryType]?.[viewId], undefined); return { ...initialQuery, + ...queryOverride, ...queryProduct, ...queryView }; @@ -76,21 +80,27 @@ const useProductGraphTallyQuery = ({ ); /** - * Return the inventory query for guests. + * Return the inventory query for guests. Use fallback/defaults for guests offset, limit. * * @param {object} options + * @param {number} options.defaultLimit + * @param {number} options.defaultOffset * @param {string} options.queryType * @param {object} options.schemaCheck * @param {object} options.options * @returns {object} */ const useProductInventoryGuestsQuery = ({ + defaultLimit = 100, + defaultOffset = 0, queryType = 'inventoryGuestsQuery', schemaCheck = rhsmApiTypes.RHSM_API_QUERY_SET_INVENTORY_GUESTS_TYPES, options } = {}) => reduxHelpers.setApiQuery( { + [RHSM_API_QUERY_SET_TYPES.LIMIT]: defaultLimit, + [RHSM_API_QUERY_SET_TYPES.OFFSET]: defaultOffset, ...useProductQuery(), ...useProductQueryFactory(queryType, options) }, @@ -229,6 +239,22 @@ const useProductGraphConfig = ({ useProductContext: useAliasProductContext = use }; }; +/** + * Return guests inventory configuration. + * + * @param {object} options + * @param {Function} options.useProductContext + * @returns {{settings: object, filters: Array}} + */ +const useProductInventoryGuestsConfig = ({ useProductContext: useAliasProductContext = useProductContext } = {}) => { + const { inventoryGuestsQuery = {}, initialGuestsFilters, initialGuestsSettings = {} } = useAliasProductContext(); + return { + filters: initialGuestsFilters, + initialQuery: inventoryGuestsQuery, + settings: initialGuestsSettings + }; +}; + /** * Return hosts inventory configuration. * @@ -288,6 +314,7 @@ const context = { useInventorySubscriptionsQuery: useProductInventorySubscriptionsQuery, useProduct, useGraphConfig: useProductGraphConfig, + useInventoryGuestsConfig: useProductInventoryGuestsConfig, useInventoryHostsConfig: useProductInventoryHostsConfig, useInventorySubscriptionsConfig: useProductInventorySubscriptionsConfig, useToolbarConfig: useProductToolbarConfig @@ -307,6 +334,7 @@ export { useProductInventorySubscriptionsQuery, useProduct, useProductGraphConfig, + useProductInventoryGuestsConfig, useProductInventoryHostsConfig, useProductInventorySubscriptionsConfig, useProductToolbarConfig diff --git a/src/components/productView/productViewOpenShiftContainer.js b/src/components/productView/productViewOpenShiftContainer.js index 76c223c72..ee866b1e0 100644 --- a/src/components/productView/productViewOpenShiftContainer.js +++ b/src/components/productView/productViewOpenShiftContainer.js @@ -53,14 +53,10 @@ const ProductViewOpenShiftContainer = ({ t, useRouteDetail: useAliasRouteDetail return null; } - const { - graphTallyQuery: initialGraphTallyQuery, - inventoryHostsQuery: initialInventoryHostsQuery, - inventorySubscriptionsQuery: initialInventorySubscriptionsQuery - } = apiQueries.parseRhsmQuery(query, { graphTallyQuery, inventoryHostsQuery, inventorySubscriptionsQuery }); + const { graphTallyQuery: initialGraphTallyQuery, inventoryHostsQuery: initialInventoryHostsQuery } = + apiQueries.parseRhsmQuery(query, { graphTallyQuery, inventoryHostsQuery, inventorySubscriptionsQuery }); let inventoryFilters = initialInventoryFilters; - let subscriptionsInventoryFilters = initialSubscriptionsInventoryFilters; let uomFilter; if (productContextFilterUom) { @@ -74,7 +70,6 @@ const ProductViewOpenShiftContainer = ({ t, useRouteDetail: useAliasRouteDetail }; inventoryFilters = initialInventoryFilters.filter(filter); - subscriptionsInventoryFilters = initialSubscriptionsInventoryFilters.filter(filter); } const graphCardTitle = ( @@ -140,13 +135,7 @@ const ProductViewOpenShiftContainer = ({ t, useRouteDetail: useAliasRouteDetail key={`inventory_subs_${productId}`} title={t('curiosity-inventory.tabSubscriptions', { context: productId })} > - + )} diff --git a/src/config/__tests__/__snapshots__/product.openshiftContainer.test.js.snap b/src/config/__tests__/__snapshots__/product.openshiftContainer.test.js.snap index 2b86a1680..2da483c04 100644 --- a/src/config/__tests__/__snapshots__/product.openshiftContainer.test.js.snap +++ b/src/config/__tests__/__snapshots__/product.openshiftContainer.test.js.snap @@ -291,7 +291,7 @@ Object { "title": "", }, Object { - "title": "", + "title": "2022-01-01", }, ], "columnHeaders": Array [ diff --git a/src/config/__tests__/__snapshots__/product.rhel.test.js.snap b/src/config/__tests__/__snapshots__/product.rhel.test.js.snap index 7c2356ba0..c7ccfa1f4 100644 --- a/src/config/__tests__/__snapshots__/product.rhel.test.js.snap +++ b/src/config/__tests__/__snapshots__/product.rhel.test.js.snap @@ -323,7 +323,7 @@ Object { "title": "", }, Object { - "title": "", + "title": "2022-01-01", }, ], "columnHeaders": Array [ diff --git a/src/config/product.openshiftContainer.js b/src/config/product.openshiftContainer.js index 495de1aa0..01391572a 100644 --- a/src/config/product.openshiftContainer.js +++ b/src/config/product.openshiftContainer.js @@ -201,11 +201,7 @@ const config = { }, { id: 'nextEventDate', - cell: data => - (data?.nextEventDate?.value && - helpers.isDate(data?.nextEventDate?.value) && - moment.utc(data?.nextEventDate?.value).format('YYYY-DD-MM')) || - '', + cell: data => (data?.nextEventDate?.value && moment.utc(data?.nextEventDate?.value).format('YYYY-DD-MM')) || '', isSortable: true, isWrappable: true, cellWidth: 15 diff --git a/src/config/product.rhel.js b/src/config/product.rhel.js index f31c5d7d6..a77aa912b 100644 --- a/src/config/product.rhel.js +++ b/src/config/product.rhel.js @@ -217,11 +217,7 @@ const config = { }, { id: 'nextEventDate', - cell: data => - (data?.nextEventDate?.value && - helpers.isDate(data?.nextEventDate?.value) && - moment.utc(data?.nextEventDate?.value).format('YYYY-DD-MM')) || - '', + cell: data => (data?.nextEventDate?.value && moment.utc(data?.nextEventDate?.value).format('YYYY-DD-MM')) || '', isSortable: true, isWrappable: true, cellWidth: 15 diff --git a/src/redux/hooks/__tests__/__snapshots__/useReactRedux.test.js.snap b/src/redux/hooks/__tests__/__snapshots__/useReactRedux.test.js.snap index 858169deb..4a90465e8 100644 --- a/src/redux/hooks/__tests__/__snapshots__/useReactRedux.test.js.snap +++ b/src/redux/hooks/__tests__/__snapshots__/useReactRedux.test.js.snap @@ -1,5 +1,114 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`useReactRedux should apply a hook for aggregating multiple selector responses with useSelectorsResponse: aggregated calls, cancelled 1`] = ` +Object { + "cancelled": true, + "data": Array [], + "error": false, + "fulfilled": false, + "pending": false, +} +`; + +exports[`useReactRedux should apply a hook for aggregating multiple selector responses with useSelectorsResponse: aggregated calls, cancelled error 1`] = ` +Object { + "cancelled": false, + "data": Array [], + "error": true, + "fulfilled": false, + "pending": false, +} +`; + +exports[`useReactRedux should apply a hook for aggregating multiple selector responses with useSelectorsResponse: aggregated calls, error 1`] = ` +Object { + "cancelled": false, + "data": Array [], + "error": true, + "fulfilled": false, + "pending": false, +} +`; + +exports[`useReactRedux should apply a hook for aggregating multiple selector responses with useSelectorsResponse: aggregated calls, fulfilled 1`] = ` +Object { + "cancelled": false, + "data": Array [ + undefined, + undefined, + ], + "error": false, + "fulfilled": true, + "pending": false, +} +`; + +exports[`useReactRedux should apply a hook for aggregating multiple selector responses with useSelectorsResponse: aggregated calls, fulfilled cancelled 1`] = ` +Object { + "cancelled": false, + "data": Array [ + undefined, + ], + "error": false, + "fulfilled": true, + "pending": false, +} +`; + +exports[`useReactRedux should apply a hook for aggregating multiple selector responses with useSelectorsResponse: aggregated calls, fulfilled error 1`] = ` +Object { + "cancelled": false, + "data": Array [ + undefined, + ], + "error": false, + "fulfilled": true, + "pending": false, +} +`; + +exports[`useReactRedux should apply a hook for aggregating multiple selector responses with useSelectorsResponse: aggregated calls, fulfilled pending 1`] = ` +Object { + "cancelled": false, + "data": Array [ + undefined, + ], + "error": false, + "fulfilled": false, + "pending": true, +} +`; + +exports[`useReactRedux should apply a hook for aggregating multiple selector responses with useSelectorsResponse: aggregated calls, pending 1`] = ` +Object { + "cancelled": false, + "data": Array [], + "error": false, + "fulfilled": false, + "pending": true, +} +`; + +exports[`useReactRedux should apply a hook for aggregating multiple selector responses with useSelectorsResponse: aggregated calls, pending cancelled 1`] = ` +Object { + "cancelled": false, + "data": Array [], + "error": false, + "fulfilled": false, + "pending": true, +} +`; + +exports[`useReactRedux should apply a hook for aggregating multiple selector responses with useSelectorsResponse: aggregated calls, pending error 1`] = ` +Object { + "cancelled": false, + "data": Array [], + "error": false, + "fulfilled": false, + "pending": true, +} +`; + exports[`useReactRedux should apply a hook for useDispatch: dispatch 1`] = ` Array [ Array [ @@ -17,5 +126,6 @@ Object { "useDispatch": [Function], "useSelector": [Function], "useSelectors": [Function], + "useSelectorsResponse": [Function], } `; diff --git a/src/redux/hooks/__tests__/useReactRedux.test.js b/src/redux/hooks/__tests__/useReactRedux.test.js index ad55a1d60..5dd05865c 100644 --- a/src/redux/hooks/__tests__/useReactRedux.test.js +++ b/src/redux/hooks/__tests__/useReactRedux.test.js @@ -45,4 +45,66 @@ describe('useReactRedux', () => { mockSelectorOne.mockClear(); mockSelectorTwo.mockClear(); }); + + it('should apply a hook for aggregating multiple selector responses with useSelectorsResponse', () => { + const errorError = reactReduxHooks.useSelectorsResponse(() => {}, { + useSelectors: () => [{ error: true }, { error: true }] + }); + + expect(errorError).toMatchSnapshot('aggregated calls, error'); + + const cancelledError = reactReduxHooks.useSelectorsResponse(() => {}, { + useSelectors: () => [{ cancelled: true }, { error: true }] + }); + + expect(cancelledError).toMatchSnapshot('aggregated calls, cancelled error'); + + const cancelledCancelled = reactReduxHooks.useSelectorsResponse(() => {}, { + useSelectors: () => [{ cancelled: true }, { cancelled: true }] + }); + + expect(cancelledCancelled).toMatchSnapshot('aggregated calls, cancelled'); + + const pendingError = reactReduxHooks.useSelectorsResponse(() => {}, { + useSelectors: () => [{ pending: true }, { error: true }] + }); + + expect(pendingError).toMatchSnapshot('aggregated calls, pending error'); + + const pendingCancelled = reactReduxHooks.useSelectorsResponse(() => {}, { + useSelectors: () => [{ pending: true }, { cancelled: true }] + }); + + expect(pendingCancelled).toMatchSnapshot('aggregated calls, pending cancelled'); + + const pendingPending = reactReduxHooks.useSelectorsResponse(() => {}, { + useSelectors: () => [{ pending: true }, { pending: true }] + }); + + expect(pendingPending).toMatchSnapshot('aggregated calls, pending'); + + const fulfilledError = reactReduxHooks.useSelectorsResponse(() => {}, { + useSelectors: () => [{ fulfilled: true }, { error: true }] + }); + + expect(fulfilledError).toMatchSnapshot('aggregated calls, fulfilled error'); + + const fulfilledCancelled = reactReduxHooks.useSelectorsResponse(() => {}, { + useSelectors: () => [{ fulfilled: true }, { cancelled: true }] + }); + + expect(fulfilledCancelled).toMatchSnapshot('aggregated calls, fulfilled cancelled'); + + const fulfilledPending = reactReduxHooks.useSelectorsResponse(() => {}, { + useSelectors: () => [{ fulfilled: true }, { pending: true }] + }); + + expect(fulfilledPending).toMatchSnapshot('aggregated calls, fulfilled pending'); + + const fulfilledFulfilled = reactReduxHooks.useSelectorsResponse(() => {}, { + useSelectors: () => [{ fulfilled: true }, { fulfilled: true }] + }); + + expect(fulfilledFulfilled).toMatchSnapshot('aggregated calls, fulfilled'); + }); }); diff --git a/src/redux/hooks/useReactRedux.js b/src/redux/hooks/useReactRedux.js index 435311b9d..d7de1c9c3 100644 --- a/src/redux/hooks/useReactRedux.js +++ b/src/redux/hooks/useReactRedux.js @@ -47,11 +47,85 @@ const useSelectors = ( return useAliasSelector(multiSelector, equality) ?? value; }; +/** + * Return a combined selector response from an API. + * + * @param {Array|Function} selectors A Redux state selector function, or array of functions. + * @param {object} options + * @param {Function} options.useSelectors + * @returns {object} + */ +const useSelectorsResponse = (selectors, { useSelectors: useAliasSelectors = useSelectors } = {}) => { + const updatedSelectors = Array.isArray(selectors) ? selectors : [selectors]; + const selectorResponse = useAliasSelectors(updatedSelectors, []); + + const updatedData = []; + let isPending = false; + let isFulfilled = false; + let errorCount = 0; + let cancelCount = 0; + + const parsedSelectorResponse = selectorResponse.map(response => { + const { pending, fulfilled, error, cancelled, data } = response || {}; + + if (pending) { + isPending = true; + } + + if (fulfilled) { + isFulfilled = true; + updatedData.push(data); + } + + if (error) { + errorCount += 1; + } + + if (cancelled) { + cancelCount += 1; + } + + return response; + }); + + const response = { + data: updatedData, + cancelled: false, + error: false, + fulfilled: false, + pending: false + }; + + if (cancelCount === parsedSelectorResponse.length) { + response.cancelled = true; + } else if ( + errorCount === parsedSelectorResponse.length || + cancelCount + errorCount === parsedSelectorResponse.length + ) { + response.error = true; + } else if (isPending) { + response.pending = true; + } else if (isFulfilled) { + response.fulfilled = true; + } + + return response; +}; + const reactReduxHooks = { shallowEqual, useDispatch, useSelector, - useSelectors + useSelectors, + useSelectorsResponse }; -export { reactReduxHooks as default, reactReduxHooks, shallowEqual, useDispatch, useSelector, useSelectors }; +export { + reactReduxHooks as default, + reactReduxHooks, + shallowEqual, + useDispatch, + useSelector, + useSelectors, + useSelectorsResponse +}; diff --git a/src/redux/reducers/__tests__/__snapshots__/viewReducer.test.js.snap b/src/redux/reducers/__tests__/__snapshots__/viewReducer.test.js.snap index 62b151784..adbd015b6 100644 --- a/src/redux/reducers/__tests__/__snapshots__/viewReducer.test.js.snap +++ b/src/redux/reducers/__tests__/__snapshots__/viewReducer.test.js.snap @@ -4,7 +4,11 @@ exports[`ViewReducer should handle specific defined types: defined type SET_QUER Object { "result": Object { "graphTallyQuery": Object {}, - "inventoryGuestsQuery": Object {}, + "inventoryGuestsQuery": Object { + "test_id": Object { + "offset": 5, + }, + }, "inventoryHostsQuery": Object { "test_id": Object { "dir": "dolor desc direction", @@ -27,11 +31,44 @@ Object { } `; +exports[`ViewReducer should handle specific defined types: defined type SET_QUERY_CLEAR_INVENTORY_GUESTS_LIST 1`] = ` +Object { + "result": Object { + "graphTallyQuery": Object {}, + "inventoryGuestsQuery": Object { + "test_id": Object { + "offset": 0, + }, + }, + "inventoryHostsQuery": Object { + "test_id": Object { + "dir": "dolor desc direction", + "offset": 30, + "sort": "dolor sort", + }, + }, + "inventorySubscriptionsQuery": Object { + "test_id": Object { + "dir": "ipsum desc direction", + "offset": 20, + "sort": "ipsum sort", + }, + }, + "query": Object {}, + }, + "type": "SET_QUERY_CLEAR_INVENTORY_GUESTS_LIST", +} +`; + exports[`ViewReducer should handle specific defined types: defined type SET_QUERY_CLEAR_INVENTORY_LIST 1`] = ` Object { "result": Object { "graphTallyQuery": Object {}, - "inventoryGuestsQuery": Object {}, + "inventoryGuestsQuery": Object { + "test_id": Object { + "offset": 5, + }, + }, "inventoryHostsQuery": Object { "test_id": Object { "dir": "dolor desc direction", @@ -56,7 +93,11 @@ exports[`ViewReducer should handle specific defined types: defined type SET_QUER Object { "result": Object { "graphTallyQuery": Object {}, - "inventoryGuestsQuery": Object {}, + "inventoryGuestsQuery": Object { + "test_id": Object { + "offset": 5, + }, + }, "inventoryHostsQuery": Object { "test_id": Object { "offset": 0, @@ -73,11 +114,74 @@ Object { } `; +exports[`ViewReducer should handle specific defined types: defined type SET_QUERY_RHSM_GUESTS_INVENTORY_limit 1`] = ` +Object { + "result": Object { + "graphTallyQuery": Object {}, + "inventoryGuestsQuery": Object { + "test_id": Object { + "limit": 10, + "offset": 5, + }, + }, + "inventoryHostsQuery": Object { + "test_id": Object { + "dir": "dolor desc direction", + "offset": 30, + "sort": "dolor sort", + }, + }, + "inventorySubscriptionsQuery": Object { + "test_id": Object { + "dir": "ipsum desc direction", + "offset": 20, + "sort": "ipsum sort", + }, + }, + "query": Object {}, + }, + "type": "SET_QUERY_RHSM_GUESTS_INVENTORY_limit", +} +`; + +exports[`ViewReducer should handle specific defined types: defined type SET_QUERY_RHSM_GUESTS_INVENTORY_offset 1`] = ` +Object { + "result": Object { + "graphTallyQuery": Object {}, + "inventoryGuestsQuery": Object { + "test_id": Object { + "offset": 10, + }, + }, + "inventoryHostsQuery": Object { + "test_id": Object { + "dir": "dolor desc direction", + "offset": 30, + "sort": "dolor sort", + }, + }, + "inventorySubscriptionsQuery": Object { + "test_id": Object { + "dir": "ipsum desc direction", + "offset": 20, + "sort": "ipsum sort", + }, + }, + "query": Object {}, + }, + "type": "SET_QUERY_RHSM_GUESTS_INVENTORY_offset", +} +`; + exports[`ViewReducer should handle specific defined types: defined type SET_QUERY_RHSM_HOSTS_INVENTORY_dir 1`] = ` Object { "result": Object { "graphTallyQuery": Object {}, - "inventoryGuestsQuery": Object {}, + "inventoryGuestsQuery": Object { + "test_id": Object { + "offset": 5, + }, + }, "inventoryHostsQuery": Object { "test_id": Object { "dir": "lorem asc direction", @@ -102,7 +206,11 @@ exports[`ViewReducer should handle specific defined types: defined type SET_QUER Object { "result": Object { "graphTallyQuery": Object {}, - "inventoryGuestsQuery": Object {}, + "inventoryGuestsQuery": Object { + "test_id": Object { + "offset": 5, + }, + }, "inventoryHostsQuery": Object { "test_id": Object { "dir": "dolor desc direction", @@ -128,7 +236,11 @@ exports[`ViewReducer should handle specific defined types: defined type SET_QUER Object { "result": Object { "graphTallyQuery": Object {}, - "inventoryGuestsQuery": Object {}, + "inventoryGuestsQuery": Object { + "test_id": Object { + "offset": 5, + }, + }, "inventoryHostsQuery": Object { "test_id": Object { "dir": "dolor desc direction", @@ -154,7 +266,11 @@ exports[`ViewReducer should handle specific defined types: defined type SET_QUER Object { "result": Object { "graphTallyQuery": Object {}, - "inventoryGuestsQuery": Object {}, + "inventoryGuestsQuery": Object { + "test_id": Object { + "offset": 5, + }, + }, "inventoryHostsQuery": Object { "test_id": Object { "dir": "dolor desc direction", @@ -179,7 +295,11 @@ exports[`ViewReducer should handle specific defined types: defined type SET_QUER Object { "result": Object { "graphTallyQuery": Object {}, - "inventoryGuestsQuery": Object {}, + "inventoryGuestsQuery": Object { + "test_id": Object { + "offset": 5, + }, + }, "inventoryHostsQuery": Object { "test_id": Object { "dir": "dolor desc direction", @@ -204,7 +324,11 @@ exports[`ViewReducer should handle specific defined types: defined type SET_QUER Object { "result": Object { "graphTallyQuery": Object {}, - "inventoryGuestsQuery": Object {}, + "inventoryGuestsQuery": Object { + "test_id": Object { + "offset": 5, + }, + }, "inventoryHostsQuery": Object { "test_id": Object { "dir": "dolor desc direction", @@ -229,7 +353,11 @@ exports[`ViewReducer should handle specific defined types: defined type SET_QUER Object { "result": Object { "graphTallyQuery": Object {}, - "inventoryGuestsQuery": Object {}, + "inventoryGuestsQuery": Object { + "test_id": Object { + "offset": 5, + }, + }, "inventoryHostsQuery": Object { "test_id": Object { "dir": "dolor desc direction", @@ -255,7 +383,11 @@ exports[`ViewReducer should handle specific defined types: defined type SET_QUER Object { "result": Object { "graphTallyQuery": Object {}, - "inventoryGuestsQuery": Object {}, + "inventoryGuestsQuery": Object { + "test_id": Object { + "offset": 5, + }, + }, "inventoryHostsQuery": Object { "test_id": Object { "dir": "dolor desc direction", @@ -280,7 +412,11 @@ exports[`ViewReducer should handle specific defined types: defined type SET_QUER Object { "result": Object { "graphTallyQuery": Object {}, - "inventoryGuestsQuery": Object {}, + "inventoryGuestsQuery": Object { + "test_id": Object { + "offset": 5, + }, + }, "inventoryHostsQuery": Object { "test_id": Object { "dir": "dolor desc direction", @@ -305,7 +441,11 @@ exports[`ViewReducer should handle specific defined types: defined type SET_QUER Object { "result": Object { "graphTallyQuery": Object {}, - "inventoryGuestsQuery": Object {}, + "inventoryGuestsQuery": Object { + "test_id": Object { + "offset": 5, + }, + }, "inventoryHostsQuery": Object { "test_id": Object { "dir": "dolor desc direction", @@ -334,7 +474,11 @@ exports[`ViewReducer should handle specific defined types: defined type SET_QUER Object { "result": Object { "graphTallyQuery": Object {}, - "inventoryGuestsQuery": Object {}, + "inventoryGuestsQuery": Object { + "test_id": Object { + "offset": 5, + }, + }, "inventoryHostsQuery": Object { "test_id": Object { "dir": "dolor desc direction", @@ -367,7 +511,11 @@ Object { "granularity": "lorem granularity", }, }, - "inventoryGuestsQuery": Object {}, + "inventoryGuestsQuery": Object { + "test_id": Object { + "offset": 5, + }, + }, "inventoryHostsQuery": Object { "test_id": Object { "dir": "dolor desc direction", @@ -392,7 +540,11 @@ exports[`ViewReducer should handle specific defined types: defined type SET_QUER Object { "result": Object { "graphTallyQuery": Object {}, - "inventoryGuestsQuery": Object {}, + "inventoryGuestsQuery": Object { + "test_id": Object { + "offset": 5, + }, + }, "inventoryHostsQuery": Object { "test_id": Object { "dir": "dolor desc direction", @@ -421,7 +573,11 @@ exports[`ViewReducer should handle specific defined types: defined type SET_QUER Object { "result": Object { "graphTallyQuery": Object {}, - "inventoryGuestsQuery": Object {}, + "inventoryGuestsQuery": Object { + "test_id": Object { + "offset": 5, + }, + }, "inventoryHostsQuery": Object { "test_id": Object { "dir": "dolor desc direction", @@ -450,7 +606,11 @@ exports[`ViewReducer should handle specific defined types: defined type SET_QUER Object { "result": Object { "graphTallyQuery": Object {}, - "inventoryGuestsQuery": Object {}, + "inventoryGuestsQuery": Object { + "test_id": Object { + "offset": 5, + }, + }, "inventoryHostsQuery": Object { "test_id": Object { "dir": "dolor desc direction", diff --git a/src/redux/reducers/__tests__/viewReducer.test.js b/src/redux/reducers/__tests__/viewReducer.test.js index 656fef481..4fd911ca0 100644 --- a/src/redux/reducers/__tests__/viewReducer.test.js +++ b/src/redux/reducers/__tests__/viewReducer.test.js @@ -12,16 +12,22 @@ describe('ViewReducer', () => { ...Object.values(types.SET_QUERY_RHSM_TYPES), ...Object.values(types.SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES), ...Object.values(types.SET_QUERY_RHSM_SUBSCRIPTIONS_INVENTORY_TYPES), + ...Object.values(types.SET_QUERY_RHSM_GUESTS_INVENTORY_TYPES), types.SET_QUERY_CLEAR, types.SET_QUERY_CLEAR_INVENTORY_LIST, - types.SET_QUERY_RESET_INVENTORY_LIST + types.SET_QUERY_RESET_INVENTORY_LIST, + types.SET_QUERY_CLEAR_INVENTORY_GUESTS_LIST ]; specificTypes.forEach(value => { const state = { query: {}, graphTallyQuery: {}, - inventoryGuestsQuery: {}, + inventoryGuestsQuery: { + test_id: { + [RHSM_API_QUERY_TYPES.OFFSET]: 5 + } + }, inventoryHostsQuery: { test_id: { [RHSM_API_QUERY_TYPES.DIRECTION]: 'dolor desc direction', diff --git a/src/redux/reducers/viewReducer.js b/src/redux/reducers/viewReducer.js index 4e0cfe036..29af0e810 100644 --- a/src/redux/reducers/viewReducer.js +++ b/src/redux/reducers/viewReducer.js @@ -86,6 +86,33 @@ const viewReducer = (state = initialState, action) => { reset: false } ); + case reduxTypes.query.SET_QUERY_CLEAR_INVENTORY_GUESTS_LIST: + const updateClearGuestQuery = (query = {}, id) => { + const queryIds = routerHelpers.productGroups[id] || (query[id] && [id]) || []; + const updatedQuery = { ...query }; + + queryIds.forEach(queryId => { + const productQuery = updatedQuery[queryId] || {}; + + if (typeof productQuery[RHSM_API_QUERY_TYPES.OFFSET] === 'number') { + productQuery[RHSM_API_QUERY_TYPES.OFFSET] = 0; + } + }); + + return updatedQuery; + }; + + return reduxHelpers.setStateProp( + null, + { + ...state, + inventoryGuestsQuery: updateClearGuestQuery(state.inventoryGuestsQuery, action.viewId) + }, + { + state, + reset: false + } + ); case reduxTypes.query.SET_QUERY_CLEAR: return reduxHelpers.setStateProp( 'query', @@ -184,6 +211,34 @@ const viewReducer = (state = initialState, action) => { reset: false } ); + case reduxTypes.query.SET_QUERY_RHSM_GUESTS_INVENTORY_TYPES[RHSM_API_QUERY_TYPES.LIMIT]: + return reduxHelpers.setStateProp( + 'inventoryGuestsQuery', + { + [action.viewId]: { + ...state.inventoryGuestsQuery[action.viewId], + [RHSM_API_QUERY_TYPES.LIMIT]: action[RHSM_API_QUERY_TYPES.LIMIT] + } + }, + { + state, + reset: false + } + ); + case reduxTypes.query.SET_QUERY_RHSM_GUESTS_INVENTORY_TYPES[RHSM_API_QUERY_TYPES.OFFSET]: + return reduxHelpers.setStateProp( + 'inventoryGuestsQuery', + { + [action.viewId]: { + ...state.inventoryGuestsQuery[action.viewId], + [RHSM_API_QUERY_TYPES.OFFSET]: action[RHSM_API_QUERY_TYPES.OFFSET] + } + }, + { + state, + reset: false + } + ); case reduxTypes.query.SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES[RHSM_API_QUERY_TYPES.DISPLAY_NAME]: return reduxHelpers.setStateProp( 'inventoryHostsQuery', diff --git a/src/redux/selectors/__tests__/__snapshots__/instancesListSelectors.test.js.snap b/src/redux/selectors/__tests__/__snapshots__/instancesListSelectors.test.js.snap deleted file mode 100644 index 8a2444038..000000000 --- a/src/redux/selectors/__tests__/__snapshots__/instancesListSelectors.test.js.snap +++ /dev/null @@ -1,32 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`InstancesListSelectors should pass existing query data through response: existing query data 1`] = ` -Object { - "query": Object { - "a": "b", - "c": "d", - }, -} -`; - -exports[`InstancesListSelectors should pass existing state data through response: existing state data 1`] = ` -Object { - "data": Array [], - "meta": "meta field", - "query": Object {}, - "testing": "lorem ipsum", -} -`; - -exports[`InstancesListSelectors should pass minimal data on missing a reducer response: missing reducer error 1`] = ` -Object { - "query": Object {}, -} -`; - -exports[`InstancesListSelectors should return specific selectors: selectors 1`] = ` -Object { - "instancesList": [Function], - "makeInstancesList": [Function], -} -`; diff --git a/src/redux/selectors/__tests__/__snapshots__/subscriptionsListSelectors.test.js.snap b/src/redux/selectors/__tests__/__snapshots__/subscriptionsListSelectors.test.js.snap deleted file mode 100644 index 990771f00..000000000 --- a/src/redux/selectors/__tests__/__snapshots__/subscriptionsListSelectors.test.js.snap +++ /dev/null @@ -1,275 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SubscriptionsListSelectors should handle pending state on a product ID: pending 1`] = ` -Object { - "error": false, - "fulfilled": false, - "itemCount": 0, - "listData": Array [], - "pending": true, - "query": Object {}, - "status": undefined, -} -`; - -exports[`SubscriptionsListSelectors should map a fulfilled product ID response to an aggregated output: fulfilled 1`] = ` -Object { - "error": false, - "fulfilled": true, - "itemCount": 0, - "listData": Array [ - Object { - "nextEventDate": null, - "nextEventType": null, - "physicalCapacity": 2, - "productName": null, - "quantity": null, - "serviceLevel": null, - "sku": null, - "subscriptions": null, - "totalCapacity": 3, - "uom": null, - "usage": null, - "virtualCapacity": 1, - }, - Object { - "nextEventDate": null, - "nextEventType": null, - "physicalCapacity": 2, - "productName": null, - "quantity": null, - "serviceLevel": null, - "sku": null, - "subscriptions": null, - "totalCapacity": 3, - "uom": null, - "usage": null, - "virtualCapacity": 1, - }, - ], - "pending": false, - "query": Object { - "sla": "Premium", - }, - "status": undefined, -} -`; - -exports[`SubscriptionsListSelectors should pass minimal data on a product ID without a product ID provided: no product id error 1`] = ` -Object { - "error": false, - "fulfilled": false, - "itemCount": 0, - "listData": Array [], - "pending": false, - "query": Object {}, - "status": undefined, -} -`; - -exports[`SubscriptionsListSelectors should pass minimal data on missing a reducer response: missing reducer error 1`] = ` -Object { - "error": false, - "fulfilled": false, - "itemCount": 0, - "listData": Array [], - "pending": false, - "query": Object {}, - "status": undefined, -} -`; - -exports[`SubscriptionsListSelectors should populate data from the in memory cache: cached data: ERROR, cancelled API call, maintain prior response 1`] = ` -Object { - "error": false, - "fulfilled": true, - "itemCount": 0, - "listData": Array [ - Object { - "nextEventDate": null, - "nextEventType": null, - "physicalCapacity": 3, - "productName": null, - "quantity": null, - "serviceLevel": null, - "sku": null, - "subscriptions": null, - "totalCapacity": 6, - "uom": null, - "usage": null, - "virtualCapacity": 3, - }, - ], - "pending": false, - "query": Object { - "sla": "Premium", - }, - "status": undefined, -} -`; - -exports[`SubscriptionsListSelectors should populate data from the in memory cache: cached data: cache used and pending 1`] = ` -Object { - "error": false, - "fulfilled": true, - "itemCount": 0, - "listData": Array [ - Object { - "nextEventDate": null, - "nextEventType": null, - "physicalCapacity": 2, - "productName": null, - "quantity": null, - "serviceLevel": null, - "sku": null, - "subscriptions": null, - "totalCapacity": 3, - "uom": null, - "usage": null, - "virtualCapacity": 1, - }, - ], - "pending": false, - "query": Object { - "sla": "Premium", - }, - "status": undefined, -} -`; - -exports[`SubscriptionsListSelectors should populate data from the in memory cache: cached data: initial fulfilled 1`] = ` -Object { - "error": false, - "fulfilled": true, - "itemCount": 0, - "listData": Array [ - Object { - "nextEventDate": null, - "nextEventType": null, - "physicalCapacity": 2, - "productName": null, - "quantity": null, - "serviceLevel": null, - "sku": null, - "subscriptions": null, - "totalCapacity": 3, - "uom": null, - "usage": null, - "virtualCapacity": 1, - }, - ], - "pending": false, - "query": Object { - "sla": "Premium", - }, - "status": undefined, -} -`; - -exports[`SubscriptionsListSelectors should populate data from the in memory cache: cached data: query updated and fulfilled 1`] = ` -Object { - "error": false, - "fulfilled": true, - "itemCount": 0, - "listData": Array [ - Object { - "nextEventDate": null, - "nextEventType": null, - "physicalCapacity": 5, - "productName": null, - "quantity": null, - "serviceLevel": null, - "sku": null, - "subscriptions": null, - "totalCapacity": 10, - "uom": null, - "usage": null, - "virtualCapacity": 5, - }, - ], - "pending": false, - "query": Object { - "sla": "", - }, - "status": undefined, -} -`; - -exports[`SubscriptionsListSelectors should populate data from the in memory cache: cached data: updated and fulfilled 1`] = ` -Object { - "error": false, - "fulfilled": true, - "itemCount": 0, - "listData": Array [ - Object { - "nextEventDate": null, - "nextEventType": null, - "physicalCapacity": 3, - "productName": null, - "quantity": null, - "serviceLevel": null, - "sku": null, - "subscriptions": null, - "totalCapacity": 6, - "uom": null, - "usage": null, - "virtualCapacity": 3, - }, - ], - "pending": false, - "query": Object { - "sla": "Premium", - }, - "status": undefined, -} -`; - -exports[`SubscriptionsListSelectors should populate data on a product ID when the api response is missing expected properties: data populated, missing properties 1`] = ` -Object { - "error": false, - "fulfilled": true, - "itemCount": 0, - "listData": Array [ - Object { - "nextEventDate": null, - "nextEventType": null, - "physicalCapacity": 2, - "productName": null, - "quantity": null, - "serviceLevel": null, - "sku": null, - "subscriptions": null, - "totalCapacity": 3, - "uom": null, - "usage": null, - "virtualCapacity": 1, - }, - Object { - "nextEventDate": null, - "nextEventType": null, - "physicalCapacity": 2, - "productName": null, - "quantity": null, - "serviceLevel": null, - "sku": null, - "subscriptions": null, - "totalCapacity": 3, - "uom": null, - "usage": null, - "virtualCapacity": 1, - }, - ], - "pending": false, - "query": Object { - "sla": "Premium", - }, - "status": undefined, -} -`; - -exports[`SubscriptionsListSelectors should return specific selectors: selectors 1`] = ` -Object { - "makeSubscriptionsList": [Function], - "subscriptionsList": [Function], -} -`; diff --git a/src/redux/selectors/__tests__/instancesListSelectors.test.js b/src/redux/selectors/__tests__/instancesListSelectors.test.js deleted file mode 100644 index 4569de2bf..000000000 --- a/src/redux/selectors/__tests__/instancesListSelectors.test.js +++ /dev/null @@ -1,53 +0,0 @@ -import instancesListSelectors from '../instancesListSelectors'; - -describe('InstancesListSelectors', () => { - it('should return specific selectors', () => { - expect(instancesListSelectors).toMatchSnapshot('selectors'); - }); - - it('should pass minimal data on missing a reducer response', () => { - const state = {}; - expect(instancesListSelectors.instancesList(state)).toMatchSnapshot('missing reducer error'); - }); - - it('should pass existing state data through response', () => { - const state = { - inventory: { - instancesInventory: { - loremIpsum: { - testing: 'lorem ipsum', - data: { - data: [], - meta: 'meta field' - } - } - } - } - }; - const props = { - productId: 'loremIpsum' - }; - - expect(instancesListSelectors.instancesList(state, props)).toMatchSnapshot('existing state data'); - }); - - it('should pass existing query data through response', () => { - const state = { - inventory: { - instancesInventory: { - loremIpsum: {} - } - }, - view: { - query: { loremIpsum: { d: 'e' } }, - inventoryHostsQuery: { dolorSit: { a: 'b' }, loremIpsum: { c: 'd' } } - } - }; - const props = { - productId: 'loremIpsum', - viewId: 'dolorSit' - }; - - expect(instancesListSelectors.instancesList(state, props)).toMatchSnapshot('existing query data'); - }); -}); diff --git a/src/redux/selectors/__tests__/subscriptionsListSelectors.test.js b/src/redux/selectors/__tests__/subscriptionsListSelectors.test.js deleted file mode 100644 index efec59f65..000000000 --- a/src/redux/selectors/__tests__/subscriptionsListSelectors.test.js +++ /dev/null @@ -1,259 +0,0 @@ -import subscriptionsListSelectors from '../subscriptionsListSelectors'; -import { rhsmApiTypes } from '../../../types/rhsmApiTypes'; - -describe('SubscriptionsListSelectors', () => { - it('should return specific selectors', () => { - expect(subscriptionsListSelectors).toMatchSnapshot('selectors'); - }); - - it('should pass minimal data on missing a reducer response', () => { - const state = {}; - expect(subscriptionsListSelectors.subscriptionsList(state)).toMatchSnapshot('missing reducer error'); - }); - - it('should pass minimal data on a product ID without a product ID provided', () => { - const props = { - viewId: 'test', - productId: undefined, - query: {} - }; - const state = { - inventory: { - subscriptionsInventory: { - fulfilled: true, - metaId: undefined, - metaQuery: {}, - data: { [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_DATA]: [] } - } - } - }; - - expect(subscriptionsListSelectors.subscriptionsList(state, props)).toMatchSnapshot('no product id error'); - }); - - it('should handle pending state on a product ID', () => { - const props = { - viewId: 'test', - productId: 'Lorem Ipsum ID pending state' - }; - const state = { - inventory: { - subscriptionsInventory: { - 'Lorem Ipsum ID pending state': { - pending: true, - metaId: 'Lorem Ipsum ID pending state', - metaQuery: {}, - data: { [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_DATA]: [] } - } - } - } - }; - - expect(subscriptionsListSelectors.subscriptionsList(state, props)).toMatchSnapshot('pending'); - }); - - it('should populate data on a product ID when the api response is missing expected properties', () => { - const props = { - viewId: 'test', - productId: 'Lorem Ipsum missing expected properties', - query: { - [rhsmApiTypes.RHSM_API_QUERY_TYPES.SLA]: rhsmApiTypes.RHSM_API_QUERY_SLA_TYPES.PREMIUM - } - }; - const state = { - inventory: { - subscriptionsInventory: { - 'Lorem Ipsum missing expected properties': { - fulfilled: true, - metaId: 'Lorem Ipsum missing expected properties', - metaQuery: { - [rhsmApiTypes.RHSM_API_QUERY_TYPES.SLA]: rhsmApiTypes.RHSM_API_QUERY_SLA_TYPES.PREMIUM - }, - data: { - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_DATA]: [ - { - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.PHYSICAL_CAPACITY]: 2, - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.VIRTUAL_CAPACITY]: 1, - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.TOTAL_CAPACITY]: 3 - }, - { - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.PHYSICAL_CAPACITY]: 2, - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.VIRTUAL_CAPACITY]: 1, - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.TOTAL_CAPACITY]: 3 - } - ] - } - } - } - } - }; - - expect(subscriptionsListSelectors.subscriptionsList(state, props)).toMatchSnapshot( - 'data populated, missing properties' - ); - }); - - it('should map a fulfilled product ID response to an aggregated output', () => { - const props = { - viewId: 'test', - productId: 'Lorem Ipsum fulfilled aggregated output', - query: { - [rhsmApiTypes.RHSM_API_QUERY_TYPES.SLA]: rhsmApiTypes.RHSM_API_QUERY_SLA_TYPES.PREMIUM - } - }; - const state = { - inventory: { - subscriptionsInventory: { - 'Lorem Ipsum fulfilled aggregated output': { - fulfilled: true, - metaId: 'Lorem Ipsum fulfilled aggregated output', - metaQuery: { - [rhsmApiTypes.RHSM_API_QUERY_TYPES.SLA]: rhsmApiTypes.RHSM_API_QUERY_SLA_TYPES.PREMIUM - }, - data: { - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_DATA]: [ - { - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.PHYSICAL_CAPACITY]: 2, - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.VIRTUAL_CAPACITY]: 1, - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.TOTAL_CAPACITY]: 3 - }, - { - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.PHYSICAL_CAPACITY]: 2, - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.VIRTUAL_CAPACITY]: 1, - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.TOTAL_CAPACITY]: 3 - } - ] - } - } - } - } - }; - - expect(subscriptionsListSelectors.subscriptionsList(state, props)).toMatchSnapshot('fulfilled'); - }); - - it('should populate data from the in memory cache', () => { - const props = { - viewId: 'cache-test', - productId: 'Lorem Ipsum ID cached', - query: { - [rhsmApiTypes.RHSM_API_QUERY_TYPES.SLA]: rhsmApiTypes.RHSM_API_QUERY_SLA_TYPES.PREMIUM - } - }; - const stateInitialFulfilled = { - inventory: { - subscriptionsInventory: { - 'Lorem Ipsum ID cached': { - fulfilled: true, - metaId: 'Lorem Ipsum ID cached', - metaQuery: { - [rhsmApiTypes.RHSM_API_QUERY_TYPES.SLA]: rhsmApiTypes.RHSM_API_QUERY_SLA_TYPES.PREMIUM - }, - data: { - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_DATA]: [ - { - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.PHYSICAL_CAPACITY]: 2, - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.VIRTUAL_CAPACITY]: 1, - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.TOTAL_CAPACITY]: 3 - } - ] - } - } - } - } - }; - - expect(subscriptionsListSelectors.subscriptionsList(stateInitialFulfilled, props)).toMatchSnapshot( - 'cached data: initial fulfilled' - ); - - const statePending = { - inventory: { - subscriptionsInventory: { - 'Lorem Ipsum ID cached': { - ...stateInitialFulfilled.inventory.subscriptionsInventory['Lorem Ipsum ID cached'], - pending: true - } - } - } - }; - - expect(subscriptionsListSelectors.subscriptionsList(statePending, props)).toMatchSnapshot( - 'cached data: cache used and pending' - ); - - const stateFulfilled = { - inventory: { - subscriptionsInventory: { - 'Lorem Ipsum ID cached': { - ...stateInitialFulfilled.inventory.subscriptionsInventory['Lorem Ipsum ID cached'], - fulfilled: true, - data: { - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_DATA]: [ - { - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.PHYSICAL_CAPACITY]: 3, - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.VIRTUAL_CAPACITY]: 3, - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.TOTAL_CAPACITY]: 6 - } - ] - } - } - } - } - }; - - expect(subscriptionsListSelectors.subscriptionsList(stateFulfilled, props)).toMatchSnapshot( - 'cached data: updated and fulfilled' - ); - - const stateCancelled = { - inventory: { - subscriptionsInventory: { - 'Lorem Ipsum ID cached': { - ...stateInitialFulfilled.inventory.subscriptionsInventory['Lorem Ipsum ID cached'], - cancelled: true, - fulfilled: false - } - } - } - }; - - expect(subscriptionsListSelectors.subscriptionsList(stateCancelled, props)).toMatchSnapshot( - 'cached data: ERROR, cancelled API call, maintain prior response' - ); - - const stateFulfilledQueryUpdated = { - inventory: { - subscriptionsInventory: { - 'Lorem Ipsum ID cached': { - ...stateInitialFulfilled.inventory.subscriptionsInventory['Lorem Ipsum ID cached'], - metaQuery: { - [rhsmApiTypes.RHSM_API_QUERY_TYPES.SLA]: rhsmApiTypes.RHSM_API_QUERY_SLA_TYPES.NONE - }, - fulfilled: true, - data: { - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_DATA]: [ - { - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.PHYSICAL_CAPACITY]: 5, - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.VIRTUAL_CAPACITY]: 5, - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.TOTAL_CAPACITY]: 10 - } - ] - } - } - } - }, - view: { - query: { - 'Lorem Ipsum ID cached': { - [rhsmApiTypes.RHSM_API_QUERY_TYPES.SLA]: rhsmApiTypes.RHSM_API_QUERY_SLA_TYPES.NONE - } - } - } - }; - - expect(subscriptionsListSelectors.subscriptionsList(stateFulfilledQueryUpdated, props)).toMatchSnapshot( - 'cached data: query updated and fulfilled' - ); - }); -}); diff --git a/src/redux/selectors/index.js b/src/redux/selectors/index.js index ee9479a8a..53a620d95 100644 --- a/src/redux/selectors/index.js +++ b/src/redux/selectors/index.js @@ -2,8 +2,6 @@ import appMessagesSelectors from './appMessagesSelectors'; import guestsListSelectors from './guestsListSelectors'; import graphCardSelectors from './graphCardSelectors'; import inventoryListSelectors from './inventoryListSelectors'; -import instancesListSelectors from './instancesListSelectors'; -import subscriptionsListSelectors from './subscriptionsListSelectors'; import userSelectors from './userSelectors'; const reduxSelectors = { @@ -11,8 +9,6 @@ const reduxSelectors = { guestsList: guestsListSelectors, graphCard: graphCardSelectors, inventoryList: inventoryListSelectors, - instancesList: instancesListSelectors, - subscriptionsList: subscriptionsListSelectors, user: userSelectors }; diff --git a/src/redux/selectors/instancesListSelectors.js b/src/redux/selectors/instancesListSelectors.js deleted file mode 100644 index 69148396b..000000000 --- a/src/redux/selectors/instancesListSelectors.js +++ /dev/null @@ -1,70 +0,0 @@ -import { createSelector } from 'reselect'; -import { apiQueries } from '../common'; -import { selector as userSession } from './userSelectors'; - -/** - * Return a combined state, props object. - * - * @private - * @param {object} state - * @param {object} props - * @returns {object} - */ -const statePropsFilter = (state, props = {}) => ({ - ...state.inventory?.instancesInventory?.[props.productId] -}); - -/** - * Return a combined query object. - * - * @param {object} state - * @param {object} props - * @returns {object} - */ -const queryFilter = (state, props = {}) => { - const { inventoryHostsQuery: query } = apiQueries.parseRhsmQuery( - { - ...props.query, - ...state.view?.query?.[props.productId], - ...state.view?.query?.[props.viewId] - }, - { - inventoryHostsQuery: { - ...state.view?.inventoryHostsQuery?.[props.productId], - ...state.view?.inventoryHostsQuery?.[props.viewId] - } - } - ); - - return query; -}; - -/** - * Create selector, transform combined state, props into a consumable object. - * - * @type {{query: object}} - */ -const selector = createSelector([statePropsFilter, queryFilter], (response, query = {}) => ({ - ...response, - ...response?.data, - query -})); - -/** - * Expose selector instance. For scenarios where a selector is reused across component instances. - * - * @param {object} defaultProps - * @returns {{pending: boolean, fulfilled: boolean, graphData: object, error: boolean, session: object, - * status: (*|number)}} - */ -const makeSelector = defaultProps => (state, props) => ({ - ...userSession(state, props, defaultProps), - ...selector(state, props, defaultProps) -}); - -const instancesListSelectors = { - instancesList: selector, - makeInstancesList: makeSelector -}; - -export { instancesListSelectors as default, instancesListSelectors, selector, makeSelector }; diff --git a/src/redux/selectors/subscriptionsListSelectors.js b/src/redux/selectors/subscriptionsListSelectors.js deleted file mode 100644 index 8ba2370a0..000000000 --- a/src/redux/selectors/subscriptionsListSelectors.js +++ /dev/null @@ -1,162 +0,0 @@ -import { createSelectorCreator, defaultMemoize } from 'reselect'; -import LruCache from 'lru-cache'; -import _isEqual from 'lodash/isEqual'; -import { rhsmApiTypes } from '../../types/rhsmApiTypes'; -import { reduxHelpers } from '../common/reduxHelpers'; -import { apiQueries } from '../common'; -import { selector as userSession } from './userSelectors'; - -/** - * ToDo: Consider consolidating inventory selectors, and/or create API specific selectors, i.e. RHSM, etc - * Breaking out the inventory selectors is a temporary solution until the API is finalized. Aspects - * of the caching and applying the API schemas is now consistent enough to allow for grouping/refinement. - */ - -/** - * Create a custom "are objects equal" selector. - * - * @private - * @type {Function}} - */ -const createDeepEqualSelector = createSelectorCreator(defaultMemoize, _isEqual); - -/** - * Selector cache. - * - * @private - * @type {object} - */ -const selectorCache = new LruCache({ - maxAge: Number.parseInt(process.env.REACT_APP_SELECTOR_CACHE, 10), - max: 10, - stale: true, - updateAgeOnGet: true -}); - -/** - * Return a combined state, props object. - * - * @private - * @param {object} state - * @param {object} props - * @returns {object} - */ -const statePropsFilter = (state, props = {}) => ({ - ...state.inventory?.subscriptionsInventory?.[props.productId], - ...{ - viewId: props.viewId, - productId: props.productId - } -}); - -/** - * Return a combined query object. - * - * @param {object} state - * @param {object} props - * @returns {object} - */ -const queryFilter = (state, props = {}) => { - const { inventorySubscriptionsQuery: query } = apiQueries.parseRhsmQuery( - { - ...props.query, - ...state.view?.query?.[props.productId], - ...state.view?.query?.[props.viewId] - }, - { - inventorySubscriptionsQuery: { - ...state.view?.inventorySubscriptionsQuery?.[props.productId], - ...state.view?.inventorySubscriptionsQuery?.[props.viewId] - } - } - ); - - return query; -}; - -/** - * Create selector, transform combined state, props into a consumable object. - * - * @type {{pending: boolean, fulfilled: boolean, listData: object, error: boolean, status: (*|number)}} - */ -const selector = createDeepEqualSelector([statePropsFilter, queryFilter], (response, query = {}) => { - const { viewId = null, productId = null, metaId, metaQuery = {}, ...responseData } = response || {}; - - const updatedResponseData = { - error: responseData.error || false, - fulfilled: false, - pending: responseData.pending || responseData.cancelled || false, - listData: [], - itemCount: 0, - query, - status: responseData.status - }; - - const cache = - (viewId && productId && selectorCache.get(`${viewId}_${productId}_${JSON.stringify(query)}`)) || undefined; - - Object.assign(updatedResponseData, { ...cache }); - - if (responseData.fulfilled && productId === metaId && _isEqual(query, metaQuery)) { - const { - [rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_DATA]: listData = [], - [rhsmApiTypes.RHSM_API_RESPONSE_META]: listMeta = {} - } = responseData.data || {}; - - updatedResponseData.listData.length = 0; - - // Apply "display logic" then return a custom value for entries - const customInventoryValue = ({ key, value }) => { - switch (key) { - case rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.NEXT_EVENT_DATE: - return (value && new Date(value)) || null; - case rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES.UOM: - return value?.toLowerCase() || null; - default: - return value ?? null; - } - }; - - // Generate normalized properties - const [updatedListData, updatedListMeta] = reduxHelpers.setNormalizedResponse( - { - schema: rhsmApiTypes.RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES, - data: listData, - customResponseValue: customInventoryValue - }, - { - schema: rhsmApiTypes.RHSM_API_RESPONSE_META_TYPES, - data: listMeta - } - ); - - 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.set(`${viewId}_${productId}_${JSON.stringify(query)}`, { ...updatedResponseData }); - } - - return updatedResponseData; -}); - -/** - * Expose selector instance. For scenarios where a selector is reused across component instances. - * - * @param {object} defaultProps - * @returns {{pending: boolean, fulfilled: boolean, graphData: object, error: boolean, session: object, - * status: (*|number)}} - */ -const makeSelector = defaultProps => (state, props) => ({ - ...userSession(state, props, defaultProps), - ...selector(state, props, defaultProps) -}); - -const subscriptionsListSelectors = { - subscriptionsList: selector, - makeSubscriptionsList: makeSelector -}; - -export { subscriptionsListSelectors as default, subscriptionsListSelectors, selector, makeSelector }; diff --git a/src/redux/types/__tests__/__snapshots__/index.test.js.snap b/src/redux/types/__tests__/__snapshots__/index.test.js.snap index eff19c7d7..f7c8ea63f 100644 --- a/src/redux/types/__tests__/__snapshots__/index.test.js.snap +++ b/src/redux/types/__tests__/__snapshots__/index.test.js.snap @@ -29,8 +29,13 @@ Object { }, "query": Object { "SET_QUERY_CLEAR": "SET_QUERY_CLEAR", + "SET_QUERY_CLEAR_INVENTORY_GUESTS_LIST": "SET_QUERY_CLEAR_INVENTORY_GUESTS_LIST", "SET_QUERY_CLEAR_INVENTORY_LIST": "SET_QUERY_CLEAR_INVENTORY_LIST", "SET_QUERY_RESET_INVENTORY_LIST": "SET_QUERY_RESET_INVENTORY_LIST", + "SET_QUERY_RHSM_GUESTS_INVENTORY_TYPES": Object { + "limit": "SET_QUERY_RHSM_GUESTS_INVENTORY_limit", + "offset": "SET_QUERY_RHSM_GUESTS_INVENTORY_offset", + }, "SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES": Object { "dir": "SET_QUERY_RHSM_HOSTS_INVENTORY_dir", "display_name_contains": "SET_QUERY_RHSM_HOSTS_INVENTORY_display_name_contains", @@ -93,8 +98,13 @@ Object { }, "queryTypes": Object { "SET_QUERY_CLEAR": "SET_QUERY_CLEAR", + "SET_QUERY_CLEAR_INVENTORY_GUESTS_LIST": "SET_QUERY_CLEAR_INVENTORY_GUESTS_LIST", "SET_QUERY_CLEAR_INVENTORY_LIST": "SET_QUERY_CLEAR_INVENTORY_LIST", "SET_QUERY_RESET_INVENTORY_LIST": "SET_QUERY_RESET_INVENTORY_LIST", + "SET_QUERY_RHSM_GUESTS_INVENTORY_TYPES": Object { + "limit": "SET_QUERY_RHSM_GUESTS_INVENTORY_limit", + "offset": "SET_QUERY_RHSM_GUESTS_INVENTORY_offset", + }, "SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES": Object { "dir": "SET_QUERY_RHSM_HOSTS_INVENTORY_dir", "display_name_contains": "SET_QUERY_RHSM_HOSTS_INVENTORY_display_name_contains", @@ -140,8 +150,13 @@ Object { }, "query": Object { "SET_QUERY_CLEAR": "SET_QUERY_CLEAR", + "SET_QUERY_CLEAR_INVENTORY_GUESTS_LIST": "SET_QUERY_CLEAR_INVENTORY_GUESTS_LIST", "SET_QUERY_CLEAR_INVENTORY_LIST": "SET_QUERY_CLEAR_INVENTORY_LIST", "SET_QUERY_RESET_INVENTORY_LIST": "SET_QUERY_RESET_INVENTORY_LIST", + "SET_QUERY_RHSM_GUESTS_INVENTORY_TYPES": Object { + "limit": "SET_QUERY_RHSM_GUESTS_INVENTORY_limit", + "offset": "SET_QUERY_RHSM_GUESTS_INVENTORY_offset", + }, "SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES": Object { "dir": "SET_QUERY_RHSM_HOSTS_INVENTORY_dir", "display_name_contains": "SET_QUERY_RHSM_HOSTS_INVENTORY_display_name_contains", @@ -234,8 +249,13 @@ Object { }, "query": Object { "SET_QUERY_CLEAR": "SET_QUERY_CLEAR", + "SET_QUERY_CLEAR_INVENTORY_GUESTS_LIST": "SET_QUERY_CLEAR_INVENTORY_GUESTS_LIST", "SET_QUERY_CLEAR_INVENTORY_LIST": "SET_QUERY_CLEAR_INVENTORY_LIST", "SET_QUERY_RESET_INVENTORY_LIST": "SET_QUERY_RESET_INVENTORY_LIST", + "SET_QUERY_RHSM_GUESTS_INVENTORY_TYPES": Object { + "limit": "SET_QUERY_RHSM_GUESTS_INVENTORY_limit", + "offset": "SET_QUERY_RHSM_GUESTS_INVENTORY_offset", + }, "SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES": Object { "dir": "SET_QUERY_RHSM_HOSTS_INVENTORY_dir", "display_name_contains": "SET_QUERY_RHSM_HOSTS_INVENTORY_display_name_contains", diff --git a/src/redux/types/queryTypes.js b/src/redux/types/queryTypes.js index 7dd6c659e..8d07f23e0 100644 --- a/src/redux/types/queryTypes.js +++ b/src/redux/types/queryTypes.js @@ -2,6 +2,7 @@ import { RHSM_API_QUERY_TYPES } from '../../types/rhsmApiTypes'; const SET_QUERY_CLEAR = 'SET_QUERY_CLEAR'; const SET_QUERY_CLEAR_INVENTORY_LIST = 'SET_QUERY_CLEAR_INVENTORY_LIST'; +const SET_QUERY_CLEAR_INVENTORY_GUESTS_LIST = 'SET_QUERY_CLEAR_INVENTORY_GUESTS_LIST'; const SET_QUERY_RESET_INVENTORY_LIST = 'SET_QUERY_RESET_INVENTORY_LIST'; const SET_QUERY_RHSM_TYPES = { @@ -13,6 +14,11 @@ const SET_QUERY_RHSM_TYPES = { [RHSM_API_QUERY_TYPES.USAGE]: `SET_QUERY_RHSM_${RHSM_API_QUERY_TYPES.USAGE}` }; +const SET_QUERY_RHSM_GUESTS_INVENTORY_TYPES = { + [RHSM_API_QUERY_TYPES.LIMIT]: `SET_QUERY_RHSM_GUESTS_INVENTORY_${RHSM_API_QUERY_TYPES.LIMIT}`, + [RHSM_API_QUERY_TYPES.OFFSET]: `SET_QUERY_RHSM_GUESTS_INVENTORY_${RHSM_API_QUERY_TYPES.OFFSET}` +}; + const SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES = { [RHSM_API_QUERY_TYPES.DIRECTION]: `SET_QUERY_RHSM_HOSTS_INVENTORY_${RHSM_API_QUERY_TYPES.DIRECTION}`, [RHSM_API_QUERY_TYPES.DISPLAY_NAME]: `SET_QUERY_RHSM_HOSTS_INVENTORY_${RHSM_API_QUERY_TYPES.DISPLAY_NAME}`, @@ -32,14 +38,17 @@ const SET_QUERY_RHSM_SUBSCRIPTIONS_INVENTORY_TYPES = { * Query/filter reducer types. * * @type {{SET_QUERY_RHSM_SUBSCRIPTIONS_INVENTORY_TYPES: object, SET_QUERY_RHSM_TYPES: object, - * SET_QUERY_CLEAR: string, SET_QUERY_CLEAR_INVENTORY_LIST: string, - * SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES: object, SET_QUERY_RESET_INVENTORY_LIST: string}} + * SET_QUERY_RHSM_GUESTS_INVENTORY_TYPES: object, SET_QUERY_CLEAR: string, SET_QUERY_CLEAR_INVENTORY_LIST: string, + * SET_QUERY_CLEAR_INVENTORY_GUESTS_LIST: string, SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES: object, + * SET_QUERY_RESET_INVENTORY_LIST: string}} */ const queryTypes = { SET_QUERY_CLEAR, SET_QUERY_CLEAR_INVENTORY_LIST, + SET_QUERY_CLEAR_INVENTORY_GUESTS_LIST, SET_QUERY_RESET_INVENTORY_LIST, SET_QUERY_RHSM_TYPES, + SET_QUERY_RHSM_GUESTS_INVENTORY_TYPES, SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES, SET_QUERY_RHSM_SUBSCRIPTIONS_INVENTORY_TYPES }; @@ -49,8 +58,10 @@ export { queryTypes, SET_QUERY_CLEAR, SET_QUERY_CLEAR_INVENTORY_LIST, + SET_QUERY_CLEAR_INVENTORY_GUESTS_LIST, SET_QUERY_RESET_INVENTORY_LIST, SET_QUERY_RHSM_TYPES, + SET_QUERY_RHSM_GUESTS_INVENTORY_TYPES, SET_QUERY_RHSM_HOSTS_INVENTORY_TYPES, SET_QUERY_RHSM_SUBSCRIPTIONS_INVENTORY_TYPES }; diff --git a/src/services/rhsm/__tests__/__snapshots__/rhsmConstants.test.js.snap b/src/services/rhsm/__tests__/__snapshots__/rhsmConstants.test.js.snap index 248034a31..72c760521 100644 --- a/src/services/rhsm/__tests__/__snapshots__/rhsmConstants.test.js.snap +++ b/src/services/rhsm/__tests__/__snapshots__/rhsmConstants.test.js.snap @@ -48,6 +48,14 @@ Object { "STORAGE_GIBIBYTES": "Storage-gibibytes", "TRANSFER_GIBIBYTES": "Transfer-gibibytes", }, + "RHSM_API_QUERY_INVENTORY_SUBSCRIPTIONS_SORT_TYPES": Object { + "NEXT_EVENT_DATE": "next_event_date", + "NEXT_EVENT_TYPE": "next_event_type", + "QUANTITY": "quantity", + "SERVICE_LEVEL": "service_level", + "SKU": "sku", + "USAGE": "usage", + }, "RHSM_API_QUERY_SET_INVENTORY_TYPES": Object { "DIRECTION": "dir", "DISPLAY_NAME": "display_name_contains", @@ -112,6 +120,7 @@ Object { "INVENTORY_ID": "inventory_id", "LAST_SEEN": "last_seen", "MEASUREMENTS": "measurements", + "NUMBER_OF_GUESTS": "number_of_guests", "SUBSCRIPTION_MANAGER_ID": "subscription_manager_id", }, "RHSM_API_RESPONSE_INSTANCES_META_TYPES": Object { @@ -203,6 +212,14 @@ Object { "STORAGE_GIBIBYTES": "Storage-gibibytes", "TRANSFER_GIBIBYTES": "Transfer-gibibytes", }, + "RHSM_API_QUERY_INVENTORY_SUBSCRIPTIONS_SORT_TYPES": Object { + "NEXT_EVENT_DATE": "next_event_date", + "NEXT_EVENT_TYPE": "next_event_type", + "QUANTITY": "quantity", + "SERVICE_LEVEL": "service_level", + "SKU": "sku", + "USAGE": "usage", + }, "RHSM_API_QUERY_SET_INVENTORY_TYPES": Object { "DIRECTION": "dir", "DISPLAY_NAME": "display_name_contains", @@ -267,6 +284,7 @@ Object { "INVENTORY_ID": "inventory_id", "LAST_SEEN": "last_seen", "MEASUREMENTS": "measurements", + "NUMBER_OF_GUESTS": "number_of_guests", "SUBSCRIPTION_MANAGER_ID": "subscription_manager_id", }, "RHSM_API_RESPONSE_INSTANCES_META_TYPES": Object { @@ -359,6 +377,14 @@ Object { "STORAGE_GIBIBYTES": "Storage-gibibytes", "TRANSFER_GIBIBYTES": "Transfer-gibibytes", }, + "RHSM_API_QUERY_INVENTORY_SUBSCRIPTIONS_SORT_TYPES": Object { + "NEXT_EVENT_DATE": "next_event_date", + "NEXT_EVENT_TYPE": "next_event_type", + "QUANTITY": "quantity", + "SERVICE_LEVEL": "service_level", + "SKU": "sku", + "USAGE": "usage", + }, "RHSM_API_QUERY_SET_INVENTORY_TYPES": Object { "DIRECTION": "dir", "DISPLAY_NAME": "display_name_contains", @@ -423,6 +449,7 @@ Object { "INVENTORY_ID": "inventory_id", "LAST_SEEN": "last_seen", "MEASUREMENTS": "measurements", + "NUMBER_OF_GUESTS": "number_of_guests", "SUBSCRIPTION_MANAGER_ID": "subscription_manager_id", }, "RHSM_API_RESPONSE_INSTANCES_META_TYPES": Object { @@ -519,6 +546,14 @@ Object { "STORAGE_GIBIBYTES": "Storage-gibibytes", "TRANSFER_GIBIBYTES": "Transfer-gibibytes", }, + "RHSM_API_QUERY_INVENTORY_SUBSCRIPTIONS_SORT_TYPES": Object { + "NEXT_EVENT_DATE": "next_event_date", + "NEXT_EVENT_TYPE": "next_event_type", + "QUANTITY": "quantity", + "SERVICE_LEVEL": "service_level", + "SKU": "sku", + "USAGE": "usage", + }, "RHSM_API_QUERY_SET_INVENTORY_TYPES": Object { "DIRECTION": "dir", "DISPLAY_NAME": "display_name_contains", @@ -583,6 +618,7 @@ Object { "INVENTORY_ID": "inventory_id", "LAST_SEEN": "last_seen", "MEASUREMENTS": "measurements", + "NUMBER_OF_GUESTS": "number_of_guests", "SUBSCRIPTION_MANAGER_ID": "subscription_manager_id", }, "RHSM_API_RESPONSE_INSTANCES_META_TYPES": Object { diff --git a/src/services/rhsm/__tests__/__snapshots__/rhsmSchemas.test.js.snap b/src/services/rhsm/__tests__/__snapshots__/rhsmSchemas.test.js.snap index d65fdc6e1..5088dafe6 100644 --- a/src/services/rhsm/__tests__/__snapshots__/rhsmSchemas.test.js.snap +++ b/src/services/rhsm/__tests__/__snapshots__/rhsmSchemas.test.js.snap @@ -3,7 +3,9 @@ exports[`RHSM Schemas should have specific RHSM response schemas: specific schemas 1`] = ` Object { "errors": [Function], + "guests": [Function], "instances": [Function], + "subscriptions": [Function], "tally": [Function], } `; @@ -14,7 +16,23 @@ Object { } `; -exports[`RHSM Schemas should have valid RHSM schemas that validate API responses: response schema 1`] = ` +exports[`RHSM Schemas should have valid RHSM schemas that validate API responses: guests response schema 1`] = ` +Object { + "data": Array [], + "links": Object {}, + "meta": Object {}, +} +`; + +exports[`RHSM Schemas should have valid RHSM schemas that validate API responses: instances response schema 1`] = ` +Object { + "data": Array [], + "links": Object {}, + "meta": Object {}, +} +`; + +exports[`RHSM Schemas should have valid RHSM schemas that validate API responses: tally response schema 1`] = ` Object { "data": Array [], "links": Object {}, diff --git a/src/services/rhsm/__tests__/__snapshots__/rhsmTransformers.test.js.snap b/src/services/rhsm/__tests__/__snapshots__/rhsmTransformers.test.js.snap index c483067f0..f2d51f72d 100644 --- a/src/services/rhsm/__tests__/__snapshots__/rhsmTransformers.test.js.snap +++ b/src/services/rhsm/__tests__/__snapshots__/rhsmTransformers.test.js.snap @@ -21,6 +21,8 @@ Object { "a": 0.0003456, "b": 2, "c": 1000, + "numberOfGuests": undefined, + "subscriptionManagerId": undefined, }, ], "meta": Object { diff --git a/src/services/rhsm/__tests__/rhsmSchemas.test.js b/src/services/rhsm/__tests__/rhsmSchemas.test.js index 8370ce77f..cdbe0357a 100644 --- a/src/services/rhsm/__tests__/rhsmSchemas.test.js +++ b/src/services/rhsm/__tests__/rhsmSchemas.test.js @@ -7,6 +7,8 @@ describe('RHSM Schemas', () => { it('should have valid RHSM schemas that validate API responses', () => { expect(rhsmSchemas.errors({})).toMatchSnapshot('error schema'); - expect(rhsmSchemas.tally({})).toMatchSnapshot('response schema'); + expect(rhsmSchemas.tally({})).toMatchSnapshot('tally response schema'); + expect(rhsmSchemas.instances({})).toMatchSnapshot('instances response schema'); + expect(rhsmSchemas.guests({})).toMatchSnapshot('guests response schema'); }); }); diff --git a/src/services/rhsm/rhsmConstants.js b/src/services/rhsm/rhsmConstants.js index 7e95c7236..378c6acca 100644 --- a/src/services/rhsm/rhsmConstants.js +++ b/src/services/rhsm/rhsmConstants.js @@ -72,14 +72,15 @@ const RHSM_API_RESPONSE_META_TYPES = { /** * RHSM response Instance DATA types. * - * @type {{MEASUREMENTS: string, SUBSCRIPTION_MANAGER_ID: string, INVENTORY_ID: string, DISPLAY_NAME: string, - * LAST_SEEN: string}} + * @type {{MEASUREMENTS: string, SUBSCRIPTION_MANAGER_ID: string, INVENTORY_ID: string, NUMBER_OF_GUESTS: string, + * DISPLAY_NAME: string, LAST_SEEN: string}} */ const RHSM_API_RESPONSE_INSTANCES_DATA_TYPES = { DISPLAY_NAME: 'display_name', INVENTORY_ID: 'inventory_id', LAST_SEEN: 'last_seen', MEASUREMENTS: 'measurements', + NUMBER_OF_GUESTS: 'number_of_guests', SUBSCRIPTION_MANAGER_ID: 'subscription_manager_id' }; @@ -207,6 +208,21 @@ const RHSM_API_QUERY_INVENTORY_SORT_DIRECTION_TYPES = { DESCENDING: 'desc' }; +/** + * RHSM API query/search parameter SORT type values for SUBSCRIPTIONS. + * + * @type {{QUANTITY: string, USAGE: string, NEXT_EVENT_TYPE: string, NEXT_EVENT_DATE: string, SKU: string, + * SERVICE_LEVEL: string}} + */ +const RHSM_API_QUERY_INVENTORY_SUBSCRIPTIONS_SORT_TYPES = { + NEXT_EVENT_DATE: 'next_event_date', + NEXT_EVENT_TYPE: 'next_event_type', + QUANTITY: 'quantity', + SKU: 'sku', + SERVICE_LEVEL: 'service_level', + USAGE: 'usage' +}; + const RHSM_API_QUERY_SLA_TYPES = RHSM_API_RESPONSE_SLA_TYPES; const RHSM_API_QUERY_UOM_TYPES = RHSM_API_RESPONSE_UOM_TYPES; @@ -297,6 +313,7 @@ const rhsmConstants = { RHSM_API_QUERY_GRANULARITY_TYPES, RHSM_API_QUERY_INVENTORY_SORT_TYPES, RHSM_API_QUERY_INVENTORY_SORT_DIRECTION_TYPES, + RHSM_API_QUERY_INVENTORY_SUBSCRIPTIONS_SORT_TYPES, RHSM_API_QUERY_SLA_TYPES, RHSM_API_QUERY_UOM_TYPES, RHSM_API_QUERY_USAGE_TYPES, @@ -325,6 +342,7 @@ export { RHSM_API_QUERY_GRANULARITY_TYPES, RHSM_API_QUERY_INVENTORY_SORT_TYPES, RHSM_API_QUERY_INVENTORY_SORT_DIRECTION_TYPES, + RHSM_API_QUERY_INVENTORY_SUBSCRIPTIONS_SORT_TYPES, RHSM_API_QUERY_SLA_TYPES, RHSM_API_QUERY_UOM_TYPES, RHSM_API_QUERY_USAGE_TYPES, diff --git a/src/services/rhsm/rhsmSchemas.js b/src/services/rhsm/rhsmSchemas.js index 08b996212..bd9496107 100644 --- a/src/services/rhsm/rhsmSchemas.js +++ b/src/services/rhsm/rhsmSchemas.js @@ -40,6 +40,42 @@ const metaResponseSchema = Joi.object() }) .unknown(true); +/** + * Guests response meta field. + * + * @type {*} Joi schema + */ +const guestsMetaSchema = Joi.object() + .keys({ + count: Joi.number().integer().default(0) + }) + .unknown(true); + +/** + * Instances response item. + * + * @type {*} Joi schema + */ +const guestsItem = Joi.object({ + inventory_id: Joi.string().optional().allow(null), + display_name: Joi.string().optional().allow(null), + subscription_manager_id: Joi.string().optional().allow(null), + last_seen: Joi.date().utc().allow(null) +}) + .unknown(true) + .default(); + +/** + * Instances response. + * + * @type {*} Joi schema + */ +const guestsResponseSchema = Joi.object().keys({ + data: Joi.array().items(guestsItem).default([]), + links: linksSchema.default({}), + meta: guestsMetaSchema.default({}) +}); + /** * Instances response meta field. * @@ -79,6 +115,42 @@ const instancesResponseSchema = Joi.object().keys({ meta: instancesMetaSchema.default({}) }); +/** + * Subscriptions response meta field. + * + * @type {*} Joi schema + */ +const subscriptionsMetaSchema = metaResponseSchema; + +/** + * Subscriptions response item. + * + * @type {*} Joi schema + */ +const subscriptionsItem = Joi.object({ + next_event_date: Joi.date().utc().allow(null), + product_name: Joi.string().optional().allow(null), + quantity: Joi.number().allow(null).default(0), + service_level: Joi.string().valid(...Object.values(rhsmConstants.RHSM_API_RESPONSE_SLA_TYPES)), + total_capacity: Joi.number().allow(null).default(0), + uom: Joi.string() + .lowercase() + .valid(...Object.values(rhsmConstants.RHSM_API_RESPONSE_UOM_TYPES)) +}) + .unknown(true) + .default(); + +/** + * Subscriptions response. + * + * @type {*} Joi schema + */ +const subscriptionsResponseSchema = Joi.object().keys({ + data: Joi.array().items(subscriptionsItem).default([]), + links: linksSchema.default({}), + meta: subscriptionsMetaSchema.default({}) +}); + /** * Tally response item. * @@ -119,7 +191,10 @@ const tallyResponseSchema = Joi.object().keys({ const rhsmSchemas = { errors: response => schemaResponse({ response, schema: errorResponseSchema, id: 'RHSM errors' }), + guests: response => schemaResponse({ response, casing: 'camel', schema: guestsResponseSchema, id: 'RHSM guests' }), instances: response => schemaResponse({ response, schema: instancesResponseSchema, id: 'RHSM instances' }), + subscriptions: response => + schemaResponse({ response, casing: 'camel', schema: subscriptionsResponseSchema, id: 'RHSM subscriptions' }), tally: response => schemaResponse({ response, schema: tallyResponseSchema, id: 'RHSM tally' }) }; diff --git a/src/services/rhsm/rhsmServices.js b/src/services/rhsm/rhsmServices.js index c0ddfe069..96c6bc559 100644 --- a/src/services/rhsm/rhsmServices.js +++ b/src/services/rhsm/rhsmServices.js @@ -1490,6 +1490,7 @@ const getGraphReports = (id, params = {}, options = {}) => { * @param {string|Array} id String ID, or an array of IDs * @param {object} params Query/search params * @param {object} options + * @param {boolean} options.cache * @param {boolean} options.cancel * @param {string} options.cancelId * @param {Array} options.schema An array of callbacks used to transform the response, ie. [SUCCESS SCHEMA, ERROR SCHEMA] @@ -2142,18 +2143,30 @@ const getHostsInventory = (id, params = {}, options = {}) => { * @param {string} id Subscription Manager ID * @param {object} params Query/search params * @param {object} options + * @param {boolean} options.cache * @param {boolean} options.cancel * @param {string} options.cancelId + * @param {Array} options.schema An array of callbacks used to transform the response, ie. [SUCCESS SCHEMA, ERROR SCHEMA] + * @param {Array} options.transform An array of callbacks used to transform the response, ie. [SUCCESS TRANSFORM, ERROR TRANSFORM] * @returns {Promise<*>} */ const getHostsInventoryGuests = (id, params = {}, options = {}) => { - const { cache = true, cancel = false, cancelId } = options; + const { + cache = true, + cancel = false, + cancelId, + schema = [rhsmSchemas.guests, rhsmSchemas.errors], + transform = [] + } = options; + return serviceCall({ url: process.env.REACT_APP_SERVICES_RHSM_INVENTORY_GUESTS.replace('{0}', id), params, cache, cancel, - cancelId + cancelId, + schema, + transform }); }; @@ -2169,6 +2182,7 @@ const getHostsInventoryGuests = (id, params = {}, options = {}) => { * { * "data" : [ * { + * "number_of_guests": 70, * "inventory_id": "d6214a0b-b344-4778-831c-d53dcacb2da3", * "subscription_manager_id": "adafd9d5-5b00-42fa-a6c9-75801d45cc6d", * "display_name": "rhv.example.com", @@ -2236,8 +2250,11 @@ const getHostsInventoryGuests = (id, params = {}, options = {}) => { * @param {string} id Product ID * @param {object} params Query/search params * @param {object} options + * @param {boolean} options.cache * @param {boolean} options.cancel * @param {string} options.cancelId + * @param {Array} options.schema An array of callbacks used to transform the response, ie. [SUCCESS SCHEMA, ERROR SCHEMA] + * @param {Array} options.transform An array of callbacks used to transform the response, ie. [SUCCESS TRANSFORM, ERROR TRANSFORM] * @returns {Promise<*>} */ const getInstancesInventory = (id, params = {}, options = {}) => { @@ -2261,7 +2278,7 @@ const getInstancesInventory = (id, params = {}, options = {}) => { }; /** - * @apiMock {DelayResponse} 250 + * @apiMock {DelayResponse} 0 * @api {get} /api/rhsm-subscriptions/v1/subscriptions/products/:product_id Get RHSM subscriptions table/inventory data * @apiDescription Retrieve subscriptions table/inventory data. * @@ -2346,18 +2363,30 @@ const getInstancesInventory = (id, params = {}, options = {}) => { * @param {string} id Product ID * @param {object} params Query/search params * @param {object} options + * @param {boolean} options.cache * @param {boolean} options.cancel * @param {string} options.cancelId + * @param {Array} options.schema An array of callbacks used to transform the response, ie. [SUCCESS SCHEMA, ERROR SCHEMA] + * @param {Array} options.transform An array of callbacks used to transform the response, ie. [SUCCESS TRANSFORM, ERROR TRANSFORM] * @returns {Promise<*>} */ const getSubscriptionsInventory = (id, params = {}, options = {}) => { - const { cache = true, cancel = true, cancelId } = options; + const { + cache = true, + cancel = true, + cancelId, + schema = [rhsmSchemas.subscriptions, rhsmSchemas.errors], + transform = [] + } = options; + return serviceCall({ url: `${process.env.REACT_APP_SERVICES_RHSM_INVENTORY_SUBSCRIPTIONS}${id}`, params, cache, cancel, - cancelId + cancelId, + schema, + transform }); }; diff --git a/src/services/rhsm/rhsmTranformers.js b/src/services/rhsm/rhsmTranformers.js index 75409b7c9..4d8974265 100644 --- a/src/services/rhsm/rhsmTranformers.js +++ b/src/services/rhsm/rhsmTranformers.js @@ -23,17 +23,26 @@ const rhsmInstances = response => { response || {}; const metaMeasurements = meta[INSTANCES_META_TYPES.MEASUREMENTS]; - updatedResponse.data = data.map(({ [INSTANCES_DATA_TYPES.MEASUREMENTS]: measurements, ...dataResponse }) => { - const updatedData = { + updatedResponse.data = data.map( + ({ + [INSTANCES_DATA_TYPES.MEASUREMENTS]: measurements, + [INSTANCES_DATA_TYPES.SUBSCRIPTION_MANAGER_ID]: subscriptionManagerId, + [INSTANCES_DATA_TYPES.NUMBER_OF_GUESTS]: numberOfGuests, ...dataResponse - }; + }) => { + const updatedData = { + numberOfGuests, + subscriptionManagerId, + ...dataResponse + }; - metaMeasurements?.forEach((measurement, index) => { - updatedData[measurement] = measurements[index]; - }); + metaMeasurements?.forEach((measurement, index) => { + updatedData[measurement] = measurements[index]; + }); - return updatedData; - }); + return updatedData; + } + ); updatedResponse.meta = { count: meta[INSTANCES_META_TYPES.COUNT], diff --git a/src/types/__tests__/__snapshots__/index.test.js.snap b/src/types/__tests__/__snapshots__/index.test.js.snap index d2832b7fb..104bacc91 100644 --- a/src/types/__tests__/__snapshots__/index.test.js.snap +++ b/src/types/__tests__/__snapshots__/index.test.js.snap @@ -60,8 +60,16 @@ Object { "WEEKLY": "Weekly", }, "RHSM_API_QUERY_SET_INVENTORY_GUESTS_TYPES": Object { + "DIRECTION": "dir", + "DISPLAY_NAME": "display_name_contains", + "END_DATE": "ending", "LIMIT": "limit", "OFFSET": "offset", + "SLA": "sla", + "SORT": "sort", + "START_DATE": "beginning", + "UOM": "uom", + "USAGE": "usage", }, "RHSM_API_QUERY_SET_INVENTORY_SUBSCRIPTIONS_TYPES": Object { "DIRECTION": "dir", @@ -119,10 +127,6 @@ Object { "NAME": "display_name", "SOCKETS": "sockets", }, - "RHSM_API_QUERY_SUBSCRIPTIONS_EVENT_TYPES": Object { - "EVENT_END": "Subscription End", - "EVENT_START": "Subscription Start", - }, "RHSM_API_QUERY_SUBSCRIPTIONS_SORT_TYPES": Object { "NEXT_EVENT_DATE": "next_event_date", "NEXT_EVENT_TYPE": "next_event_type", @@ -202,20 +206,6 @@ Object { "NAME": "display_name", "SUBSCRIPTION_ID": "subscription_manager_id", }, - "RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES": Object { - "NEXT_EVENT_DATE": "next_event_date", - "NEXT_EVENT_TYPE": "next_event_type", - "PHYSICAL_CAPACITY": "physical_capacity", - "PRODUCT_NAME": "product_name", - "QUANTITY": "quantity", - "SERVICE_LEVEL": "service_level", - "SKU": "sku", - "SUBSCRIPTIONS": "subscriptions", - "TOTAL_CAPACITY": "total_capacity", - "UOM": "uom", - "USAGE": "usage", - "VIRTUAL_CAPACITY": "virtual_capacity", - }, "RHSM_API_RESPONSE_LINKS": "links", "RHSM_API_RESPONSE_LINKS_TYPES": Object { "FIRST": "first", @@ -307,8 +297,16 @@ Object { "WEEKLY": "Weekly", }, "RHSM_API_QUERY_SET_INVENTORY_GUESTS_TYPES": Object { + "DIRECTION": "dir", + "DISPLAY_NAME": "display_name_contains", + "END_DATE": "ending", "LIMIT": "limit", "OFFSET": "offset", + "SLA": "sla", + "SORT": "sort", + "START_DATE": "beginning", + "UOM": "uom", + "USAGE": "usage", }, "RHSM_API_QUERY_SET_INVENTORY_SUBSCRIPTIONS_TYPES": Object { "DIRECTION": "dir", @@ -366,10 +364,6 @@ Object { "NAME": "display_name", "SOCKETS": "sockets", }, - "RHSM_API_QUERY_SUBSCRIPTIONS_EVENT_TYPES": Object { - "EVENT_END": "Subscription End", - "EVENT_START": "Subscription Start", - }, "RHSM_API_QUERY_SUBSCRIPTIONS_SORT_TYPES": Object { "NEXT_EVENT_DATE": "next_event_date", "NEXT_EVENT_TYPE": "next_event_type", @@ -449,20 +443,6 @@ Object { "NAME": "display_name", "SUBSCRIPTION_ID": "subscription_manager_id", }, - "RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES": Object { - "NEXT_EVENT_DATE": "next_event_date", - "NEXT_EVENT_TYPE": "next_event_type", - "PHYSICAL_CAPACITY": "physical_capacity", - "PRODUCT_NAME": "product_name", - "QUANTITY": "quantity", - "SERVICE_LEVEL": "service_level", - "SKU": "sku", - "SUBSCRIPTIONS": "subscriptions", - "TOTAL_CAPACITY": "total_capacity", - "UOM": "uom", - "USAGE": "usage", - "VIRTUAL_CAPACITY": "virtual_capacity", - }, "RHSM_API_RESPONSE_LINKS": "links", "RHSM_API_RESPONSE_LINKS_TYPES": Object { "FIRST": "first", @@ -553,8 +533,16 @@ Object { "WEEKLY": "Weekly", }, "RHSM_API_QUERY_SET_INVENTORY_GUESTS_TYPES": Object { + "DIRECTION": "dir", + "DISPLAY_NAME": "display_name_contains", + "END_DATE": "ending", "LIMIT": "limit", "OFFSET": "offset", + "SLA": "sla", + "SORT": "sort", + "START_DATE": "beginning", + "UOM": "uom", + "USAGE": "usage", }, "RHSM_API_QUERY_SET_INVENTORY_SUBSCRIPTIONS_TYPES": Object { "DIRECTION": "dir", @@ -612,10 +600,6 @@ Object { "NAME": "display_name", "SOCKETS": "sockets", }, - "RHSM_API_QUERY_SUBSCRIPTIONS_EVENT_TYPES": Object { - "EVENT_END": "Subscription End", - "EVENT_START": "Subscription Start", - }, "RHSM_API_QUERY_SUBSCRIPTIONS_SORT_TYPES": Object { "NEXT_EVENT_DATE": "next_event_date", "NEXT_EVENT_TYPE": "next_event_type", @@ -695,20 +679,6 @@ Object { "NAME": "display_name", "SUBSCRIPTION_ID": "subscription_manager_id", }, - "RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES": Object { - "NEXT_EVENT_DATE": "next_event_date", - "NEXT_EVENT_TYPE": "next_event_type", - "PHYSICAL_CAPACITY": "physical_capacity", - "PRODUCT_NAME": "product_name", - "QUANTITY": "quantity", - "SERVICE_LEVEL": "service_level", - "SKU": "sku", - "SUBSCRIPTIONS": "subscriptions", - "TOTAL_CAPACITY": "total_capacity", - "UOM": "uom", - "USAGE": "usage", - "VIRTUAL_CAPACITY": "virtual_capacity", - }, "RHSM_API_RESPONSE_LINKS": "links", "RHSM_API_RESPONSE_LINKS_TYPES": Object { "FIRST": "first", @@ -803,8 +773,16 @@ Object { "WEEKLY": "Weekly", }, "RHSM_API_QUERY_SET_INVENTORY_GUESTS_TYPES": Object { + "DIRECTION": "dir", + "DISPLAY_NAME": "display_name_contains", + "END_DATE": "ending", "LIMIT": "limit", "OFFSET": "offset", + "SLA": "sla", + "SORT": "sort", + "START_DATE": "beginning", + "UOM": "uom", + "USAGE": "usage", }, "RHSM_API_QUERY_SET_INVENTORY_SUBSCRIPTIONS_TYPES": Object { "DIRECTION": "dir", @@ -862,10 +840,6 @@ Object { "NAME": "display_name", "SOCKETS": "sockets", }, - "RHSM_API_QUERY_SUBSCRIPTIONS_EVENT_TYPES": Object { - "EVENT_END": "Subscription End", - "EVENT_START": "Subscription Start", - }, "RHSM_API_QUERY_SUBSCRIPTIONS_SORT_TYPES": Object { "NEXT_EVENT_DATE": "next_event_date", "NEXT_EVENT_TYPE": "next_event_type", @@ -945,20 +919,6 @@ Object { "NAME": "display_name", "SUBSCRIPTION_ID": "subscription_manager_id", }, - "RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES": Object { - "NEXT_EVENT_DATE": "next_event_date", - "NEXT_EVENT_TYPE": "next_event_type", - "PHYSICAL_CAPACITY": "physical_capacity", - "PRODUCT_NAME": "product_name", - "QUANTITY": "quantity", - "SERVICE_LEVEL": "service_level", - "SKU": "sku", - "SUBSCRIPTIONS": "subscriptions", - "TOTAL_CAPACITY": "total_capacity", - "UOM": "uom", - "USAGE": "usage", - "VIRTUAL_CAPACITY": "virtual_capacity", - }, "RHSM_API_RESPONSE_LINKS": "links", "RHSM_API_RESPONSE_LINKS_TYPES": Object { "FIRST": "first", diff --git a/src/types/rhsmApiTypes.js b/src/types/rhsmApiTypes.js index d92771b58..fcb7993e0 100644 --- a/src/types/rhsmApiTypes.js +++ b/src/types/rhsmApiTypes.js @@ -1,5 +1,8 @@ import { rhsmConstants } from '../services/rhsm/rhsmConstants'; +/** + * ToDo: Clean up params, see userReducer. + */ /** * RHSM response Error DATA type. * @@ -7,6 +10,9 @@ import { rhsmConstants } from '../services/rhsm/rhsmConstants'; */ const RHSM_API_RESPONSE_ERROR_DATA = 'errors'; +/** + * ToDo: Clean up params, see userReducer. + */ /** * RHSM response Error DATA types. * Schema/map of expected Error data response properties. @@ -27,6 +33,9 @@ const RHSM_API_RESPONSE_ERROR_DATA_CODE_TYPES = { ...rhsmConstants.RHSM_API_RESPONSE_ERROR_CODE_TYPES }; +/** + * ToDo: Clean up params, unused. + */ /** * RHSM response links type. * @@ -34,6 +43,9 @@ const RHSM_API_RESPONSE_ERROR_DATA_CODE_TYPES = { */ const RHSM_API_RESPONSE_LINKS = 'links'; +/** + * ToDo: Clean up params, unused. + */ /** * RHSM response LINKS type. * Schema/map of expected inventory LINKS response properties. @@ -54,6 +66,10 @@ const RHSM_API_RESPONSE_LINKS_TYPES = { */ const RHSM_API_RESPONSE_META = 'meta'; +/** + * ToDo: Clean up params, used by deprecated graph component selector and deprecated inventory component selector. + * It appears some of these may need to move over towards rhsmSchemas.js + */ /** * RHSM response META types. * Schema/map of expected META response properties. @@ -80,6 +96,10 @@ const RHSM_API_RESPONSE_DATA = 'data'; */ const RHSM_API_RESPONSE_CAPACITY_DATA = RHSM_API_RESPONSE_DATA; +/** + * ToDo: Clean up params, used by deprecated graph component selector. + * We may need to relocate this as part of a "capacity" schema some of these params towards rhsmSchemas.js + */ /** * RHSM response Capacity DATA types. * Schema/map of expected Capacity data response properties. @@ -108,6 +128,9 @@ const RHSM_API_RESPONSE_CAPACITY_DATA_TYPES = { */ const RHSM_API_RESPONSE_INVENTORY_DATA = RHSM_API_RESPONSE_DATA; +/** + * ToDo: Clean up params, used by deprecated inventory component and related selector. + */ /** * RHSM response inventory DATA types. * Schema/map of expected inventory DATA response properties. @@ -131,6 +154,9 @@ const RHSM_API_RESPONSE_INVENTORY_DATA_TYPES = { SUBSCRIPTION_ID: 'subscription_manager_id' }; +/** + * ToDo: Clean up params, used by deprecated guests component selector. + */ /** * RHSM response inventory guests DATA types. * Schema/map of expected inventory guests DATA response properties. @@ -144,28 +170,6 @@ const RHSM_API_RESPONSE_INVENTORY_GUESTS_DATA_TYPES = { LAST_SEEN: 'last_seen' }; -/** - * RHSM response subscriptions inventory DATA types. - * - * @type {{UOM: string, SUBSCRIPTIONS: string, PHYSICAL_CAPACITY: string, USAGE: string, - * QUANTITY: string, NEXT_EVENT_TYPE: string, NEXT_EVENT_DATE: string, VIRTUAL_CAPACITY: string, - * TOTAL_CAPACITY: string, SKU: string, PRODUCT_NAME: string, SERVICE_LEVEL: string}} - */ -const RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES = { - SKU: 'sku', - PRODUCT_NAME: 'product_name', - SERVICE_LEVEL: 'service_level', - USAGE: 'usage', - SUBSCRIPTIONS: 'subscriptions', - NEXT_EVENT_DATE: 'next_event_date', - NEXT_EVENT_TYPE: 'next_event_type', - QUANTITY: 'quantity', - PHYSICAL_CAPACITY: 'physical_capacity', - VIRTUAL_CAPACITY: 'virtual_capacity', - TOTAL_CAPACITY: 'total_capacity', - UOM: 'uom' -}; - /** * RHSM response Reporting/Tally DATA type. * @@ -173,6 +177,10 @@ const RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES = { */ const RHSM_API_RESPONSE_PRODUCTS_DATA = RHSM_API_RESPONSE_DATA; +/** + * ToDo: Clean up params, used by app message component, deprecated graph component, and Tally response selectors. + * May need to move part of this towards rhsmConstants.js + */ /** * RHSM response Reporting/Tally DATA types. * Schema/map of expected Reporting/Tally DATA response properties. @@ -244,16 +252,6 @@ const RHSM_API_QUERY_SORT_TYPES = { SOCKETS: 'sockets' }; -/** - * RHSM API query/search parameter of EVENT type values for Subscriptions. - * - * @type {{EVENT_START: string, EVENT_END: string}} - */ -const RHSM_API_QUERY_SUBSCRIPTIONS_EVENT_TYPES = { - EVENT_START: 'Subscription Start', - EVENT_END: 'Subscription End' -}; - /** * RHSM API query/search parameter SORT type values for SUBSCRIPTIONS. * @@ -261,12 +259,7 @@ const RHSM_API_QUERY_SUBSCRIPTIONS_EVENT_TYPES = { * SERVICE_LEVEL: string}} */ const RHSM_API_QUERY_SUBSCRIPTIONS_SORT_TYPES = { - NEXT_EVENT_DATE: 'next_event_date', - NEXT_EVENT_TYPE: 'next_event_type', - QUANTITY: 'quantity', - SKU: 'sku', - SERVICE_LEVEL: 'service_level', - USAGE: 'usage' + ...rhsmConstants.RHSM_API_QUERY_INVENTORY_SUBSCRIPTIONS_SORT_TYPES }; /** @@ -305,6 +298,9 @@ const RHSM_API_QUERY_USAGE_TYPES = { ...rhsmConstants.RHSM_API_QUERY_USAGE_TYPES }; +/** + * ToDo: Clean up params, unused. + */ /** * RHSM API query/search parameter OPTIN type values. * @@ -341,8 +337,7 @@ const RHSM_API_QUERY_SET_INVENTORY_TYPES = { * @type {{OFFSET: string, LIMIT: string}} */ const RHSM_API_QUERY_SET_INVENTORY_GUESTS_TYPES = { - LIMIT: 'limit', - OFFSET: 'offset' + ...rhsmConstants.RHSM_API_QUERY_SET_INVENTORY_TYPES }; /** @@ -373,44 +368,37 @@ const RHSM_API_QUERY_TYPES = { /** * RHSM API types. * - * @type {{RHSM_API_QUERY_SET_INVENTORY_SUBSCRIPTIONS_TYPES: {UOM: string, USAGE: string, DIRECTION: string, SORT: string, - * OFFSET: string, SLA: string, LIMIT: string}, RHSM_API_RESPONSE_DATA: string, - * RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES: {UOM: string, SUBSCRIPTIONS: string, PHYSICAL_CAPACITY: string, - * USAGE: string, QUANTITY: string, NEXT_EVENT_TYPE: string, NEXT_EVENT_DATE: string, VIRTUAL_CAPACITY: string, - * TOTAL_CAPACITY: string, SKU: string, PRODUCT_NAME: string, SERVICE_LEVEL: 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_RESPONSE_ERROR_DATA_TYPES: {CODE: string, DETAIL: string}, + * @type {{RHSM_API_QUERY_SET_INVENTORY_SUBSCRIPTIONS_TYPES: {UOM: string, USAGE: string, DIRECTION: string, SORT: string, OFFSET: string, + * SLA: string, LIMIT: string}, RHSM_API_RESPONSE_DATA: 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_RESPONSE_ERROR_DATA_TYPES: {CODE: string, DETAIL: 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_QUERY_SUBSCRIPTIONS_SORT_TYPES: {QUANTITY: string, USAGE: string, NEXT_EVENT_TYPE: string, - * NEXT_EVENT_DATE: string, SKU: string, SERVICE_LEVEL: string}, RHSM_API_RESPONSE_META_TYPES: {COUNT: string, - * TOTAL_INSTANCE_HOURS: string, TOTAL_CORE_HOURS: string}, RHSM_API_QUERY_GRANULARITY_TYPES: {WEEKLY: string, - * QUARTERLY: string, DAILY: string, MONTHLY: string}, RHSM_API_QUERY_SORT_DIRECTION_TYPES: {ASCENDING: string, - * DESCENDING: string}, RHSM_API_RESPONSE_PRODUCTS_DATA: string, - * RHSM_API_QUERY_SUBSCRIPTIONS_EVENT_TYPES: {EVENT_START: string, EVENT_END: string}, - * RHSM_API_QUERY_TYPES: {GRANULARITY: string, TALLY_SYNC: string, DIRECTION: string, END_DATE: string, SLA: string, - * START_DATE: string, LIMIT: string, UOM: string, TALLY_REPORT: string, USAGE: string, SORT: string, OFFSET: string, - * CONDUIT_SYNC: string}, RHSM_API_RESPONSE_LINKS: string, RHSM_API_QUERY_SET_INVENTORY_GUESTS_TYPES: {OFFSET: string, - * LIMIT: string}, RHSM_API_PATH_ID_TYPES: {RHEL_ARM: string, OPENSHIFT_METRICS: string, SATELLITE: string, - * RHEL_WORKSTATION: string, RHOSAK: string, RHEL_COMPUTE_NODE: string, RHEL_X86: string, OPENSHIFT: string, - * SATELLITE_SERVER: string, OPENSHIFT_DEDICATED_METRICS: string, RHEL_DESKTOP: string, RHEL: string, - * SATELLITE_CAPSULE: string, RHEL_SERVER: string, RHEL_IBM_Z: string, RHEL_IBM_POWER: string}, + * RHSM_API_QUERY_SUBSCRIPTIONS_SORT_TYPES: {QUANTITY: string, USAGE: string, NEXT_EVENT_TYPE: string, NEXT_EVENT_DATE: string, + * SKU: string, SERVICE_LEVEL: string}, RHSM_API_RESPONSE_META_TYPES: {COUNT: string, TOTAL_INSTANCE_HOURS: string, + * TOTAL_CORE_HOURS: string}, RHSM_API_QUERY_GRANULARITY_TYPES: {WEEKLY: string, QUARTERLY: string, DAILY: string, + * MONTHLY: string}, RHSM_API_QUERY_SORT_DIRECTION_TYPES: {ASCENDING: string, DESCENDING: string}, + * RHSM_API_RESPONSE_PRODUCTS_DATA: string, RHSM_API_QUERY_TYPES: {GRANULARITY: string, TALLY_SYNC: string, DIRECTION: string, + * END_DATE: string, SLA: string, START_DATE: string, LIMIT: string, UOM: string, TALLY_REPORT: string, USAGE: string, + * SORT: string, OFFSET: string, CONDUIT_SYNC: string}, RHSM_API_RESPONSE_LINKS: string, + * RHSM_API_QUERY_SET_INVENTORY_GUESTS_TYPES: {OFFSET: string, LIMIT: string}, RHSM_API_PATH_ID_TYPES: {RHEL_ARM: string, + * OPENSHIFT_METRICS: string, SATELLITE: string, RHEL_WORKSTATION: string, RHOSAK: string, RHEL_COMPUTE_NODE: string, + * RHEL_X86: string, OPENSHIFT: string, SATELLITE_SERVER: string, OPENSHIFT_DEDICATED_METRICS: string, RHEL_DESKTOP: string, + * RHEL: string, SATELLITE_CAPSULE: string, RHEL_SERVER: string, RHEL_IBM_Z: string, RHEL_IBM_POWER: 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: {UOM: string, USAGE: string, DIRECTION: string, SORT: string, OFFSET: string, - * SLA: string, LIMIT: string}, RHSM_API_QUERY_SORT_TYPES: {CORES: string, CORE_HOURS: string, HARDWARE: string, - * SOCKETS: string, MEASUREMENT: string, LAST_SEEN: string, NAME: string}, - * RHSM_API_RESPONSE_PRODUCTS_DATA_TYPES: {HYPERVISOR_SOCKETS: string, CORES: string, INSTANCE_HOURS: string, - * SOCKETS: string, CLOUD_CORES: string, HAS_DATA: string, PHYSICAL_SOCKETS: string, PHYSICAL_CORES: string, - * CLOUD_INSTANCES: string, DATE: string, CORE_HOURS: string, CLOUD_SOCKETS: string, HAS_CLOUDIGRADE_DATA: string, - * HAS_CLOUDIGRADE_MISMATCH: string, HYPERVISOR_CORES: string}, RHSM_API_QUERY_UOM_TYPES: {CORES: string, - * SOCKETS: 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_RESPONSE_ERROR_DATA: string, RHSM_API_RESPONSE_META: string, - * RHSM_API_RESPONSE_INVENTORY_DATA_TYPES: {CORES: string, CORE_HOURS: string, HARDWARE: string, SOCKETS: string, - * SUBSCRIPTION_ID: string, INVENTORY_ID: string, MEASUREMENT: string, ID: string, GUESTS: string, - * CLOUD_PROVIDER: string, LAST_SEEN: string, NAME: string}, + * RHSM_API_QUERY_SET_INVENTORY_TYPES: {UOM: string, USAGE: string, DIRECTION: string, SORT: string, OFFSET: string, SLA: string, + * LIMIT: string}, RHSM_API_QUERY_SORT_TYPES: {CORES: string, CORE_HOURS: string, HARDWARE: string, SOCKETS: string, + * MEASUREMENT: string, LAST_SEEN: string, NAME: string}, RHSM_API_RESPONSE_PRODUCTS_DATA_TYPES: {HYPERVISOR_SOCKETS: string, + * CORES: string, INSTANCE_HOURS: string, SOCKETS: string, CLOUD_CORES: string, HAS_DATA: string, PHYSICAL_SOCKETS: string, + * PHYSICAL_CORES: string, CLOUD_INSTANCES: string, DATE: string, CORE_HOURS: string, CLOUD_SOCKETS: string, + * HAS_CLOUDIGRADE_DATA: string, HAS_CLOUDIGRADE_MISMATCH: string, HYPERVISOR_CORES: string}, + * RHSM_API_QUERY_UOM_TYPES: {CORES: string, SOCKETS: 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_RESPONSE_ERROR_DATA: string, RHSM_API_RESPONSE_META: string, RHSM_API_RESPONSE_INVENTORY_DATA_TYPES: {CORES: string, + * CORE_HOURS: string, HARDWARE: string, SOCKETS: string, SUBSCRIPTION_ID: string, INVENTORY_ID: string, MEASUREMENT: string, + * ID: string, GUESTS: string, CLOUD_PROVIDER: string, LAST_SEEN: string, NAME: string}, * RHSM_API_QUERY_SET_REPORT_CAPACITY_TYPES: {GRANULARITY: string, USAGE: string, END_DATE: string, SLA: string, * START_DATE: string}}} */ @@ -428,13 +416,11 @@ const rhsmApiTypes = { RHSM_API_RESPONSE_INVENTORY_DATA, RHSM_API_RESPONSE_INVENTORY_DATA_TYPES, RHSM_API_RESPONSE_INVENTORY_GUESTS_DATA_TYPES, - RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES, RHSM_API_RESPONSE_PRODUCTS_DATA, RHSM_API_RESPONSE_PRODUCTS_DATA_TYPES, RHSM_API_PATH_ID_TYPES, RHSM_API_QUERY_GRANULARITY_TYPES, RHSM_API_QUERY_SORT_TYPES, - RHSM_API_QUERY_SUBSCRIPTIONS_EVENT_TYPES, RHSM_API_QUERY_SUBSCRIPTIONS_SORT_TYPES, RHSM_API_QUERY_SORT_DIRECTION_TYPES, RHSM_API_QUERY_SLA_TYPES, @@ -464,13 +450,11 @@ export { RHSM_API_RESPONSE_INVENTORY_DATA, RHSM_API_RESPONSE_INVENTORY_DATA_TYPES, RHSM_API_RESPONSE_INVENTORY_GUESTS_DATA_TYPES, - RHSM_API_RESPONSE_INVENTORY_SUBSCRIPTIONS_DATA_TYPES, RHSM_API_RESPONSE_PRODUCTS_DATA, RHSM_API_RESPONSE_PRODUCTS_DATA_TYPES, RHSM_API_PATH_ID_TYPES, RHSM_API_QUERY_GRANULARITY_TYPES, RHSM_API_QUERY_SORT_TYPES, - RHSM_API_QUERY_SUBSCRIPTIONS_EVENT_TYPES, RHSM_API_QUERY_SUBSCRIPTIONS_SORT_TYPES, RHSM_API_QUERY_SORT_DIRECTION_TYPES, RHSM_API_QUERY_SLA_TYPES, diff --git a/tests/__snapshots__/code.test.js.snap b/tests/__snapshots__/code.test.js.snap index 408cadcae..a6dcfff4e 100644 --- a/tests/__snapshots__/code.test.js.snap +++ b/tests/__snapshots__/code.test.js.snap @@ -2,10 +2,10 @@ exports[`General code checks should only have specific console.[warn|log|info|error] methods: console methods 1`] = ` Array [ - "components/inventoryList/inventoryCard.js:74: console.warn(\`Sorting can only be performed on select fields, confirm field \${id} is allowed.\`);", + "components/inventoryList/inventoryCardContext.js:127: console.warn(\`Sorting can only be performed on select fields, confirm field \${id} is allowed.\`);", "components/inventoryList/inventoryCardHelpers.js:194: console.error(", "components/inventoryList/inventoryList.deprecated.js:62: console.warn(\`Sorting can only be performed on select fields, confirm field \${id} is allowed.\`);", - "components/inventorySubscriptions/inventorySubscriptions.js:60: console.warn(\`Sorting can only be performed on select fields, confirm field \${id} is allowed.\`);", + "components/inventorySubscriptions/inventorySubscriptionsContext.js:127: 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);", "services/common/helpers.js:64: console.error(", diff --git a/tests/__snapshots__/dist.test.js.snap b/tests/__snapshots__/dist.test.js.snap index 7b36817fe..61670d4b7 100644 --- a/tests/__snapshots__/dist.test.js.snap +++ b/tests/__snapshots__/dist.test.js.snap @@ -21,6 +21,7 @@ Array [ "./dist/js/1339*js", "./dist/js/1355*js", "./dist/js/136*js", + "./dist/js/1692*js", "./dist/js/1750*js", "./dist/js/1799*js", "./dist/js/1858*js", @@ -31,6 +32,7 @@ Array [ "./dist/js/2738*js", "./dist/js/2902*js", "./dist/js/3056*js", + "./dist/js/3060*js", "./dist/js/31*js", "./dist/js/3267*js", "./dist/js/3313*js", @@ -49,12 +51,15 @@ Array [ "./dist/js/4291*js", "./dist/js/4291*txt", "./dist/js/4314*js", + "./dist/js/4379*js", + "./dist/js/4393*js", "./dist/js/4418*js", "./dist/js/4569*js", "./dist/js/4569*txt", "./dist/js/4590*js", "./dist/js/4590*txt", "./dist/js/4944*js", + "./dist/js/5016*js", "./dist/js/5020*js", "./dist/js/5068*js", "./dist/js/5394*js", @@ -72,13 +77,14 @@ Array [ "./dist/js/6873*js", "./dist/js/6937*js", "./dist/js/6937*txt", + "./dist/js/7015*js", "./dist/js/7159*js", "./dist/js/7159*txt", "./dist/js/7235*js", "./dist/js/7294*js", "./dist/js/7294*txt", - "./dist/js/7493*js", "./dist/js/7514*js", + "./dist/js/7573*js", "./dist/js/7585*js", "./dist/js/7891*js", "./dist/js/8139*js", @@ -128,6 +134,7 @@ Array [ "./dist/sourcemaps/1339*map", "./dist/sourcemaps/1355*map", "./dist/sourcemaps/136*map", + "./dist/sourcemaps/1692*map", "./dist/sourcemaps/1750*map", "./dist/sourcemaps/1799*map", "./dist/sourcemaps/1858*map", @@ -136,6 +143,7 @@ Array [ "./dist/sourcemaps/2738*map", "./dist/sourcemaps/2902*map", "./dist/sourcemaps/3056*map", + "./dist/sourcemaps/3060*map", "./dist/sourcemaps/31*map", "./dist/sourcemaps/3267*map", "./dist/sourcemaps/3313*map", @@ -152,10 +160,13 @@ Array [ "./dist/sourcemaps/4220*map", "./dist/sourcemaps/4291*map", "./dist/sourcemaps/4314*map", + "./dist/sourcemaps/4379*map", + "./dist/sourcemaps/4393*map", "./dist/sourcemaps/4418*map", "./dist/sourcemaps/4569*map", "./dist/sourcemaps/4590*map", "./dist/sourcemaps/4944*map", + "./dist/sourcemaps/5016*map", "./dist/sourcemaps/5020*map", "./dist/sourcemaps/5068*map", "./dist/sourcemaps/5394*map", @@ -171,11 +182,12 @@ Array [ "./dist/sourcemaps/6816*map", "./dist/sourcemaps/6873*map", "./dist/sourcemaps/6937*map", + "./dist/sourcemaps/7015*map", "./dist/sourcemaps/7159*map", "./dist/sourcemaps/7235*map", "./dist/sourcemaps/7294*map", - "./dist/sourcemaps/7493*map", "./dist/sourcemaps/7514*map", + "./dist/sourcemaps/7573*map", "./dist/sourcemaps/7585*map", "./dist/sourcemaps/7891*map", "./dist/sourcemaps/8139*map",