diff --git a/public/locales/en.json b/public/locales/en.json index d88c0b5d..3ea44160 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -56,9 +56,12 @@ "label_option_TLSv1_2": "TLSv1.2", "label_option_sshKey": "SSH Key", "label_option_usernamePassword": "Username and Password", - "label_option_network": "Network Credential", - "label_option_satellite": "Satellite Credential", - "label_option_vcenter": "VCenter Credential", + "label_option_network": "Network", + "label_option_network_credential": "Network Credential", + "label_option_satellite": "Satellite", + "label_option_satellite_credential": "Satellite Credential", + "label_option_vcenter": "VCenter", + "label_option_vcenter_credential": "VCenter Credential", "label_placeholder_add-credential": "Add Credential", "label_placeholder_add-source_credential": "Select a credential", "label_placeholder_add-source_credential_add": "Add a credential", @@ -202,6 +205,31 @@ "title_error": "Error", "title_warning": "Warning" }, + "toolbar": { + "label_chip": "{{context}}", + "label_chip_network": "Network", + "label_chip_satellite": "Satellite", + "label_chip_vcenter": "VCenter", + "label_clear-filters": "Reset filters", + "label_option": "", + "label_option_name": "Name", + "label_option_cred_type": "Credential type", + "label_option_most_recent_connect_scan__start_time": "Most recent", + "label_option_search_credentials_by_name": "Credential name", + "label_option_search_by_name": "Name", + "label_option_search_sources_by_name": "Source name", + "label_option_source_type": "Source type", + "label_placeholder_filter": "Filter by", + "label_placeholder_filter_cred_type": "Filter by credential type", + "label_placeholder_filter_search_credentials_by_name": "Filter by credential name", + "label_placeholder_filter_search_by_name": "Filter by name", + "label_placeholder_filter_search_sources_by_name": "Filter by source name", + "label_placeholder_filter_source_type": "Filter by source type", + "label_placeholder_sort": "Sort by", + "label_tooltip_sort": "Sort order", + "label_tooltip_sort_asc": "Sort ascending", + "label_tooltip_sort_dsc": "Sort descending" + }, "view": { "empty-state_description_credentials": "Credentials contain authentication information needed to scan a source. A credential includes a username and a password or SSH key. {{name}} uses SSH to connect to servers on the network and uses credentials to access those servers.", "empty-state_description_scans": "Select a Source to scan from the Sources page.", diff --git a/src/common/__tests__/__snapshots__/helpers.test.js.snap b/src/common/__tests__/__snapshots__/helpers.test.js.snap index 0c787426..4785b3b3 100644 --- a/src/common/__tests__/__snapshots__/helpers.test.js.snap +++ b/src/common/__tests__/__snapshots__/helpers.test.js.snap @@ -64,13 +64,6 @@ Object { } `; -exports[`Helpers should handle view related selectors and props updates: createViewQueryObject 1`] = ` -Object { - "page": 1, - "page_size": 10, -} -`; - exports[`Helpers should have specific functions: helpers 1`] = ` Object { "DEV_MODE": false, @@ -85,7 +78,6 @@ Object { "UI_VERSION": "0.0.0.0000000", "aggregatedError": [Function], "copyClipboard": [Function], - "createViewQueryObject": [Function], "devModeNormalizeCount": [Function], "downloadData": [Function], "generateId": [Function], diff --git a/src/common/__tests__/helpers.test.js b/src/common/__tests__/helpers.test.js index fcd7d7d1..0a17e752 100644 --- a/src/common/__tests__/helpers.test.js +++ b/src/common/__tests__/helpers.test.js @@ -40,19 +40,6 @@ describe('Helpers', () => { expect(helpers.setPropIfTruthy(truthyObj, ['lorem'], '')).toMatchSnapshot('setPropIfTruthy string'); }); - it('should handle view related selectors and props updates', () => { - const viewOptions = { - currentPage: 1, - pageSize: 10 - }; - - const queryObj = { - page: 2 - }; - - expect(helpers.createViewQueryObject(viewOptions, queryObj)).toMatchSnapshot('createViewQueryObject'); - }); - it('should handle http status less than 400 message from response', () => { const payload = { data: {}, diff --git a/src/common/helpers.js b/src/common/helpers.js index d2e295f0..b76b99fd 100644 --- a/src/common/helpers.js +++ b/src/common/helpers.js @@ -180,36 +180,6 @@ const setPropIfDefined = (obj, props, value) => (obj && value !== undefined ? _s */ const setPropIfTruthy = (obj, props, value) => (obj && value ? _set(obj, props, value) : obj); -/** - * Generate a consistent query parameter object for views. - * - * @param {object} viewOptions - * @param {object} queryObj - * @returns {object} - */ -const createViewQueryObject = (viewOptions, queryObj) => { - const queryObject = { - ...queryObj - }; - - if (viewOptions) { - if (viewOptions.sortField) { - queryObject.ordering = viewOptions.sortAscending ? viewOptions.sortField : `-${viewOptions.sortField}`; - } - - if (viewOptions.activeFilters) { - viewOptions.activeFilters.forEach(filter => { - queryObject[filter.field.id] = filter.field.filterType === 'select' ? filter.value.id : filter.value; - }); - } - - queryObject.page = viewOptions.currentPage; - queryObject.page_size = viewOptions.pageSize; - } - - return queryObject; -}; - /** * A redux helper associated with getting a message from results. * @@ -483,7 +453,6 @@ const helpers = { noopTranslate, setPropIfDefined, setPropIfTruthy, - createViewQueryObject, getMessageFromResults, getStatusFromResults, getTimeStampFromResults, diff --git a/src/components/addCredentialType/addCredentialType.js b/src/components/addCredentialType/addCredentialType.js index 8ef8f84e..2b0732bf 100644 --- a/src/components/addCredentialType/addCredentialType.js +++ b/src/components/addCredentialType/addCredentialType.js @@ -10,9 +10,12 @@ import { translate } from '../i18n/i18n'; * @type {{title: Function|string, value: string}[]} */ const fieldOptions = [ - { title: () => translate('form-dialog.label', { context: ['option', 'network'] }), value: 'network' }, - { title: () => translate('form-dialog.label', { context: ['option', 'satellite'] }), value: 'satellite' }, - { title: () => translate('form-dialog.label', { context: ['option', 'vcenter'] }), value: 'vcenter' } + { title: () => translate('form-dialog.label', { context: ['option', 'network', 'credential'] }), value: 'network' }, + { + title: () => translate('form-dialog.label', { context: ['option', 'satellite', 'credential'] }), + value: 'satellite' + }, + { title: () => translate('form-dialog.label', { context: ['option', 'vcenter', 'credential'] }), value: 'vcenter' } ]; /** diff --git a/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizard.test.js.snap b/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizard.test.js.snap index db1356ca..81588766 100644 --- a/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizard.test.js.snap +++ b/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizard.test.js.snap @@ -47,7 +47,8 @@ Array [ ], Array [ Object { - "type": "UPDATE_SOURCES", + "type": "UPDATE_VIEW", + "viewId": "sources", }, ], ] diff --git a/src/components/addSourceWizard/addSourceWizard.js b/src/components/addSourceWizard/addSourceWizard.js index d3c00bc3..bccfe967 100644 --- a/src/components/addSourceWizard/addSourceWizard.js +++ b/src/components/addSourceWizard/addSourceWizard.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { ModalVariant } from '@patternfly/react-core'; import { reduxActions, reduxTypes, storeHooks } from '../../redux'; +import { CONFIG as sourcesConfig } from '../sources/sources'; import { addSourceWizardSteps, editSourceWizardSteps } from './addSourceWizardConstants'; import { Wizard } from '../wizard/wizard'; import { useGetAddSource } from './addSourceWizardContext'; @@ -74,7 +75,8 @@ const AddSourceWizard = ({ if (fulfilled) { dispatch({ - type: reduxTypes.sources.UPDATE_SOURCES + type: reduxTypes.view.UPDATE_VIEW, + viewId: sourcesConfig.viewId }); } }; diff --git a/src/components/createCredentialDialog/__tests__/createCredentialDialog.test.js b/src/components/createCredentialDialog/__tests__/createCredentialDialog.test.js index 19a9ff2f..0e89f829 100644 --- a/src/components/createCredentialDialog/__tests__/createCredentialDialog.test.js +++ b/src/components/createCredentialDialog/__tests__/createCredentialDialog.test.js @@ -12,7 +12,7 @@ describe('CreateCredentialDialog Component', () => { const generateEmptyStore = (obj = {}) => configureMockStore()(obj); it('should render a connected component', () => { - const store = generateEmptyStore({ credentials: { dialog: { show: true } }, viewOptions: {} }); + const store = generateEmptyStore({ credentials: { dialog: { show: true } } }); const component = mount( diff --git a/src/components/createCredentialDialog/createCredentialDialog.js b/src/components/createCredentialDialog/createCredentialDialog.js index 94f5d6c7..7b63ec01 100644 --- a/src/components/createCredentialDialog/createCredentialDialog.js +++ b/src/components/createCredentialDialog/createCredentialDialog.js @@ -86,7 +86,7 @@ class CreateCredentialDialog extends React.Component { // eslint-disable-next-line camelcase UNSAFE_componentWillReceiveProps(nextProps) { - const { edit, fulfilled, getCredentials, show, viewOptions } = this.props; + const { edit, fulfilled, getCredentials, show } = this.props; if (!show && nextProps.show) { this.resetInitialState(nextProps); @@ -105,7 +105,7 @@ class CreateCredentialDialog extends React.Component { }); this.onCancel(); - getCredentials(helpers.createViewQueryObject(viewOptions)); + getCredentials(); } } @@ -466,8 +466,7 @@ CreateCredentialDialog.propTypes = { fulfilled: PropTypes.bool, error: PropTypes.bool, errorMessage: PropTypes.string, - t: PropTypes.func, - viewOptions: PropTypes.object + t: PropTypes.func }; CreateCredentialDialog.defaultProps = { @@ -482,8 +481,7 @@ CreateCredentialDialog.defaultProps = { fulfilled: false, error: false, errorMessage: null, - t: translate, - viewOptions: {} + t: translate }; const mapDispatchToProps = dispatch => ({ @@ -493,8 +491,7 @@ const mapDispatchToProps = dispatch => ({ }); const mapStateToProps = state => ({ - ...state.credentials.dialog, - viewOptions: state.viewOptions[reduxTypes.view.CREDENTIALS_VIEW] + ...state.credentials.dialog }); const ConnectedCreateCredentialDialog = connect(mapStateToProps, mapDispatchToProps)(CreateCredentialDialog); diff --git a/src/components/createScanDialog/createScanDialog.js b/src/components/createScanDialog/createScanDialog.js index 0fcab118..a3cb9d94 100644 --- a/src/components/createScanDialog/createScanDialog.js +++ b/src/components/createScanDialog/createScanDialog.js @@ -15,6 +15,7 @@ import { import { FieldLevelHelp } from 'patternfly-react'; import { Modal } from '../modal/modal'; import { connect, reduxActions, reduxTypes, store } from '../../redux'; +import { CONFIG as sourcesConfig } from '../sources/sources'; import { FormState } from '../formState/formState'; import { formHelpers } from '../form/formHelpers'; import { Checkbox } from '../form/checkbox'; @@ -148,7 +149,8 @@ class CreateScanDialog extends React.Component { }); store.dispatch({ - type: reduxTypes.sources.UPDATE_SOURCES + type: reduxTypes.view.UPDATE_VIEW, + viewId: sourcesConfig.viewId }); }, () => { diff --git a/src/components/credentials/__tests__/__snapshots__/credentials.test.js.snap b/src/components/credentials/__tests__/__snapshots__/credentials.test.js.snap index 8f588684..4e08d63f 100644 --- a/src/components/credentials/__tests__/__snapshots__/credentials.test.js.snap +++ b/src/components/credentials/__tests__/__snapshots__/credentials.test.js.snap @@ -21,7 +21,8 @@ exports[`Credentials Component should handle multiple display states, pending, e className="quipucords-view-container" > } - activeFilters={Array []} - filterFields={ - Array [ - Object { - "filterType": "text", - "id": "search_by_name", - "placeholder": "Filter by Name", - "title": "Name", - }, - Object { - "filterType": "select", - "filterValues": Array [ - Object { - "id": "network", - "title": "Network", - }, - Object { - "id": "satellite", - "title": "Satellite", - }, - Object { - "id": "vcenter", - "title": "VCenter", - }, - ], - "id": "cred_type", - "placeholder": "Filter by Credential Type", - "title": "Credential Type", - }, - ] - } - filterType={Object {}} - filterValue="" - itemsType="Credential" - itemsTypePlural="Credentials" - lastRefresh={NaN} - onRefresh={[Function]} - selectedCount={0} - sortAscending={true} - sortFields={ - Array [ - Object { - "id": "name", - "isNumeric": false, - "title": "Name", - }, - Object { - "id": "cred_type", - "isNumeric": false, - "title": "Credential Type", - }, - ] - } - sortType={Object {}} - totalCount={0} - viewType="CREDENTIALS_VIEW" + t={[Function]} + useOnRefresh={[Function]} + useSelector={[Function]} + useToolbarFieldClear={[Function]} + useToolbarFieldClearAll={[Function]} + useView={[Function]} />
{ expect(component).toMatchSnapshot('empty state, no data'); component.setProps({ - useSelectors: () => [{ activeFilters: ['test filter'] }] + useView: () => ({ + isFilteringActive: true + }) }); expect(component.find(EmptyState)).toMatchSnapshot('empty state, filtering active'); diff --git a/src/components/credentials/__tests__/credentialsConstants.test.js b/src/components/credentials/__tests__/credentialsConstants.test.js deleted file mode 100644 index 7cd46522..00000000 --- a/src/components/credentials/__tests__/credentialsConstants.test.js +++ /dev/null @@ -1,11 +0,0 @@ -import { CredentialFilterFields, CredentialSortFields } from '../credentialConstants'; - -describe('CredentialTypes', () => { - it('should have specific CredentialFilterFields properties', () => { - expect(CredentialFilterFields).toMatchSnapshot('CredentialFilterFields'); - }); - - it('should have specific CredentialSortFields properties', () => { - expect(CredentialSortFields).toMatchSnapshot('CredentialSortFields'); - }); -}); diff --git a/src/components/credentials/__tests__/credentialsContext.test.js b/src/components/credentials/__tests__/credentialsContext.test.js index a24a6462..18211c79 100644 --- a/src/components/credentials/__tests__/credentialsContext.test.js +++ b/src/components/credentials/__tests__/credentialsContext.test.js @@ -1,6 +1,5 @@ -import { context, useGetCredentials, useOnDelete, useOnEdit } from '../credentialsContext'; +import { context, useCredentials, useGetCredentials, useOnDelete, useOnEdit } from '../credentialsContext'; import { apiTypes } from '../../../constants/apiConstants'; -import { reduxTypes } from '../../../redux'; describe('CredentialsContext', () => { it('should return specific properties', () => { @@ -47,27 +46,69 @@ describe('CredentialsContext', () => { it('should apply a hook for retrieving data from multiple selectors', () => { const { result: errorResponse } = shallowHook(() => - useGetCredentials({ + useCredentials({ useSelectorsResponse: () => ({ error: true, message: 'Lorem ipsum' }) }) ); const { result: pendingResponse } = shallowHook(() => - useGetCredentials({ + useCredentials({ useSelectorsResponse: () => ({ pending: true }) }) ); const { result: fulfilledResponse } = shallowHook(() => - useGetCredentials({ + useCredentials({ useSelectorsResponse: () => ({ fulfilled: true, data: { view: { results: ['dolor', 'sit'] } } }) }) ); + const { result: mockStoreSuccessResponse } = shallowHook(() => useCredentials(), { + state: { + view: { + update: {} + }, + credentials: { + expanded: {}, + selected: {}, + view: { + fulfilled: true, + data: { + results: ['lorem', 'ipsum'] + } + } + } + } + }); + + expect({ errorResponse, fulfilledResponse, pendingResponse, mockStoreSuccessResponse }).toMatchSnapshot( + 'selector responses' + ); + }); + + it('should apply a hook for returning a get response', () => { + const { result: errorResponse } = shallowHook(() => + useGetCredentials({ + useCredentials: () => ({ error: true, message: 'Lorem ipsum' }) + }) + ); + + const { result: pendingResponse } = shallowHook(() => + useGetCredentials({ + useCredentials: () => ({ pending: true }) + }) + ); + + const { result: fulfilledResponse } = shallowHook(() => + useGetCredentials({ + useCredentials: () => ({ fulfilled: true, data: { view: { results: ['dolor', 'sit'] } } }) + }) + ); + const { result: mockStoreSuccessResponse } = shallowHook(() => useGetCredentials(), { state: { - viewOptions: { - [reduxTypes.view.SCANS_VIEW]: {} + view: { + update: {} }, credentials: { expanded: {}, @@ -83,7 +124,7 @@ describe('CredentialsContext', () => { }); expect({ errorResponse, fulfilledResponse, pendingResponse, mockStoreSuccessResponse }).toMatchSnapshot( - 'responses' + 'get responses' ); }); }); diff --git a/src/components/credentials/__tests__/credentialsToolbar.test.js b/src/components/credentials/__tests__/credentialsToolbar.test.js new file mode 100644 index 00000000..3b0f72f6 --- /dev/null +++ b/src/components/credentials/__tests__/credentialsToolbar.test.js @@ -0,0 +1,7 @@ +import { CredentialsToolbar } from '../credentialsToolbar'; + +describe('CredentialsToolbar', () => { + it('should have specific properties', () => { + expect(CredentialsToolbar).toMatchSnapshot('CredentialsToolbar'); + }); +}); diff --git a/src/components/credentials/credentialConstants.js b/src/components/credentials/credentialConstants.js deleted file mode 100644 index 3354f30b..00000000 --- a/src/components/credentials/credentialConstants.js +++ /dev/null @@ -1,37 +0,0 @@ -const CredentialFilterFields = [ - { - id: 'search_by_name', - title: 'Name', - placeholder: 'Filter by Name', - filterType: 'text' - }, - { - id: 'cred_type', - title: 'Credential Type', - placeholder: 'Filter by Credential Type', - filterType: 'select', - filterValues: [ - { title: 'Network', id: 'network' }, - { title: 'Satellite', id: 'satellite' }, - { title: 'VCenter', id: 'vcenter' } - ] - } -]; - -/** - * ID: Enum with the following possible values [name, cred_type] - */ -const CredentialSortFields = [ - { - id: 'name', - title: 'Name', - isNumeric: false - }, - { - id: 'cred_type', - title: 'Credential Type', - isNumeric: false - } -]; - -export { CredentialFilterFields, CredentialSortFields }; diff --git a/src/components/credentials/credentials.js b/src/components/credentials/credentials.js index e30ca539..659e5399 100644 --- a/src/components/credentials/credentials.js +++ b/src/components/credentials/credentials.js @@ -21,13 +21,12 @@ import { ButtonVariant as CredentialButtonVariant, SelectPosition } from '../addCredentialType/addCredentialType'; -import { reduxTypes, storeHooks } from '../../redux'; import { useOnShowAddSourceWizard } from '../addSourceWizard/addSourceWizardContext'; import { useView } from '../view/viewContext'; -import ViewToolbar from '../viewToolbar/viewToolbar'; -import ViewPaginationRow from '../viewPaginationRow/viewPaginationRow'; +import { useToolbarFieldClearAll } from '../viewToolbar/viewToolbarContext'; +import { ViewToolbar } from '../viewToolbar/viewToolbar'; +import { ViewPaginationRow } from '../viewPaginationRow/viewPaginationRow'; import { CredentialsEmptyState } from './credentialsEmptyState'; -import { CredentialFilterFields, CredentialSortFields } from './credentialConstants'; import { Table } from '../table/table'; import { credentialsTableCells } from './credentialsTableCells'; import { @@ -37,14 +36,15 @@ import { useOnDelete, useOnEdit, useOnExpand, - useOnRefresh, useOnSelect } from './credentialsContext'; +import { CredentialsToolbar } from './credentialsToolbar'; import { translate } from '../i18n/i18n'; const CONFIG = { viewId: VIEW_ID, - initialQuery: INITIAL_QUERY + initialQuery: INITIAL_QUERY, + toolbar: CredentialsToolbar }; /** @@ -52,35 +52,30 @@ const CONFIG = { * * @param {object} props * @param {Function} props.t - * @param {Function} props.useDispatch * @param {Function} props.useGetCredentials * @param {Function} props.useOnDelete * @param {Function} props.useOnEdit * @param {Function} props.useOnExpand - * @param {Function} props.useOnRefresh * @param {Function} props.useOnSelect - * @param {Function} props.useSelectors * @param {Function} props.useOnShowAddSourceWizard + * @param {Function} props.useToolbarFieldClearAll * @param {Function} props.useView * @returns {React.ReactNode} */ const Credentials = ({ t, - useDispatch: useAliasDispatch, useGetCredentials: useAliasGetCredentials, useOnDelete: useAliasOnDelete, useOnEdit: useAliasOnEdit, useOnExpand: useAliasOnExpand, - useOnRefresh: useAliasOnRefresh, useOnSelect: useAliasOnSelect, - useSelectors: useAliasSelectors, useOnShowAddSourceWizard: useAliasOnShowAddSourceWizard, + useToolbarFieldClearAll: useAliasToolbarFieldClearAll, useView: useAliasView }) => { - const { viewId } = useAliasView(); - const dispatch = useAliasDispatch(); + const onToolbarFieldClearAll = useAliasToolbarFieldClearAll(); + const { isFilteringActive, viewId } = useAliasView(); const onExpand = useAliasOnExpand(); - const onRefresh = useAliasOnRefresh(); const onDelete = useAliasOnDelete(); const onEdit = useAliasOnEdit(); const onSelect = useAliasOnSelect(); @@ -93,24 +88,10 @@ const Credentials = ({ date, data, selectedRows = {}, - expandedRows = {} + expandedRows = {}, + totalResults } = useAliasGetCredentials(); - const [viewOptions = {}] = useAliasSelectors([ - ({ viewOptions: stateViewOptions }) => stateViewOptions[reduxTypes.view.CREDENTIALS_VIEW] - ]); - const isActive = viewOptions?.activeFilters?.length > 0 || data?.length > 0 || false; - - /** - * Clear toolbar filters - * - * @event onToolbarFieldClearAll - */ - const onToolbarFieldClearAll = () => { - dispatch({ - type: reduxTypes.viewToolbar.CLEAR_FILTERS, - viewType: reduxTypes.view.CREDENTIALS_VIEW - }); - }; + const isActive = isFilteringActive || data?.length > 0 || false; /** * Toolbar actions onDeleteCredentials @@ -171,19 +152,8 @@ const Credentials = ({
{isActive && ( - onRefresh()} - lastRefresh={new Date(date).getTime()} - actions={renderToolbarActions()} - itemsType="Credential" - itemsTypePlural="Credentials" - selectedCount={viewOptions.selectedItems?.length} - {...viewOptions} - /> - + + )}
@@ -248,42 +218,38 @@ const Credentials = ({ /** * Prop types * - * @type {{useOnEdit: Function, useView: Function, useOnSelect: Function, t: Function, useOnRefresh: Function, - * useDispatch: Function, useOnDelete: Function, useOnExpand: Function, useSelectors: Function, useGetCredentials: Function, + * @type {{useOnEdit: Function, useView: Function, useOnSelect: Function, t: Function, useOnDelete: Function, + * useOnExpand: Function, useToolbarFieldClearAll: Function, useGetCredentials: Function, * useOnShowAddSourceWizard: Function}} */ Credentials.propTypes = { t: PropTypes.func, - useDispatch: PropTypes.func, useGetCredentials: PropTypes.func, useOnDelete: PropTypes.func, useOnEdit: PropTypes.func, useOnExpand: PropTypes.func, - useOnRefresh: PropTypes.func, useOnSelect: PropTypes.func, useOnShowAddSourceWizard: PropTypes.func, - useSelectors: PropTypes.func, + useToolbarFieldClearAll: PropTypes.func, useView: PropTypes.func }; /** * Default props * - * @type {{useOnEdit: Function, useView: Function, useOnSelect: Function, t: translate, useOnRefresh: Function, - * useDispatch: Function, useOnDelete: Function, useOnExpand: Function, useSelectors: Function, useGetCredentials: Function, + * @type {{useOnEdit: Function, useView: Function, useOnSelect: Function, t: translate, useOnDelete: Function, + * useOnExpand: Function, useToolbarFieldClearAll: Function, useGetCredentials: Function, * useOnShowAddSourceWizard: Function}} */ Credentials.defaultProps = { t: translate, - useDispatch: storeHooks.reactRedux.useDispatch, useGetCredentials, useOnDelete, useOnEdit, useOnExpand, - useOnRefresh, useOnSelect, useOnShowAddSourceWizard, - useSelectors: storeHooks.reactRedux.useSelectors, + useToolbarFieldClearAll, useView }; diff --git a/src/components/credentials/credentialsContext.js b/src/components/credentials/credentialsContext.js index dff77a3a..06ce0ab8 100644 --- a/src/components/credentials/credentialsContext.js +++ b/src/components/credentials/credentialsContext.js @@ -4,8 +4,8 @@ import { AlertVariant, List, ListItem } from '@patternfly/react-core'; import { ContextIcon, ContextIconVariant } from '../contextIcon/contextIcon'; import { reduxActions, reduxTypes, storeHooks } from '../../redux'; import { useConfirmation } from '../../hooks/useConfirmation'; +import { useView } from '../view/viewContext'; import { API_QUERY_SORT_TYPES, API_QUERY_TYPES, apiTypes } from '../../constants/apiConstants'; -import { helpers } from '../../common'; import { translate } from '../i18n/i18n'; /** @@ -35,6 +35,7 @@ const INITIAL_QUERY = { * @param {Function} options.useConfirmation * @param {Function} options.useDispatch * @param {Function} options.useSelectorsResponse + * @param {Function} options.useView * @returns {(function(*): void)|*} */ const useOnDelete = ({ @@ -42,8 +43,10 @@ const useOnDelete = ({ t = translate, useConfirmation: useAliasConfirmation = useConfirmation, useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, - useSelectorsResponse: useAliasSelectorsResponse = storeHooks.reactRedux.useSelectorsResponse + useSelectorsResponse: useAliasSelectorsResponse = storeHooks.reactRedux.useSelectorsResponse, + useView: useAliasView = useView } = {}) => { + const { viewId } = useAliasView(); const onConfirmation = useAliasConfirmation(); const [credentialsToDelete, setCredentialsToDelete] = useState([]); const dispatch = useAliasDispatch(); @@ -83,7 +86,8 @@ const useOnDelete = ({ item: credentialsToDelete }, { - type: reduxTypes.credentials.UPDATE_CREDENTIALS + type: reduxTypes.view.UPDATE_VIEW, + viewId } ]); @@ -111,7 +115,8 @@ const useOnDelete = ({ item: credentialsToDelete }, { - type: reduxTypes.credentials.UPDATE_CREDENTIALS + type: reduxTypes.view.UPDATE_VIEW, + viewId } ]); @@ -203,23 +208,6 @@ const useOnExpand = ({ useDispatch: useAliasDispatch = storeHooks.reactRedux.use }; }; -/** - * On refresh view. - * - * @param {object} options - * @param {Function} options.useDispatch - * @returns {Function} - */ -const useOnRefresh = ({ useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch } = {}) => { - const dispatch = useAliasDispatch(); - - return () => { - dispatch({ - type: reduxTypes.credentials.UPDATE_CREDENTIALS - }); - }; -}; - /** * On select a row. * @@ -240,28 +228,21 @@ const useOnSelect = ({ useDispatch: useAliasDispatch = storeHooks.reactRedux.use }; /** - * Get credentials + * Use credentials' response * * @param {object} options - * @param {Function} options.getCredentials - * @param {Function} options.useDispatch * @param {Function} options.useSelectors * @param {Function} options.useSelectorsResponse - * @returns {{date: *, data: *[], pending: boolean, errorMessage: null, fulfilled: boolean, selectedRows: *, - * expandedRows: *, error: boolean}} + * @returns {{date: *, totalResults: (*|number), data: *[], pending: boolean, hasData: boolean, errorMessage: null, + * fulfilled: boolean, selectedRows: *, expandedRows: *, error: boolean}} */ -const useGetCredentials = ({ - getCredentials = reduxActions.credentials.getCredentials, - useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, +const useCredentials = ({ useSelectors: useAliasSelectors = storeHooks.reactRedux.useSelectors, useSelectorsResponse: useAliasSelectorsResponse = storeHooks.reactRedux.useSelectorsResponse } = {}) => { - const dispatch = useAliasDispatch(); - const [refreshUpdate, selectedRows, expandedRows, viewOptions] = useAliasSelectors([ - ({ credentials }) => credentials?.update, + const [selectedRows, expandedRows] = useAliasSelectors([ ({ credentials }) => credentials?.selected, - ({ credentials }) => credentials?.expanded, - ({ viewOptions: stateViewOptions }) => stateViewOptions?.[reduxTypes.view.CREDENTIALS_VIEW] + ({ credentials }) => credentials?.expanded ]); const { data: responseData, @@ -277,11 +258,6 @@ const useGetCredentials = ({ [apiTypes.API_RESPONSE_CREDENTIALS_COUNT]: totalResults, [apiTypes.API_RESPONSE_CREDENTIALS_RESULTS]: data = [] } = responseData?.view || {}; - const query = helpers.createViewQueryObject(viewOptions); - - useShallowCompareEffect(() => { - getCredentials(null, query)(dispatch); - }, [dispatch, getCredentials, query, refreshUpdate]); return { pending, @@ -298,29 +274,44 @@ const useGetCredentials = ({ }; /** - * Get credentials in the context of the credentials view. + * Get credentials * * @param {object} options - * @param {Function} options.useGetCredentials - * @returns {{date: *, data: *[], pending: boolean, errorMessage: null, fulfilled: boolean, selectedRows: *, expandedRows: *, error: boolean}} + * @param {Function} options.getCredentials + * @param {Function} options.useCredentials + * @param {Function} options.useDispatch + * @param {Function} options.useSelectors + * @param {Function} options.useView + * @returns {{date: *, data: *[], pending: boolean, errorMessage: null, fulfilled: boolean, selectedRows: *, + * expandedRows: *, error: boolean}} */ -const useContextGetCredentials = ({ useGetCredentials: useAliasGetCredentials = useGetCredentials } = {}) => { - const results = useAliasGetCredentials(); +const useGetCredentials = ({ + getCredentials = reduxActions.credentials.getCredentials, + useCredentials: useAliasCredentials = useCredentials, + useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, + useSelectors: useAliasSelectors = storeHooks.reactRedux.useSelectors, + useView: useAliasView = useView +} = {}) => { + const { query, viewId } = useAliasView(); + const dispatch = useAliasDispatch(); + const [refreshUpdate] = useAliasSelectors([({ view }) => view.update?.[viewId]]); + const response = useAliasCredentials(); - return { - ...results - }; + useShallowCompareEffect(() => { + getCredentials(null, query)(dispatch); + }, [dispatch, getCredentials, query, refreshUpdate]); + + return response; }; const context = { VIEW_ID, INITIAL_QUERY, - useContextGetCredentials, + useCredentials, useGetCredentials, useOnDelete, useOnEdit, useOnExpand, - useOnRefresh, useOnSelect }; @@ -329,11 +320,10 @@ export { context, VIEW_ID, INITIAL_QUERY, - useContextGetCredentials, + useCredentials, useGetCredentials, useOnDelete, useOnEdit, useOnExpand, - useOnRefresh, useOnSelect }; diff --git a/src/components/credentials/credentialsToolbar.js b/src/components/credentials/credentialsToolbar.js new file mode 100644 index 00000000..3ca6bfc4 --- /dev/null +++ b/src/components/credentials/credentialsToolbar.js @@ -0,0 +1,52 @@ +import React from 'react'; +import { translate } from '../i18n/i18n'; +import { ViewToolbarSelect } from '../viewToolbar/viewToolbarSelect'; +import { ViewToolbarTextInput } from '../viewToolbar/viewToolbarTextInput'; +import { API_QUERY_SORT_TYPES, API_QUERY_TYPES } from '../../constants/apiConstants'; + +/** + * Available filtering + * + * @type {{component: React.ReactNode, selected: boolean, title: Function|string, selected: boolean}[]} + */ +const CredentialsFilterFields = [ + { + title: () => translate('toolbar.label', { context: ['option', API_QUERY_TYPES.SEARCH_NAME] }), + value: API_QUERY_TYPES.SEARCH_NAME, + component: function SearchName(props) { + return ; + }, + selected: true + }, + { + title: () => translate('toolbar.label', { context: ['option', API_QUERY_TYPES.CREDENTIAL_TYPE] }), + value: API_QUERY_TYPES.CREDENTIAL_TYPE, + component: function CredentialType(props) { + return ; + } + } +]; + +/** + * Available sorting + * + * @type {{isNumeric: boolean, title: string|Function, value: string, selected: boolean}[]} + */ +const CredentialsSortFields = [ + { + title: () => translate('toolbar.label', { context: ['option', API_QUERY_SORT_TYPES.NAME] }), + value: API_QUERY_SORT_TYPES.NAME, + selected: true + }, + { + title: () => translate('toolbar.label', { context: ['option', API_QUERY_SORT_TYPES.CREDENTIAL_TYPE] }), + value: API_QUERY_SORT_TYPES.CREDENTIAL_TYPE + } +]; + +const CredentialsToolbar = { + filterFields: CredentialsFilterFields, + sortFields: CredentialsSortFields +}; + +export { CredentialsToolbar as default, CredentialsToolbar, CredentialsFilterFields, CredentialsSortFields }; diff --git a/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap b/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap index 1e6c686f..f7c618eb 100644 --- a/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap +++ b/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap @@ -53,15 +53,15 @@ Array [ "keys": Array [ Object { "key": "form-dialog.label", - "match": "translate('form-dialog.label', { context: ['option', 'network'] })", + "match": "translate('form-dialog.label', { context: ['option', 'network', 'credential'] })", }, Object { "key": "form-dialog.label", - "match": "translate('form-dialog.label', { context: ['option', 'satellite'] })", + "match": "translate('form-dialog.label', { context: ['option', 'satellite', 'credential'] })", }, Object { "key": "form-dialog.label", - "match": "translate('form-dialog.label', { context: ['option', 'vcenter'] })", + "match": "translate('form-dialog.label', { context: ['option', 'vcenter', 'credential'] })", }, Object { "key": "form-dialog.label", @@ -406,6 +406,27 @@ Array [ }, ], }, + Object { + "file": "./src/components/credentials/credentialsToolbar.js", + "keys": Array [ + Object { + "key": "toolbar.label", + "match": "translate('toolbar.label', { context: ['option', API_QUERY_TYPES.SEARCH_NAME] })", + }, + Object { + "key": "toolbar.label", + "match": "translate('toolbar.label', { context: ['option', API_QUERY_TYPES.CREDENTIAL_TYPE] })", + }, + Object { + "key": "toolbar.label", + "match": "translate('toolbar.label', { context: ['option', API_QUERY_SORT_TYPES.NAME] })", + }, + Object { + "key": "toolbar.label", + "match": "translate('toolbar.label', { context: ['option', API_QUERY_SORT_TYPES.CREDENTIAL_TYPE] })", + }, + ], + }, Object { "file": "./src/components/mergeReportsDialog/mergeReportsDialog.js", "keys": Array [ @@ -624,6 +645,27 @@ Array [ }, ], }, + Object { + "file": "./src/components/scans/scansToolbar.js", + "keys": Array [ + Object { + "key": "toolbar.label", + "match": "translate('toolbar.label', { context: ['option', API_QUERY_TYPES.SEARCH_NAME] })", + }, + Object { + "key": "toolbar.label", + "match": "translate('toolbar.label', { context: ['option', API_QUERY_TYPES.SEARCH_SOURCES_NAME] })", + }, + Object { + "key": "toolbar.label", + "match": "translate('toolbar.label', { context: ['option', API_QUERY_SORT_TYPES.NAME] })", + }, + Object { + "key": "toolbar.label", + "match": "translate('toolbar.label', { context: ['option', API_QUERY_SORT_TYPES.MOST_RECENT_CONNECT_SCAN_START_TIME] })", + }, + ], + }, Object { "file": "./src/components/sources/sources.js", "keys": Array [ @@ -792,6 +834,35 @@ Array [ }, ], }, + Object { + "file": "./src/components/sources/sourcesToolbar.js", + "keys": Array [ + Object { + "key": "toolbar.label", + "match": "translate('toolbar.label', { context: ['option', API_QUERY_TYPES.SEARCH_NAME] })", + }, + Object { + "key": "toolbar.label", + "match": "translate('toolbar.label', { context: ['option', API_QUERY_TYPES.SEARCH_CREDENTIALS_NAME] })", + }, + Object { + "key": "toolbar.label", + "match": "translate('toolbar.label', { context: ['option', API_QUERY_TYPES.SOURCE_TYPE] })", + }, + Object { + "key": "toolbar.label", + "match": "translate('toolbar.label', { context: ['option', API_QUERY_SORT_TYPES.NAME] })", + }, + Object { + "key": "toolbar.label", + "match": "translate('toolbar.label', { context: ['option', API_QUERY_SORT_TYPES.SOURCE_TYPE] })", + }, + Object { + "key": "toolbar.label", + "match": "translate('toolbar.label', { context: ['option', API_QUERY_SORT_TYPES.MOST_RECENT_CONNECT_SCAN_START_TIME] })", + }, + ], + }, Object { "file": "./src/components/table/tableEmpty.js", "keys": Array [ @@ -805,6 +876,88 @@ Array [ }, ], }, + Object { + "file": "./src/components/viewToolbar/viewToolbar.js", + "keys": Array [ + Object { + "key": "toolbar.label", + "match": "t('toolbar.label', { context: ['chip', categoryValue] })", + }, + Object { + "key": "toolbar.label", + "match": "t('toolbar.label', { context: 'clear-filters' })", + }, + ], + }, + Object { + "file": "./src/components/viewToolbar/viewToolbarFieldSort.js", + "keys": Array [ + Object { + "key": "toolbar.label", + "match": "t('toolbar.label', { context: ['option', 'sort'] })", + }, + ], + }, + Object { + "file": "./src/components/viewToolbar/viewToolbarFieldSortButton.js", + "keys": Array [ + Object { + "key": "toolbar.label", + "match": "t('toolbar.label', { context: ['tooltip', 'sort', (isDescending && 'dsc')", + }, + ], + }, + Object { + "file": "./src/components/viewToolbar/viewToolbarSelect.js", + "keys": Array [ + Object { + "key": "form-dialog.label", + "match": "translate('form-dialog.label', { context: ['option', 'network'] })", + }, + Object { + "key": "form-dialog.label", + "match": "translate('form-dialog.label', { context: ['option', 'satellite'] })", + }, + Object { + "key": "form-dialog.label", + "match": "translate('form-dialog.label', { context: ['option', 'vcenter'] })", + }, + Object { + "key": "toolbar.label", + "match": "t('toolbar.label', { context: ['placeholder', 'filter', filter] })", + }, + Object { + "key": "toolbar.label", + "match": "t('toolbar.label', { context: ['placeholder', 'filter', filter] })", + }, + ], + }, + Object { + "file": "./src/components/viewToolbar/viewToolbarSelectCategory.js", + "keys": Array [ + Object { + "key": "toolbar.label", + "match": "t('toolbar.label', { context: ['placeholder', 'filter'] })", + }, + Object { + "key": "toolbar.label", + "match": "t('toolbar.label', { context: ['placeholder', 'filter'] })", + }, + ], + }, + Object { + "file": "./src/components/viewToolbar/viewToolbarTextInput.js", + "keys": Array [ + Object { + "key": "toolbar.label", + "match": "t('toolbar.label', { context: ['placeholder', 'filter', filter] })", + }, + Object { + "key": "toolbar.label", + "match": "t('toolbar.label', { context: ['placeholder', 'filter', filter] })", + }, + ], + }, ] `; @@ -906,6 +1059,22 @@ Array [ "file": "./src/components/credentials/credentialsEmptyState.js", "key": "view.empty-state", }, + Object { + "file": "./src/components/credentials/credentialsToolbar.js", + "key": "toolbar.label", + }, + Object { + "file": "./src/components/credentials/credentialsToolbar.js", + "key": "toolbar.label", + }, + Object { + "file": "./src/components/credentials/credentialsToolbar.js", + "key": "toolbar.label", + }, + Object { + "file": "./src/components/credentials/credentialsToolbar.js", + "key": "toolbar.label", + }, Object { "file": "./src/components/scanHostList/scanHostList.js", "key": "view.error", @@ -990,6 +1159,22 @@ Array [ "file": "./src/components/scans/scanSourceList.js", "key": "view.error-message", }, + Object { + "file": "./src/components/scans/scansToolbar.js", + "key": "toolbar.label", + }, + Object { + "file": "./src/components/scans/scansToolbar.js", + "key": "toolbar.label", + }, + Object { + "file": "./src/components/scans/scansToolbar.js", + "key": "toolbar.label", + }, + Object { + "file": "./src/components/scans/scansToolbar.js", + "key": "toolbar.label", + }, Object { "file": "./src/components/sources/sources.js", "key": "view.error", @@ -1054,6 +1239,30 @@ Array [ "file": "./src/components/sources/sourcesEmptyState.js", "key": "view.empty-state", }, + Object { + "file": "./src/components/sources/sourcesToolbar.js", + "key": "toolbar.label", + }, + Object { + "file": "./src/components/sources/sourcesToolbar.js", + "key": "toolbar.label", + }, + Object { + "file": "./src/components/sources/sourcesToolbar.js", + "key": "toolbar.label", + }, + Object { + "file": "./src/components/sources/sourcesToolbar.js", + "key": "toolbar.label", + }, + Object { + "file": "./src/components/sources/sourcesToolbar.js", + "key": "toolbar.label", + }, + Object { + "file": "./src/components/sources/sourcesToolbar.js", + "key": "toolbar.label", + }, Object { "file": "./src/components/table/tableEmpty.js", "key": "table.empty-state_title", @@ -1062,6 +1271,46 @@ Array [ "file": "./src/components/table/tableEmpty.js", "key": "table.empty-state_description", }, + Object { + "file": "./src/components/viewToolbar/viewToolbar.js", + "key": "toolbar.label", + }, + Object { + "file": "./src/components/viewToolbar/viewToolbar.js", + "key": "toolbar.label", + }, + Object { + "file": "./src/components/viewToolbar/viewToolbarFieldSort.js", + "key": "toolbar.label", + }, + Object { + "file": "./src/components/viewToolbar/viewToolbarFieldSortButton.js", + "key": "toolbar.label", + }, + Object { + "file": "./src/components/viewToolbar/viewToolbarSelect.js", + "key": "toolbar.label", + }, + Object { + "file": "./src/components/viewToolbar/viewToolbarSelect.js", + "key": "toolbar.label", + }, + Object { + "file": "./src/components/viewToolbar/viewToolbarSelectCategory.js", + "key": "toolbar.label", + }, + Object { + "file": "./src/components/viewToolbar/viewToolbarSelectCategory.js", + "key": "toolbar.label", + }, + Object { + "file": "./src/components/viewToolbar/viewToolbarTextInput.js", + "key": "toolbar.label", + }, + Object { + "file": "./src/components/viewToolbar/viewToolbarTextInput.js", + "key": "toolbar.label", + }, ] `; diff --git a/src/components/pageLayout/__tests__/__snapshots__/pageLayout.test.js.snap b/src/components/pageLayout/__tests__/__snapshots__/pageLayout.test.js.snap index 81ac4dbd..0540782e 100644 --- a/src/components/pageLayout/__tests__/__snapshots__/pageLayout.test.js.snap +++ b/src/components/pageLayout/__tests__/__snapshots__/pageLayout.test.js.snap @@ -357,6 +357,42 @@ exports[`PageLayout Component should render a non-connected component unauthoriz "page": 1, "page_size": 10, }, + "toolbar": Object { + "filterFields": Array [ + Object { + "component": [Function], + "selected": true, + "title": [Function], + "value": "search_by_name", + }, + Object { + "component": [Function], + "title": [Function], + "value": "search_credentials_by_name", + }, + Object { + "component": [Function], + "title": [Function], + "value": "source_type", + }, + ], + "sortFields": Array [ + Object { + "selected": true, + "title": [Function], + "value": "name", + }, + Object { + "title": [Function], + "value": "source_type", + }, + Object { + "isDefaultDescending": true, + "title": [Function], + "value": "most_recent_connect_scan__start_time", + }, + ], + }, "viewId": "sources", } } @@ -368,11 +404,10 @@ exports[`PageLayout Component should render a non-connected component unauthoriz useOnDelete={[Function]} useOnEdit={[Function]} useOnExpand={[Function]} - useOnRefresh={[Function]} useOnScan={[Function]} useOnSelect={[Function]} useOnShowAddSourceWizard={[Function]} - useSelectors={[Function]} + useToolbarFieldClearAll={[Function]} useView={[Function]} /> , @@ -391,6 +426,33 @@ exports[`PageLayout Component should render a non-connected component unauthoriz "page_size": 10, "scan_type": "inspect", }, + "toolbar": Object { + "filterFields": Array [ + Object { + "component": [Function], + "selected": true, + "title": [Function], + "value": "search_by_name", + }, + Object { + "component": [Function], + "title": [Function], + "value": "search_sources_by_name", + }, + ], + "sortFields": Array [ + Object { + "selected": true, + "title": [Function], + "value": "name", + }, + Object { + "isDefaultDescending": true, + "title": [Function], + "value": "most_recent_connect_scan__start_time", + }, + ], + }, "viewId": "scans", } } @@ -400,10 +462,9 @@ exports[`PageLayout Component should render a non-connected component unauthoriz useDispatch={[Function]} useGetScans={[Function]} useOnExpand={[Function]} - useOnRefresh={[Function]} useOnScanAction={[Function]} useOnSelect={[Function]} - useSelectors={[Function]} + useToolbarFieldClearAll={[Function]} useView={[Function]} /> , @@ -420,21 +481,45 @@ exports[`PageLayout Component should render a non-connected component unauthoriz "page": 1, "page_size": 10, }, + "toolbar": Object { + "filterFields": Array [ + Object { + "component": [Function], + "selected": true, + "title": [Function], + "value": "search_by_name", + }, + Object { + "component": [Function], + "title": [Function], + "value": "cred_type", + }, + ], + "sortFields": Array [ + Object { + "selected": true, + "title": [Function], + "value": "name", + }, + Object { + "title": [Function], + "value": "cred_type", + }, + ], + }, "viewId": "credentials", } } > , diff --git a/src/components/router/__tests__/__snapshots__/router.test.js.snap b/src/components/router/__tests__/__snapshots__/router.test.js.snap index e3923343..d5babf7f 100644 --- a/src/components/router/__tests__/__snapshots__/router.test.js.snap +++ b/src/components/router/__tests__/__snapshots__/router.test.js.snap @@ -23,6 +23,42 @@ exports[`Router Component should shallow render a basic component 1`] = ` "page": 1, "page_size": 10, }, + "toolbar": Object { + "filterFields": Array [ + Object { + "component": [Function], + "selected": true, + "title": [Function], + "value": "search_by_name", + }, + Object { + "component": [Function], + "title": [Function], + "value": "search_credentials_by_name", + }, + Object { + "component": [Function], + "title": [Function], + "value": "source_type", + }, + ], + "sortFields": Array [ + Object { + "selected": true, + "title": [Function], + "value": "name", + }, + Object { + "title": [Function], + "value": "source_type", + }, + Object { + "isDefaultDescending": true, + "title": [Function], + "value": "most_recent_connect_scan__start_time", + }, + ], + }, "viewId": "sources", } } @@ -34,11 +70,10 @@ exports[`Router Component should shallow render a basic component 1`] = ` useOnDelete={[Function]} useOnEdit={[Function]} useOnExpand={[Function]} - useOnRefresh={[Function]} useOnScan={[Function]} useOnSelect={[Function]} useOnShowAddSourceWizard={[Function]} - useSelectors={[Function]} + useToolbarFieldClearAll={[Function]} useView={[Function]} /> @@ -57,6 +92,33 @@ exports[`Router Component should shallow render a basic component 1`] = ` "page_size": 10, "scan_type": "inspect", }, + "toolbar": Object { + "filterFields": Array [ + Object { + "component": [Function], + "selected": true, + "title": [Function], + "value": "search_by_name", + }, + Object { + "component": [Function], + "title": [Function], + "value": "search_sources_by_name", + }, + ], + "sortFields": Array [ + Object { + "selected": true, + "title": [Function], + "value": "name", + }, + Object { + "isDefaultDescending": true, + "title": [Function], + "value": "most_recent_connect_scan__start_time", + }, + ], + }, "viewId": "scans", } } @@ -66,10 +128,9 @@ exports[`Router Component should shallow render a basic component 1`] = ` useDispatch={[Function]} useGetScans={[Function]} useOnExpand={[Function]} - useOnRefresh={[Function]} useOnScanAction={[Function]} useOnSelect={[Function]} - useSelectors={[Function]} + useToolbarFieldClearAll={[Function]} useView={[Function]} /> @@ -87,21 +148,45 @@ exports[`Router Component should shallow render a basic component 1`] = ` "page": 1, "page_size": 10, }, + "toolbar": Object { + "filterFields": Array [ + Object { + "component": [Function], + "selected": true, + "title": [Function], + "value": "search_by_name", + }, + Object { + "component": [Function], + "title": [Function], + "value": "cred_type", + }, + ], + "sortFields": Array [ + Object { + "selected": true, + "title": [Function], + "value": "name", + }, + Object { + "title": [Function], + "value": "cred_type", + }, + ], + }, "viewId": "credentials", } } > diff --git a/src/components/router/__tests__/__snapshots__/routerConstants.test.js.snap b/src/components/router/__tests__/__snapshots__/routerConstants.test.js.snap index 0056747f..08085c2b 100644 --- a/src/components/router/__tests__/__snapshots__/routerConstants.test.js.snap +++ b/src/components/router/__tests__/__snapshots__/routerConstants.test.js.snap @@ -13,6 +13,42 @@ Array [ "page": 1, "page_size": 10, }, + "toolbar": Object { + "filterFields": Array [ + Object { + "component": [Function], + "selected": true, + "title": [Function], + "value": "search_by_name", + }, + Object { + "component": [Function], + "title": [Function], + "value": "search_credentials_by_name", + }, + Object { + "component": [Function], + "title": [Function], + "value": "source_type", + }, + ], + "sortFields": Array [ + Object { + "selected": true, + "title": [Function], + "value": "name", + }, + Object { + "title": [Function], + "value": "source_type", + }, + Object { + "isDefaultDescending": true, + "title": [Function], + "value": "most_recent_connect_scan__start_time", + }, + ], + }, "viewId": "sources", } } @@ -24,11 +60,10 @@ Array [ useOnDelete={[Function]} useOnEdit={[Function]} useOnExpand={[Function]} - useOnRefresh={[Function]} useOnScan={[Function]} useOnSelect={[Function]} useOnShowAddSourceWizard={[Function]} - useSelectors={[Function]} + useToolbarFieldClearAll={[Function]} useView={[Function]} /> , @@ -47,6 +82,33 @@ Array [ "page_size": 10, "scan_type": "inspect", }, + "toolbar": Object { + "filterFields": Array [ + Object { + "component": [Function], + "selected": true, + "title": [Function], + "value": "search_by_name", + }, + Object { + "component": [Function], + "title": [Function], + "value": "search_sources_by_name", + }, + ], + "sortFields": Array [ + Object { + "selected": true, + "title": [Function], + "value": "name", + }, + Object { + "isDefaultDescending": true, + "title": [Function], + "value": "most_recent_connect_scan__start_time", + }, + ], + }, "viewId": "scans", } } @@ -56,10 +118,9 @@ Array [ useDispatch={[Function]} useGetScans={[Function]} useOnExpand={[Function]} - useOnRefresh={[Function]} useOnScanAction={[Function]} useOnSelect={[Function]} - useSelectors={[Function]} + useToolbarFieldClearAll={[Function]} useView={[Function]} /> , @@ -76,21 +137,45 @@ Array [ "page": 1, "page_size": 10, }, + "toolbar": Object { + "filterFields": Array [ + Object { + "component": [Function], + "selected": true, + "title": [Function], + "value": "search_by_name", + }, + Object { + "component": [Function], + "title": [Function], + "value": "cred_type", + }, + ], + "sortFields": Array [ + Object { + "selected": true, + "title": [Function], + "value": "name", + }, + Object { + "title": [Function], + "value": "cred_type", + }, + ], + }, "viewId": "credentials", } } > , diff --git a/src/components/scans/__tests__/__snapshots__/scanConstants.test.js.snap b/src/components/scans/__tests__/__snapshots__/scanConstants.test.js.snap deleted file mode 100644 index 8122e3e0..00000000 --- a/src/components/scans/__tests__/__snapshots__/scanConstants.test.js.snap +++ /dev/null @@ -1,34 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ScanTypes Should have specific scanFilterFields properties: scanFilterFields 1`] = ` -Array [ - Object { - "filterType": "text", - "id": "search_by_name", - "placeholder": "Filter by Name", - "title": "Name", - }, - Object { - "filterType": "text", - "id": "search_sources_by_name", - "placeholder": "Filter by Source Name", - "title": "Source", - }, -] -`; - -exports[`ScanTypes Should have specific scanSortFields properties: scanSortFields 1`] = ` -Array [ - Object { - "id": "name", - "isNumeric": false, - "title": "Name", - }, - Object { - "id": "most_recent_scanjob__start_time", - "isNumeric": true, - "sortAscending": false, - "title": "Most Recent", - }, -] -`; diff --git a/src/components/scans/__tests__/__snapshots__/scans.test.js.snap b/src/components/scans/__tests__/__snapshots__/scans.test.js.snap index 70083043..b4512164 100644 --- a/src/components/scans/__tests__/__snapshots__/scans.test.js.snap +++ b/src/components/scans/__tests__/__snapshots__/scans.test.js.snap @@ -21,7 +21,8 @@ exports[`Scans Component should handle multiple display states, pending, error, className="quipucords-view-container" > } - activeFilters={Array []} - filterFields={ - Array [ - Object { - "filterType": "text", - "id": "search_by_name", - "placeholder": "Filter by Name", - "title": "Name", - }, - Object { - "filterType": "text", - "id": "search_sources_by_name", - "placeholder": "Filter by Source Name", - "title": "Source", - }, - ] - } - filterType={Object {}} - filterValue="" - itemsType="Scan" - itemsTypePlural="Scans" - lastRefresh={NaN} - onRefresh={[Function]} - selectedCount={0} - sortAscending={true} - sortFields={ - Array [ - Object { - "id": "name", - "isNumeric": false, - "title": "Name", - }, - Object { - "id": "most_recent_scanjob__start_time", - "isNumeric": true, - "sortAscending": false, - "title": "Most Recent", - }, - ] - } - sortType={Object {}} - totalCount={0} - viewType="SCANS_VIEW" + t={[Function]} + useOnRefresh={[Function]} + useSelector={[Function]} + useToolbarFieldClear={[Function]} + useToolbarFieldClearAll={[Function]} + useView={[Function]} />
@@ -464,9 +428,9 @@ exports[`Scans Component should return an empty state when there are no scans: e diff --git a/src/components/scans/__tests__/__snapshots__/scansContext.test.js.snap b/src/components/scans/__tests__/__snapshots__/scansContext.test.js.snap index 405d5c25..85bf03f1 100644 --- a/src/components/scans/__tests__/__snapshots__/scansContext.test.js.snap +++ b/src/components/scans/__tests__/__snapshots__/scansContext.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ScansContext should apply a hook for retrieving data from multiple selectors: responses 1`] = ` +exports[`ScansContext should apply a hook for retrieving data from multiple selectors: selector responses 1`] = ` Object { "errorResponse": Object { "data": Array [], @@ -59,6 +59,44 @@ Object { } `; +exports[`ScansContext should apply a hook for returning a get response: get responses 1`] = ` +Object { + "errorResponse": Object { + "error": true, + "message": "Lorem ipsum", + }, + "fulfilledResponse": Object { + "data": Object { + "view": Object { + "results": Array [ + "dolor", + "sit", + ], + }, + }, + "fulfilled": true, + }, + "mockStoreSuccessResponse": Object { + "data": Array [ + "lorem", + "ipsum", + ], + "date": undefined, + "error": false, + "errorMessage": null, + "expandedRows": Object {}, + "fulfilled": true, + "hasData": false, + "pending": false, + "selectedRows": Object {}, + "totalResults": 0, + }, + "pendingResponse": Object { + "pending": true, + }, +} +`; + exports[`ScansContext should attempt to poll scans: timeout 1`] = ` Array [ Array [ @@ -109,12 +147,11 @@ Object { "scan_type": "inspect", }, "VIEW_ID": "scans", - "useContextGetScans": [Function], "useGetScans": [Function], "useOnExpand": [Function], - "useOnRefresh": [Function], "useOnScanAction": [Function], "useOnSelect": [Function], "usePoll": [Function], + "useScans": [Function], } `; diff --git a/src/components/scans/__tests__/__snapshots__/scansToolbar.test.js.snap b/src/components/scans/__tests__/__snapshots__/scansToolbar.test.js.snap new file mode 100644 index 00000000..6e387999 --- /dev/null +++ b/src/components/scans/__tests__/__snapshots__/scansToolbar.test.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ScansToolbar should have specific properties: ScansToolbar 1`] = ` +Object { + "filterFields": Array [ + Object { + "component": [Function], + "selected": true, + "title": [Function], + "value": "search_by_name", + }, + Object { + "component": [Function], + "title": [Function], + "value": "search_sources_by_name", + }, + ], + "sortFields": Array [ + Object { + "selected": true, + "title": [Function], + "value": "name", + }, + Object { + "isDefaultDescending": true, + "title": [Function], + "value": "most_recent_connect_scan__start_time", + }, + ], +} +`; diff --git a/src/components/scans/__tests__/scanConstants.test.js b/src/components/scans/__tests__/scanConstants.test.js deleted file mode 100644 index f3ccd145..00000000 --- a/src/components/scans/__tests__/scanConstants.test.js +++ /dev/null @@ -1,11 +0,0 @@ -import { ScanFilterFields, ScanSortFields } from '../scanConstants'; - -describe('ScanTypes', () => { - it('Should have specific scanFilterFields properties', () => { - expect(ScanFilterFields).toMatchSnapshot('scanFilterFields'); - }); - - it('Should have specific scanSortFields properties', () => { - expect(ScanSortFields).toMatchSnapshot('scanSortFields'); - }); -}); diff --git a/src/components/scans/__tests__/scans.test.js b/src/components/scans/__tests__/scans.test.js index 33adece1..1ffe3c57 100644 --- a/src/components/scans/__tests__/scans.test.js +++ b/src/components/scans/__tests__/scans.test.js @@ -64,7 +64,9 @@ describe('Scans Component', () => { expect(component).toMatchSnapshot('empty state, no data'); component.setProps({ - useSelectors: () => [{ activeFilters: ['test filter'] }] + useView: () => ({ + isFilteringActive: true + }) }); expect(component.find(EmptyState)).toMatchSnapshot('empty state, filtering active'); diff --git a/src/components/scans/__tests__/scansContext.test.js b/src/components/scans/__tests__/scansContext.test.js index 5862f429..7405ab8d 100644 --- a/src/components/scans/__tests__/scansContext.test.js +++ b/src/components/scans/__tests__/scansContext.test.js @@ -1,6 +1,5 @@ -import { context, useGetScans, useOnScanAction, usePoll } from '../scansContext'; +import { context, useGetScans, useOnScanAction, usePoll, useScans } from '../scansContext'; import { apiTypes } from '../../../constants/apiConstants'; -import { reduxTypes } from '../../../redux'; describe('ScansContext', () => { it('should return specific properties', () => { @@ -57,27 +56,69 @@ describe('ScansContext', () => { it('should apply a hook for retrieving data from multiple selectors', () => { const { result: errorResponse } = shallowHook(() => - useGetScans({ + useScans({ useSelectorsResponse: () => ({ error: true, message: 'Lorem ipsum' }) }) ); const { result: pendingResponse } = shallowHook(() => - useGetScans({ + useScans({ useSelectorsResponse: () => ({ pending: true }) }) ); const { result: fulfilledResponse } = shallowHook(() => - useGetScans({ + useScans({ useSelectorsResponse: () => ({ fulfilled: true, data: { view: { results: ['dolor', 'sit'] } } }) }) ); + const { result: mockStoreSuccessResponse } = shallowHook(() => useScans(), { + state: { + view: { + update: {} + }, + scans: { + expanded: {}, + selected: {}, + view: { + fulfilled: true, + data: { + results: ['lorem', 'ipsum'] + } + } + } + } + }); + + expect({ errorResponse, fulfilledResponse, pendingResponse, mockStoreSuccessResponse }).toMatchSnapshot( + 'selector responses' + ); + }); + + it('should apply a hook for returning a get response', () => { + const { result: errorResponse } = shallowHook(() => + useGetScans({ + useScans: () => ({ error: true, message: 'Lorem ipsum' }) + }) + ); + + const { result: pendingResponse } = shallowHook(() => + useGetScans({ + useScans: () => ({ pending: true }) + }) + ); + + const { result: fulfilledResponse } = shallowHook(() => + useGetScans({ + useScans: () => ({ fulfilled: true, data: { view: { results: ['dolor', 'sit'] } } }) + }) + ); + const { result: mockStoreSuccessResponse } = shallowHook(() => useGetScans(), { state: { - viewOptions: { - [reduxTypes.view.SCANS_VIEW]: {} + view: { + update: {} }, scans: { expanded: {}, @@ -93,7 +134,7 @@ describe('ScansContext', () => { }); expect({ errorResponse, fulfilledResponse, pendingResponse, mockStoreSuccessResponse }).toMatchSnapshot( - 'responses' + 'get responses' ); }); }); diff --git a/src/components/scans/__tests__/scansEmptyState.test.js b/src/components/scans/__tests__/scansEmptyState.test.js index 0959de64..9fb1ff19 100644 --- a/src/components/scans/__tests__/scansEmptyState.test.js +++ b/src/components/scans/__tests__/scansEmptyState.test.js @@ -4,7 +4,7 @@ import { ScansEmptyState } from '../scansEmptyState'; describe('ScansEmptyState Component', () => { it('should render a basic component', async () => { const props = { - useContextGetSources: () => ({ + useSources: () => ({ totalResults: 20, hasData: true }) @@ -16,7 +16,7 @@ describe('ScansEmptyState Component', () => { it('should render messaging if sources do not exist', async () => { const props = { - useContextGetSources: () => ({ + useSources: () => ({ hasData: false }) }; diff --git a/src/components/scans/__tests__/scansToolbar.test.js b/src/components/scans/__tests__/scansToolbar.test.js new file mode 100644 index 00000000..86f6f4ac --- /dev/null +++ b/src/components/scans/__tests__/scansToolbar.test.js @@ -0,0 +1,7 @@ +import { ScansToolbar } from '../scansToolbar'; + +describe('ScansToolbar', () => { + it('should have specific properties', () => { + expect(ScansToolbar).toMatchSnapshot('ScansToolbar'); + }); +}); diff --git a/src/components/scans/scanConstants.js b/src/components/scans/scanConstants.js deleted file mode 100644 index 138bd283..00000000 --- a/src/components/scans/scanConstants.js +++ /dev/null @@ -1,33 +0,0 @@ -const ScanFilterFields = [ - { - id: 'search_by_name', - title: 'Name', - placeholder: 'Filter by Name', - filterType: 'text' - }, - { - id: 'search_sources_by_name', - title: 'Source', - placeholder: 'Filter by Source Name', - filterType: 'text' - } -]; - -/** - * ID: Enum with the following possible values [id, name, scan_type, most_recent_scanjob__start_time, most_recent_scanjob__status] - */ -const ScanSortFields = [ - { - id: 'name', - title: 'Name', - isNumeric: false - }, - { - id: 'most_recent_scanjob__start_time', - title: 'Most Recent', - isNumeric: true, - sortAscending: false - } -]; - -export { ScanFilterFields, ScanSortFields }; diff --git a/src/components/scans/scans.js b/src/components/scans/scans.js index 63cb9e39..1a6f55e9 100644 --- a/src/components/scans/scans.js +++ b/src/components/scans/scans.js @@ -19,30 +19,22 @@ import { Modal, ModalVariant } from '../modal/modal'; import { Tooltip } from '../tooltip/tooltip'; import { reduxTypes, storeHooks } from '../../redux'; import { useView } from '../view/viewContext'; -import ViewToolbar from '../viewToolbar/viewToolbar'; -import ViewPaginationRow from '../viewPaginationRow/viewPaginationRow'; +import { useToolbarFieldClearAll } from '../viewToolbar/viewToolbarContext'; +import { ViewToolbar } from '../viewToolbar/viewToolbar'; +import { ViewPaginationRow } from '../viewPaginationRow/viewPaginationRow'; import { ScansEmptyState } from './scansEmptyState'; -import { ScanFilterFields, ScanSortFields } from './scanConstants'; import { Table } from '../table/table'; import { scansTableCells } from './scansTableCells'; -import { - VIEW_ID, - INITIAL_QUERY, - useGetScans, - useOnExpand, - useOnRefresh, - useOnScanAction, - useOnSelect -} from './scansContext'; +import { VIEW_ID, INITIAL_QUERY, useGetScans, useOnExpand, useOnScanAction, useOnSelect } from './scansContext'; +import { ScansToolbar } from './scansToolbar'; import { translate } from '../i18n/i18n'; const CONFIG = { viewId: VIEW_ID, - initialQuery: INITIAL_QUERY + initialQuery: INITIAL_QUERY, + toolbar: ScansToolbar }; -// ToDo: review onMergeReports, renderToolbarActions being standalone with upcoming toolbar updates -// ToDo: review items being selected and the page polling. Randomized dev data gives the appearance of an issue. Also applies to sources selected items /** * A scans view. * @@ -50,11 +42,10 @@ const CONFIG = { * @param {Function} props.t * @param {Function} props.useGetScans * @param {Function} props.useOnExpand - * @param {Function} props.useOnRefresh * @param {Function} props.useOnScanAction * @param {Function} props.useOnSelect * @param {Function} props.useDispatch - * @param {Function} props.useSelectors + * @param {Function} props.useToolbarFieldClearAll * @param {Function} props.useView * @returns {React.ReactNode} */ @@ -62,17 +53,16 @@ const Scans = ({ t, useGetScans: useAliasGetScans, useOnExpand: useAliasOnExpand, - useOnRefresh: useAliasOnRefresh, useOnScanAction: useAliasOnScanAction, useOnSelect: useAliasOnSelect, useDispatch: useAliasDispatch, - useSelectors: useAliasSelectors, + useToolbarFieldClearAll: useAliasToolbarFieldClearAll, useView: useAliasView }) => { - const { viewId } = useAliasView(); + const onToolbarFieldClearAll = useAliasToolbarFieldClearAll(); + const { isFilteringActive, viewId } = useAliasView(); const dispatch = useAliasDispatch(); const onExpand = useAliasOnExpand(); - const onRefresh = useAliasOnRefresh(); const { onCancel, onDownload, onPause, onRestart, onStart } = useAliasOnScanAction(); const onSelect = useAliasOnSelect(); const { @@ -83,24 +73,10 @@ const Scans = ({ date, data, selectedRows = {}, - expandedRows = {} + expandedRows = {}, + totalResults } = useAliasGetScans(); - const [viewOptions = {}] = useAliasSelectors([ - ({ viewOptions: stateViewOptions }) => stateViewOptions[reduxTypes.view.SCANS_VIEW] - ]); - const isActive = viewOptions?.activeFilters?.length > 0 || data?.length > 0 || false; - - /** - * Clear toolbar filters - * - * @event onToolbarFieldClearAll - */ - const onToolbarFieldClearAll = () => { - dispatch({ - type: reduxTypes.viewToolbar.CLEAR_FILTERS, - viewType: reduxTypes.view.SCANS_VIEW - }); - }; + const isActive = isFilteringActive || data?.length > 0 || false; /** * Toolbar actions onScanSources @@ -159,19 +135,8 @@ const Scans = ({
{isActive && ( - onRefresh()} - lastRefresh={new Date(date).getTime()} - actions={renderToolbarActions()} - itemsType="Scan" - itemsTypePlural="Scans" - selectedCount={viewOptions.selectedItems?.length} - {...viewOptions} - /> - + + )}
@@ -257,36 +222,34 @@ const Scans = ({ /** * Prop types * - * @type {{useView: Function, useOnSelect: Function, t: Function, useOnRefresh: Function, useOnScanAction: Function, - * useDispatch: Function, useGetScans: Function, useOnExpand: Function, useSelectors: Function}} + * @type {{useOnSelect: Function, useView: Function, t: Function, useOnScanAction: Function, + * useDispatch: Function, useGetScans: Function, useOnExpand: Function, useToolbarFieldClearAll: Function}} */ Scans.propTypes = { t: PropTypes.func, useDispatch: PropTypes.func, useGetScans: PropTypes.func, useOnExpand: PropTypes.func, - useOnRefresh: PropTypes.func, useOnScanAction: PropTypes.func, useOnSelect: PropTypes.func, - useSelectors: PropTypes.func, + useToolbarFieldClearAll: PropTypes.func, useView: PropTypes.func }; /** * Default props * - * @type {{useView: Function, useOnSelect: Function, t: translate, useOnRefresh: Function, useOnScanAction: Function, - * useDispatch: Function, useGetScans: Function, useOnExpand: Function, useSelectors: Function}} + * @type {{useOnSelect: Function, useView: Function, t: translate, useOnScanAction: Function, + * useDispatch: Function, useGetScans: Function, useOnExpand: Function, useToolbarFieldClearAll: Function}} */ Scans.defaultProps = { t: translate, useDispatch: storeHooks.reactRedux.useDispatch, useGetScans, useOnExpand, - useOnRefresh, useOnScanAction, useOnSelect, - useSelectors: storeHooks.reactRedux.useSelectors, + useToolbarFieldClearAll, useView }; diff --git a/src/components/scans/scansContext.js b/src/components/scans/scansContext.js index 8228afac..63021796 100644 --- a/src/components/scans/scansContext.js +++ b/src/components/scans/scansContext.js @@ -3,6 +3,7 @@ import { AlertVariant } from '@patternfly/react-core'; import { useShallowCompareEffect } from 'react-use'; import { reduxActions, reduxTypes, storeHooks } from '../../redux'; import { useTimeout } from '../../hooks'; +import { useView } from '../view/viewContext'; import { API_QUERY_SORT_TYPES, API_QUERY_TYPES, apiTypes } from '../../constants/apiConstants'; import { helpers } from '../../common'; import { translate } from '../i18n/i18n'; @@ -47,23 +48,6 @@ const useOnExpand = ({ useDispatch: useAliasDispatch = storeHooks.reactRedux.use }; }; -/** - * On refresh view. - * - * @param {object} options - * @param {Function} options.useDispatch - * @returns {Function} - */ -const useOnRefresh = ({ useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch } = {}) => { - const dispatch = useAliasDispatch(); - - return () => { - dispatch({ - type: reduxTypes.scans.UPDATE_SCANS - }); - }; -}; - /** * Report/scan actions cancel, pause, restart, start, and download. * @@ -76,6 +60,7 @@ const useOnRefresh = ({ useDispatch: useAliasDispatch = storeHooks.reactRedux.us * @param {Function} options.t * @param {Function} options.useDispatch * @param {Function} options.useSelectorsResponse + * @param {Function} options.useView * @returns {{onRestart: Function, onDownload: Function, onStart: Function, onCancel: Function, onPause: Function}} */ const useOnScanAction = ({ @@ -86,8 +71,10 @@ const useOnScanAction = ({ startScan = reduxActions.scans.startScan, t = translate, useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, - useSelectorsResponse: useAliasSelectorsResponse = storeHooks.reactRedux.useSelectorsResponse + useSelectorsResponse: useAliasSelectorsResponse = storeHooks.reactRedux.useSelectorsResponse, + useView: useAliasView = useView } = {}) => { + const { viewId } = useAliasView(); const [updatedScan, setUpdatedScan] = useState({}); const { id: scanId, name: scanName, context: scanContext } = updatedScan; const dispatch = useAliasDispatch(); @@ -126,14 +113,15 @@ const useOnScanAction = ({ dispatch([ ...dispatchList, { - type: reduxTypes.scans.UPDATE_SCANS + type: reduxTypes.view.UPDATE_VIEW, + viewId } ]); setUpdatedScan({}); } } - }, [dispatch, error, errorMessage, fulfilled, pending, scanContext, scanId, scanName, t]); + }, [dispatch, error, errorMessage, fulfilled, pending, scanContext, scanId, scanName, t, viewId]); /** * onCancel for scanning @@ -260,31 +248,21 @@ const usePoll = ({ }; /** - * Get scans + * Use scans' response * * @param {object} options - * @param {Function} options.getScans - * @param {Function} options.useDispatch - * @param {Function} options.usePoll * @param {Function} options.useSelectors * @param {Function} options.useSelectorsResponse - * @returns {{date: *, data: *[], pending: boolean, errorMessage: null, fulfilled: boolean, selectedRows: *, - * expandedRows: *, error: boolean}} + * @returns {{date: *, totalResults: (*|number), data: *[], pending: boolean, hasData: boolean, errorMessage: null, + * fulfilled: boolean, selectedRows: *, expandedRows: *, error: boolean}} */ -const useGetScans = ({ - getScans = reduxActions.scans.getScans, - useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, - usePoll: useAliasPoll = usePoll, +const useScans = ({ useSelectors: useAliasSelectors = storeHooks.reactRedux.useSelectors, useSelectorsResponse: useAliasSelectorsResponse = storeHooks.reactRedux.useSelectorsResponse } = {}) => { - const dispatch = useAliasDispatch(); - const pollUpdate = useAliasPoll(); - const [refreshUpdate, selectedRows, expandedRows, viewOptions] = useAliasSelectors([ - ({ scans }) => scans?.update, + const [selectedRows, expandedRows] = useAliasSelectors([ ({ scans }) => scans?.selected, - ({ scans }) => scans?.expanded, - ({ viewOptions: stateViewOptions }) => stateViewOptions?.[reduxTypes.view.SCANS_VIEW] + ({ scans }) => scans?.expanded ]); const { data: responseData, @@ -298,11 +276,6 @@ const useGetScans = ({ const [{ date } = {}] = responses?.list || []; const { [apiTypes.API_RESPONSE_SCANS_COUNT]: totalResults, [apiTypes.API_RESPONSE_SCANS_RESULTS]: data = [] } = responseData?.view || {}; - const query = helpers.createViewQueryObject(viewOptions, { [apiTypes.API_QUERY_TYPES.SCAN_TYPE]: 'inspect' }); - - useShallowCompareEffect(() => { - getScans(query)(dispatch); - }, [dispatch, getScans, pollUpdate, query, refreshUpdate]); return { pending, @@ -319,30 +292,48 @@ const useGetScans = ({ }; /** - * Get scans in the context of the scans view. + * Get scans * * @param {object} options - * @param {Function} options.useGetScans - * @returns {{date: *, data: *[], pending: boolean, errorMessage: null, fulfilled: boolean, selectedRows: *, expandedRows: *, error: boolean}} + * @param {Function} options.getScans + * @param {Function} options.useDispatch + * @param {Function} options.usePoll + * @param {Function} options.useScans + * @param {Function} options.useSelectors + * @param {Function} options.useView + * @returns {{date: *, totalResults: (*|number), data: *[], pending: boolean, hasData: boolean, errorMessage: null, + * fulfilled: boolean, selectedRows: *, expandedRows: *, error: boolean}} */ -const useContextGetScans = ({ useGetScans: useAliasGetScans = useGetScans } = {}) => { - const results = useAliasGetScans(); +const useGetScans = ({ + getScans = reduxActions.scans.getScans, + useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, + usePoll: useAliasPoll = usePoll, + useScans: useAliasScans = useScans, + useSelectors: useAliasSelectors = storeHooks.reactRedux.useSelectors, + useView: useAliasView = useView +} = {}) => { + const { query, viewId } = useAliasView(); + const dispatch = useAliasDispatch(); + const pollUpdate = useAliasPoll(); + const [refreshUpdate] = useAliasSelectors([({ view }) => view.update?.[viewId]]); + const response = useAliasScans(); - return { - ...results - }; + useShallowCompareEffect(() => { + getScans(query)(dispatch); + }, [dispatch, getScans, pollUpdate, query, refreshUpdate]); + + return response; }; const context = { VIEW_ID, INITIAL_QUERY, - useContextGetScans, useGetScans, useOnExpand, - useOnRefresh, useOnScanAction, useOnSelect, - usePoll + usePoll, + useScans }; export { @@ -350,11 +341,10 @@ export { context, VIEW_ID, INITIAL_QUERY, - useContextGetScans, useGetScans, useOnExpand, - useOnRefresh, useOnScanAction, useOnSelect, - usePoll + usePoll, + useScans }; diff --git a/src/components/scans/scansEmptyState.js b/src/components/scans/scansEmptyState.js index bd617242..5c6312c4 100644 --- a/src/components/scans/scansEmptyState.js +++ b/src/components/scans/scansEmptyState.js @@ -12,7 +12,7 @@ import { import { AddCircleOIcon } from '@patternfly/react-icons'; import { useNavigate } from '../router/routerContext'; import { useView } from '../view/viewContext'; -import { useContextGetSources } from '../sources/sourcesContext'; +import { useSources } from '../sources/sourcesContext'; import { helpers } from '../../common'; import { reduxTypes, storeHooks } from '../../redux'; import { translate } from '../i18n/i18n'; @@ -23,24 +23,24 @@ import { translate } from '../i18n/i18n'; * @param {object} props * @param {Function} props.t * @param {string} props.uiShortName - * @param {Function} props.useContextGetSources * @param {Function} props.useDispatch * @param {Function} props.useNavigate + * @param {Function} props.useSources * @param {Function} props.useView * @returns {React.ReactNode} */ const ScansEmptyState = ({ t, uiShortName, - useContextGetSources: useAliasContextGetSources, useDispatch: useAliasDispatch, useNavigate: useAliasNavigate, + useSources: useAliasSources, useView: useAliasView }) => { const { viewId } = useAliasView(); const dispatch = useAliasDispatch(); const navigate = useAliasNavigate(); - const { totalResults, hasData } = useAliasContextGetSources(); + const { totalResults, hasData } = useAliasSources(); const onAddSource = () => { if (hasData) { @@ -73,7 +73,7 @@ const ScansEmptyState = ({ /** * Prop types * - * @type {{uiShortName: string, useView: Function, t: Function, useContextGetSources: Function, useDispatch: Function, + * @type {{uiShortName: string, useView: Function, t: Function, useSources: Function, useDispatch: Function, * useNavigate: Function}} */ ScansEmptyState.propTypes = { @@ -81,14 +81,14 @@ ScansEmptyState.propTypes = { uiShortName: PropTypes.string, useDispatch: PropTypes.func, useNavigate: PropTypes.func, - useContextGetSources: PropTypes.func, + useSources: PropTypes.func, useView: PropTypes.func }; /** * Default props * - * @type {{uiShortName: string, useView: Function, t: translate, useContextGetSources: Function, useDispatch: Function, + * @type {{uiShortName: string, useView: Function, t: translate, useSources: Function, useDispatch: Function, * useNavigate: Function}} */ ScansEmptyState.defaultProps = { @@ -96,7 +96,7 @@ ScansEmptyState.defaultProps = { uiShortName: helpers.UI_SHORT_NAME, useDispatch: storeHooks.reactRedux.useDispatch, useNavigate, - useContextGetSources, + useSources, useView }; diff --git a/src/components/scans/scansToolbar.js b/src/components/scans/scansToolbar.js new file mode 100644 index 00000000..163d71d1 --- /dev/null +++ b/src/components/scans/scansToolbar.js @@ -0,0 +1,53 @@ +import React from 'react'; +import { translate } from '../i18n/i18n'; +import { ViewToolbarTextInput } from '../viewToolbar/viewToolbarTextInput'; +import { API_QUERY_SORT_TYPES, API_QUERY_TYPES } from '../../constants/apiConstants'; + +/** + * Available filtering + * + * @type {{component: React.ReactNode, selected: boolean, title: Function|string, selected: boolean}[]} + */ +const ScansFilterFields = [ + { + title: () => translate('toolbar.label', { context: ['option', API_QUERY_TYPES.SEARCH_NAME] }), + value: API_QUERY_TYPES.SEARCH_NAME, + component: function SearchName(props) { + return ; + }, + selected: true + }, + { + title: () => translate('toolbar.label', { context: ['option', API_QUERY_TYPES.SEARCH_SOURCES_NAME] }), + value: API_QUERY_TYPES.SEARCH_SOURCES_NAME, + component: function SearchSourcesName(props) { + return ; + } + } +]; + +/** + * Available sorting + * + * @type {{isNumeric: boolean, title: string|Function, value: string, selected: boolean}[]} + */ +const ScansSortFields = [ + { + title: () => translate('toolbar.label', { context: ['option', API_QUERY_SORT_TYPES.NAME] }), + value: API_QUERY_SORT_TYPES.NAME, + selected: true + }, + { + title: () => + translate('toolbar.label', { context: ['option', API_QUERY_SORT_TYPES.MOST_RECENT_CONNECT_SCAN_START_TIME] }), + value: API_QUERY_SORT_TYPES.MOST_RECENT_CONNECT_SCAN_START_TIME, + isDefaultDescending: true + } +]; + +const ScansToolbar = { + filterFields: ScansFilterFields, + sortFields: ScansSortFields +}; + +export { ScansToolbar as default, ScansToolbar, ScansFilterFields, ScansSortFields }; diff --git a/src/components/sources/__tests__/__snapshots__/sources.test.js.snap b/src/components/sources/__tests__/__snapshots__/sources.test.js.snap index 9996bc87..cfe03202 100644 --- a/src/components/sources/__tests__/__snapshots__/sources.test.js.snap +++ b/src/components/sources/__tests__/__snapshots__/sources.test.js.snap @@ -21,7 +21,8 @@ exports[`Sources Component should handle multiple display states, pending, error className="quipucords-view-container" > -
-
-
+ + + + + + + + + + + + + dolor sit + + + + + +`; + +exports[`ViewToolbar Component should handle updating toolbar chips: chips 1`] = ` +Object { + "categoryName": "t(toolbar.label_option, {\\"context\\":\\"search_by_name\\"})", + "children": , + "chips": Array [ + "t(toolbar.label_chip, {\\"context\\":\\"lorem ipsum\\"})", + ], + "deleteChip": [Function], + "showToolbarItem": true, +} +`; + +exports[`ViewToolbar Component should hide categories when a single filter is available: single filter 1`] = ` + + +
+ + - -
-
- -
+ + + +
+ + +`; + +exports[`ViewToolbar Component should render a basic component: basic 1`] = ` + + + + + } > -
-

+ + + - No Filters -

-
+ + - 200 -
-
-
-
-
-
+ + + + + + + + + + + + + + + + + + + + `; diff --git a/src/components/viewToolbar/__tests__/__snapshots__/viewToolbarContext.test.js.snap b/src/components/viewToolbar/__tests__/__snapshots__/viewToolbarContext.test.js.snap new file mode 100644 index 00000000..dedf7a11 --- /dev/null +++ b/src/components/viewToolbar/__tests__/__snapshots__/viewToolbarContext.test.js.snap @@ -0,0 +1,58 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ToolbarContext should apply a hook for clearing a toolbar field through redux: clear single field 1`] = ` +Array [ + Array [ + Array [ + Object { + "type": "RESET_PAGE", + "viewId": "lorem", + }, + Object { + "filter": "lorem", + "type": "SET_QUERY", + "value": undefined, + "viewId": "lorem", + }, + ], + ], +] +`; + +exports[`ToolbarContext should apply a hook for clearing all related toolbar select filters: clear all filter fields 1`] = ` +Array [ + Array [ + Array [ + Object { + "type": "RESET_PAGE", + "viewId": "lorem", + }, + Object { + "filter": "search_by_name", + "type": "SET_QUERY", + "value": undefined, + "viewId": "lorem", + }, + Object { + "filter": "search_credentials_by_name", + "type": "SET_QUERY", + "value": undefined, + "viewId": "lorem", + }, + Object { + "filter": "source_type", + "type": "SET_QUERY", + "value": undefined, + "viewId": "lorem", + }, + ], + ], +] +`; + +exports[`ToolbarContext should return specific properties: specific properties 1`] = ` +Object { + "useToolbarFieldClear": [Function], + "useToolbarFieldClearAll": [Function], +} +`; diff --git a/src/components/viewToolbar/__tests__/__snapshots__/viewToolbarFieldSort.test.js.snap b/src/components/viewToolbar/__tests__/__snapshots__/viewToolbarFieldSort.test.js.snap new file mode 100644 index 00000000..be8a16f6 --- /dev/null +++ b/src/components/viewToolbar/__tests__/__snapshots__/viewToolbarFieldSort.test.js.snap @@ -0,0 +1,80 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ViewToolbarFieldSort Component should handle updating the view query through redux state with a component: dispatch type, component 1`] = ` +Array [ + Array [ + Array [ + Object { + "filter": "ordering", + "type": "SET_QUERY", + "value": "name", + "viewId": undefined, + }, + ], + ], +] +`; + +exports[`ViewToolbarFieldSort Component should handle updating the view query through redux state with a hook: dispatch type, hook 1`] = ` +Array [ + Array [ + Array [ + Object { + "filter": "ordering", + "type": "SET_QUERY", + "value": "dolor sit", + "viewId": undefined, + }, + ], + ], +] +`; + +exports[`ViewToolbarFieldSort Component should render a basic component: basic 1`] = ` + + + + +`; diff --git a/src/components/viewToolbar/__tests__/__snapshots__/viewToolbarFieldSortButton.test.js.snap b/src/components/viewToolbar/__tests__/__snapshots__/viewToolbarFieldSortButton.test.js.snap new file mode 100644 index 00000000..5d6e4fec --- /dev/null +++ b/src/components/viewToolbar/__tests__/__snapshots__/viewToolbarFieldSortButton.test.js.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ViewToolbarFieldSortButton Component should handle updating the view query through redux state with a component: dispatch type, component 1`] = ` +Array [ + Array [ + Array [ + Object { + "filter": "ordering", + "type": "SET_QUERY", + "value": "hello world", + "viewId": undefined, + }, + ], + ], +] +`; + +exports[`ViewToolbarFieldSortButton Component should handle updating the view query through redux state with a hook: dispatch type, hook 1`] = ` +Array [ + Array [ + Array [ + Object { + "filter": "ordering", + "type": "SET_QUERY", + "value": "dolor sit", + "viewId": undefined, + }, + ], + ], +] +`; + +exports[`ViewToolbarFieldSortButton Component should render a basic component: basic 1`] = ` + + + +`; diff --git a/src/components/viewToolbar/__tests__/__snapshots__/viewToolbarSelect.test.js.snap b/src/components/viewToolbar/__tests__/__snapshots__/viewToolbarSelect.test.js.snap new file mode 100644 index 00000000..7738a98d --- /dev/null +++ b/src/components/viewToolbar/__tests__/__snapshots__/viewToolbarSelect.test.js.snap @@ -0,0 +1,118 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ViewToolbarSelect Component should export select variants and related options: field variants, options 1`] = ` +Object { + "SelectFilterVariant": Object { + "cred_type": "cred_type", + "source_type": "source_type", + }, + "SelectFilterVariantOptions": Object { + "cred_type": Array [ + Object { + "title": [Function], + "value": "network", + }, + Object { + "title": [Function], + "value": "satellite", + }, + Object { + "title": [Function], + "value": "vcenter", + }, + ], + "source_type": Array [ + Object { + "title": [Function], + "value": "network", + }, + Object { + "title": [Function], + "value": "satellite", + }, + Object { + "title": [Function], + "value": "vcenter", + }, + ], + }, +} +`; + +exports[`ViewToolbarSelect Component should handle updating the view query through redux state with a component: dispatch type, component 1`] = ` +Array [ + Array [ + Array [ + Object { + "type": "RESET_PAGE", + "viewId": undefined, + }, + Object { + "filter": "cred_type", + "type": "SET_QUERY", + "value": "network", + "viewId": undefined, + }, + ], + ], +] +`; + +exports[`ViewToolbarSelect Component should handle updating the view query through redux state with a hook: dispatch type, hook 1`] = ` +Array [ + Array [ + Array [ + Object { + "type": "RESET_PAGE", + "viewId": undefined, + }, + Object { + "filter": "lorem filter", + "type": "SET_QUERY", + "value": "dolor sit", + "viewId": undefined, + }, + ], + ], +] +`; + +exports[`ViewToolbarSelect Component should render a basic component: basic 1`] = ` + +`; diff --git a/src/components/viewToolbar/__tests__/__snapshots__/viewToolbarSelectCategory.test.js.snap b/src/components/viewToolbar/__tests__/__snapshots__/viewToolbarSelectCategory.test.js.snap new file mode 100644 index 00000000..272518e8 --- /dev/null +++ b/src/components/viewToolbar/__tests__/__snapshots__/viewToolbarSelectCategory.test.js.snap @@ -0,0 +1,87 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ViewToolbarSelectCategory Component should handle updating the view query through redux state with a component: dispatch type, component 1`] = ` +Array [ + Array [ + Array [ + Object { + "currentFilterCategory": "search_by_name", + "type": "SET_FILTER", + "viewId": undefined, + }, + ], + ], + Array [ + Array [ + Object { + "currentFilterCategory": "search_by_name", + "type": "SET_FILTER", + "viewId": undefined, + }, + ], + ], +] +`; + +exports[`ViewToolbarSelectCategory Component should handle updating the view query through redux state with a hook: dispatch type, hook 1`] = ` +Array [ + Array [ + Array [ + Object { + "currentFilterCategory": "dolor sit", + "type": "SET_FILTER", + "viewId": undefined, + }, + ], + ], +] +`; + +exports[`ViewToolbarSelectCategory Component should render a basic component: basic 1`] = ` + + } + variant="single" +/> +`; diff --git a/src/components/viewToolbar/__tests__/__snapshots__/viewToolbarTextInput.test.js.snap b/src/components/viewToolbar/__tests__/__snapshots__/viewToolbarTextInput.test.js.snap new file mode 100644 index 00000000..52e9b475 --- /dev/null +++ b/src/components/viewToolbar/__tests__/__snapshots__/viewToolbarTextInput.test.js.snap @@ -0,0 +1,90 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ViewToolbarTextInput Component should export variants: field variants 1`] = ` +Object { + "TextInputFilterVariants": Object { + "search_by_name": "search_by_name", + "search_credentials_by_name": "search_credentials_by_name", + "search_sources_by_name": "search_sources_by_name", + }, +} +`; + +exports[`ViewToolbarTextInput Component should handle clearing the view query through redux state with a hook: dispatch onClear, hook 1`] = ` +Array [ + Array [ + Array [ + Object { + "type": "RESET_PAGE", + "viewId": undefined, + }, + Object { + "filter": "lorem filter", + "type": "SET_QUERY", + "value": "", + "viewId": undefined, + }, + ], + ], +] +`; + +exports[`ViewToolbarTextInput Component should handle updating the view query through redux state with a component: dispatch type, component 1`] = ` +Array [ + Array [ + Array [ + Object { + "type": "RESET_PAGE", + "viewId": undefined, + }, + Object { + "filter": "search_credentials_by_name", + "type": "SET_QUERY", + "value": "dolor sit", + "viewId": undefined, + }, + ], + ], +] +`; + +exports[`ViewToolbarTextInput Component should handle updating the view query through redux state with a hook: dispatch type, hook 1`] = ` +Array [ + Array [ + Array [ + Object { + "type": "RESET_PAGE", + "viewId": undefined, + }, + Object { + "filter": "lorem filter", + "type": "SET_QUERY", + "value": "dolor sit", + "viewId": undefined, + }, + ], + ], +] +`; + +exports[`ViewToolbarTextInput Component should render a basic component: basic 1`] = ` + + + +`; diff --git a/src/components/viewToolbar/__tests__/viewToolbar.test.js b/src/components/viewToolbar/__tests__/viewToolbar.test.js index 5f844868..9c1c51fe 100644 --- a/src/components/viewToolbar/__tests__/viewToolbar.test.js +++ b/src/components/viewToolbar/__tests__/viewToolbar.test.js @@ -1,33 +1,61 @@ import React from 'react'; -import { mount } from 'enzyme'; -import ViewToolbar from '../viewToolbar'; -import { viewTypes } from '../../../redux/constants'; +import { ToolbarFilter } from '@patternfly/react-core'; +import { ViewToolbar } from '../viewToolbar'; +import { ViewToolbarTextInput } from '../viewToolbarTextInput'; +import { API_QUERY_TYPES } from '../../../constants/apiConstants'; +import { CONFIG as sourcesConfig } from '../../sources/sources'; -describe('ViewPaginationRow Component', () => { - it('should render', () => { +describe('ViewToolbar Component', () => { + it('should render a basic component', async () => { const props = { - viewType: viewTypes.SCANS_VIEW, - totalCount: 200, - filterFields: [ - { - id: 'filterOne', - title: 'Name', - placeholder: 'Filter by Name', - filterType: 'text' + useView: () => ({ viewId: sourcesConfig.viewId, config: { toolbar: sourcesConfig.toolbar } }) + }; + const component = await shallowHookComponent(); + + expect(component).toMatchSnapshot('basic'); + }); + + it('should hide categories when a single filter is available', async () => { + const props = { + useView: () => ({ + viewId: sourcesConfig.viewId, + config: { + toolbar: { filterFields: [sourcesConfig.toolbar.filterFields[0]] } } - ], - sortFields: [ - { - id: 'sortOne', - title: 'Name', - isNumeric: false + }) + }; + const component = await mountHookComponent(); + + expect(component.find(ViewToolbarTextInput).first()).toMatchSnapshot('single filter'); + }); + + it('should handle updating toolbar chips', async () => { + const props = { + useView: () => ({ + viewId: sourcesConfig.viewId, + query: { [API_QUERY_TYPES.SEARCH_NAME]: 'lorem ipsum' }, + config: { + toolbar: { filterFields: [sourcesConfig.toolbar.filterFields[0]] } } - ], - onRefresh: jest.fn() + }) }; + const component = await shallowHookComponent(); + + expect(component.find(ToolbarFilter).props()).toMatchSnapshot('chips'); + }); - const component = mount(); + it('should handle displaying secondary components, fields', async () => { + const props = { + useView: () => ({ + viewId: sourcesConfig.viewId, + config: { + toolbar: { filterFields: [sourcesConfig.toolbar.filterFields[0]] } + } + }), + secondaryFields: dolor sit + }; + const component = await shallowHookComponent(); - expect(component.render()).toMatchSnapshot(); + expect(component).toMatchSnapshot('secondary'); }); }); diff --git a/src/components/viewToolbar/__tests__/viewToolbarContext.test.js b/src/components/viewToolbar/__tests__/viewToolbarContext.test.js new file mode 100644 index 00000000..ced2bee5 --- /dev/null +++ b/src/components/viewToolbar/__tests__/viewToolbarContext.test.js @@ -0,0 +1,41 @@ +import { context, useToolbarFieldClear, useToolbarFieldClearAll } from '../viewToolbarContext'; +import { CONFIG as sourcesConfig } from '../../sources/sources'; + +describe('ToolbarContext', () => { + it('should return specific properties', () => { + expect(context).toMatchSnapshot('specific properties'); + }); + + it('should apply a hook for clearing a toolbar field through redux', () => { + const mockDispatch = jest.fn(); + const options = { + useView: () => ({ + viewId: 'lorem' + }), + useDispatch: () => mockDispatch + }; + const { result: onClearField } = shallowHook(() => useToolbarFieldClear(options)); + onClearField('lorem'); + + expect(mockDispatch.mock.calls).toMatchSnapshot('clear single field'); + mockDispatch.mockClear(); + }); + + it('should apply a hook for clearing all related toolbar select filters', () => { + const mockDispatch = jest.fn(); + const options = { + useView: () => ({ + viewId: 'lorem', + config: { + toolbar: sourcesConfig.toolbar + } + }), + useDispatch: () => mockDispatch + }; + const { result: onClearAllFields } = shallowHook(() => useToolbarFieldClearAll(options)); + onClearAllFields(); + + expect(mockDispatch.mock.calls).toMatchSnapshot('clear all filter fields'); + mockDispatch.mockClear(); + }); +}); diff --git a/src/components/viewToolbar/__tests__/viewToolbarFieldSort.test.js b/src/components/viewToolbar/__tests__/viewToolbarFieldSort.test.js new file mode 100644 index 00000000..c1e12dd2 --- /dev/null +++ b/src/components/viewToolbar/__tests__/viewToolbarFieldSort.test.js @@ -0,0 +1,47 @@ +import React from 'react'; +import { DropdownSelect } from '../../dropdownSelect/dropdownSelect'; +import { ViewToolbarFieldSort, useOnSelect } from '../viewToolbarFieldSort'; +import { CONFIG as sourcesConfig } from '../../sources/sources'; +import { store } from '../../../redux/store'; + +describe('ViewToolbarFieldSort Component', () => { + let mockDispatch; + + beforeEach(() => { + mockDispatch = jest.spyOn(store, 'dispatch').mockImplementation((type, data) => ({ type, data })); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render a basic component', async () => { + const props = { + useView: () => ({ viewId: sourcesConfig.viewId, config: { toolbar: sourcesConfig.toolbar } }) + }; + const component = await shallowHookComponent(); + + expect(component).toMatchSnapshot('basic'); + }); + + it('should handle updating the view query through redux state with a component', async () => { + const props = { + useView: () => ({ viewId: sourcesConfig.viewId, config: { toolbar: sourcesConfig.toolbar } }) + }; + + const component = await mountHookComponent(); + component.find(DropdownSelect).first().find('button').simulate('click'); + component.update(); + component.find('button.pf-c-select__menu-item').first().simulate('click'); + + expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch type, component'); + }); + + it('should handle updating the view query through redux state with a hook', async () => { + const options = {}; + const { result: onSelect } = await shallowHook(() => useOnSelect('lorem filter', options)); + + onSelect({ value: 'dolor sit' }); + expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch type, hook'); + }); +}); diff --git a/src/components/viewToolbar/__tests__/viewToolbarFieldSortButton.test.js b/src/components/viewToolbar/__tests__/viewToolbarFieldSortButton.test.js new file mode 100644 index 00000000..611c2d7a --- /dev/null +++ b/src/components/viewToolbar/__tests__/viewToolbarFieldSortButton.test.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { ViewToolbarFieldSortButton, useOnClick } from '../viewToolbarFieldSortButton'; +import { API_QUERY_TYPES } from '../../../constants/apiConstants'; +import { store } from '../../../redux/store'; + +describe('ViewToolbarFieldSortButton Component', () => { + let mockDispatch; + + beforeEach(() => { + mockDispatch = jest.spyOn(store, 'dispatch').mockImplementation((type, data) => ({ type, data })); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render a basic component', async () => { + const props = {}; + const component = await shallowHookComponent(); + + expect(component).toMatchSnapshot('basic'); + }); + + it('should handle updating the view query through redux state with a component', async () => { + const props = { + useQuery: () => ({ [API_QUERY_TYPES.ORDERING]: '-hello world' }) + }; + const component = await mountHookComponent(); + component.find('button').simulate('click'); + + expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch type, component'); + }); + + it('should handle updating the view query through redux state with a hook', async () => { + const options = {}; + const { result: onClick } = await shallowHook(() => useOnClick(options)); + + onClick({ value: 'dolor sit' }); + expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch type, hook'); + }); +}); diff --git a/src/components/viewToolbar/__tests__/viewToolbarSelect.test.js b/src/components/viewToolbar/__tests__/viewToolbarSelect.test.js new file mode 100644 index 00000000..2e2ce9cb --- /dev/null +++ b/src/components/viewToolbar/__tests__/viewToolbarSelect.test.js @@ -0,0 +1,54 @@ +import React from 'react'; +import { ViewToolbarSelect, SelectFilterVariant, SelectFilterVariantOptions, useOnSelect } from '../viewToolbarSelect'; +import { API_QUERY_TYPES } from '../../../constants/apiConstants'; +import { store } from '../../../redux/store'; + +describe('ViewToolbarSelect Component', () => { + let mockDispatch; + + beforeEach(() => { + mockDispatch = jest.spyOn(store, 'dispatch').mockImplementation((type, data) => ({ type, data })); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render a basic component', async () => { + const props = { + filter: SelectFilterVariant[API_QUERY_TYPES.CREDENTIAL_TYPE] + }; + const component = await shallowHookComponent(); + + expect(component).toMatchSnapshot('basic'); + }); + + it('should export select variants and related options', () => { + expect({ + SelectFilterVariant, + SelectFilterVariantOptions + }).toMatchSnapshot('field variants, options'); + }); + + it('should handle updating the view query through redux state with a component', async () => { + const props = { + filter: SelectFilterVariant[API_QUERY_TYPES.CREDENTIAL_TYPE] + }; + + const component = await mountHookComponent(); + + component.find('button').simulate('click'); + component.update(); + component.find('button.pf-c-select__menu-item').first().simulate('click'); + + expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch type, component'); + }); + + it('should handle updating the view query through redux state with a hook', async () => { + const options = {}; + const { result: onSelect } = await shallowHook(() => useOnSelect('lorem filter', options)); + + onSelect({ value: 'dolor sit' }); + expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch type, hook'); + }); +}); diff --git a/src/components/viewToolbar/__tests__/viewToolbarSelectCategory.test.js b/src/components/viewToolbar/__tests__/viewToolbarSelectCategory.test.js new file mode 100644 index 00000000..f85b7163 --- /dev/null +++ b/src/components/viewToolbar/__tests__/viewToolbarSelectCategory.test.js @@ -0,0 +1,47 @@ +import React from 'react'; +import { ViewToolbarSelectCategory, useOnSelect } from '../viewToolbarSelectCategory'; +import { CONFIG as sourcesConfig } from '../../sources/sources'; +import { store } from '../../../redux/store'; + +describe('ViewToolbarSelectCategory Component', () => { + let mockDispatch; + + beforeEach(() => { + mockDispatch = jest.spyOn(store, 'dispatch').mockImplementation((type, data) => ({ type, data })); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render a basic component', async () => { + const props = { + useView: () => ({ viewId: sourcesConfig.viewId, config: { toolbar: sourcesConfig.toolbar } }) + }; + const component = await shallowHookComponent(); + + expect(component).toMatchSnapshot('basic'); + }); + + it('should handle updating the view query through redux state with a component', async () => { + const props = { + useView: () => ({ viewId: sourcesConfig.viewId, config: { toolbar: sourcesConfig.toolbar } }) + }; + + const component = await mountHookComponent(); + + component.find('button').simulate('click'); + component.update(); + component.find('button.pf-c-select__menu-item').first().simulate('click'); + + expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch type, component'); + }); + + it('should handle updating the view query through redux state with a hook', async () => { + const options = {}; + const { result: onSelect } = await shallowHook(() => useOnSelect(options)); + + onSelect({ value: 'dolor sit' }); + expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch type, hook'); + }); +}); diff --git a/src/components/viewToolbar/__tests__/viewToolbarTextInput.test.js b/src/components/viewToolbar/__tests__/viewToolbarTextInput.test.js new file mode 100644 index 00000000..4bcad081 --- /dev/null +++ b/src/components/viewToolbar/__tests__/viewToolbarTextInput.test.js @@ -0,0 +1,63 @@ +import React from 'react'; +import { TextInput } from '../../form/textInput'; +import { ViewToolbarTextInput, TextInputFilterVariants, useOnClear, useOnSubmit } from '../viewToolbarTextInput'; +import { API_QUERY_TYPES } from '../../../constants/apiConstants'; +import { store } from '../../../redux/store'; + +describe('ViewToolbarTextInput Component', () => { + let mockDispatch; + + beforeEach(() => { + jest.useFakeTimers(); + mockDispatch = jest.spyOn(store, 'dispatch').mockImplementation((type, data) => ({ type, data })); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render a basic component', async () => { + const props = { + filter: TextInputFilterVariants[API_QUERY_TYPES.SEARCH_CREDENTIALS_NAME] + }; + const component = await shallowHookComponent(); + + expect(component).toMatchSnapshot('basic'); + }); + + it('should export variants', () => { + expect({ + TextInputFilterVariants + }).toMatchSnapshot('field variants'); + }); + + it('should handle updating the view query through redux state with a component', async () => { + const props = { + filter: TextInputFilterVariants[API_QUERY_TYPES.SEARCH_CREDENTIALS_NAME] + }; + + const component = await shallowHookComponent(); + component.find(TextInput).simulate('change', { value: '' }); + component.find(TextInput).simulate('keyUp', { value: 'dolor sit' }); + + expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch type, component'); + }); + + it('should handle updating the view query through redux state with a hook', async () => { + const options = {}; + const { result: onSelect } = await shallowHook(() => useOnSubmit('lorem filter', options)); + + onSelect({ value: 'dolor sit' }); + expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch type, hook'); + }); + + it('should handle clearing the view query through redux state with a hook', async () => { + const options = { + useSelector: () => 'dolor sit' + }; + const { result: onClear } = await shallowHook(() => useOnClear('lorem filter', options)); + + onClear(); + expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch onClear, hook'); + }); +}); diff --git a/src/components/viewToolbar/viewToolbar.js b/src/components/viewToolbar/viewToolbar.js index ea0e7942..ce41ea25 100644 --- a/src/components/viewToolbar/viewToolbar.js +++ b/src/components/viewToolbar/viewToolbar.js @@ -1,293 +1,142 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Button, Filter, Sort, Toolbar } from 'patternfly-react'; -import _size from 'lodash/size'; -import { reduxTypes, store } from '../../redux'; -import Tooltip from '../tooltip/tooltip'; -import RefreshTimeButton from '../refreshTimeButton/refreshTimeButton'; -import helpers from '../../common/helpers'; - -class ViewToolbar extends React.Component { - componentDidMount() { - const { filterType, sortType, filterFields, sortFields } = this.props; - - if (!filterType) { - this.onSelectFilterType(filterFields[0]); - } - - if (!sortType) { - this.onUpdateCurrentSortType(sortFields[0]); - } - } - - onFilterAdded = (field, value) => { - const { viewType } = this.props; - - let filterText = ''; - if (field.title) { - filterText = field.title; - } else { - filterText = field; - } - filterText += ': '; - - if (value.title) { - filterText += value.title; - } else { - filterText += value; - } - - const filter = { field, value, label: filterText }; - store.dispatch({ - type: reduxTypes.viewToolbar.ADD_FILTER, - viewType, - filter - }); - }; - - onSelectFilterType = filterType => { - const { viewType } = this.props; - store.dispatch({ - type: reduxTypes.viewToolbar.SET_FILTER_TYPE, - viewType, - filterType - }); - }; - - onFilterValueSelected = newFilterValue => { - const { filterType, viewType } = this.props; - - store.dispatch({ - type: reduxTypes.viewToolbar.SET_FILTER_VALUE, - viewType, - filterValue: newFilterValue - }); - - if (newFilterValue) { - this.onFilterAdded(filterType, newFilterValue); - } - }; - - onValueKeyPress = keyEvent => { - const { filterType, viewType } = this.props; - const filterValue = keyEvent.target.value; - - if (keyEvent.key === 'Enter' && filterValue && filterValue.length) { - this.onFilterAdded(filterType, filterValue); - keyEvent.stopPropagation(); - keyEvent.preventDefault(); - - store.dispatch({ - type: reduxTypes.viewToolbar.SET_FILTER_VALUE, - viewType, - filterValue - }); - } - }; - - onRemoveFilter = filter => { - const { viewType } = this.props; - store.dispatch({ - type: reduxTypes.viewToolbar.REMOVE_FILTER, - viewType, - filter - }); - }; - - onClearFilters = () => { - const { viewType } = this.props; - store.dispatch({ - type: reduxTypes.viewToolbar.CLEAR_FILTERS, - viewType - }); - }; - - onUpdateCurrentSortType = sortType => { - const { viewType } = this.props; - store.dispatch({ - type: reduxTypes.viewToolbar.SET_SORT_TYPE, - viewType, - sortType - }); - }; - - onToggleCurrentSortDirection = () => { - const { viewType } = this.props; - store.dispatch({ - type: reduxTypes.viewToolbar.TOGGLE_SORT_ASCENDING, - viewType - }); +import { + Divider, + Toolbar, + ToolbarContent, + ToolbarFilter, + ToolbarGroup, + ToolbarItem, + ToolbarItemVariant, + ToolbarToggleGroup +} from '@patternfly/react-core'; +import { FilterIcon } from '@patternfly/react-icons'; +import { storeHooks } from '../../redux'; +import { useToolbarFieldClear, useToolbarFieldClearAll } from './viewToolbarContext'; +import { useOnRefresh, useView } from '../view/viewContext'; +import { ViewToolbarSelectCategory } from './viewToolbarSelectCategory'; +import { ViewToolbarFieldSort } from './viewToolbarFieldSort'; +import { RefreshTimeButton } from '../refreshTimeButton/refreshTimeButton'; +import { translate } from '../i18n/i18n'; + +const ViewToolbar = ({ + lastRefresh, + secondaryFields, + t, + useOnRefresh: useAliasOnRefresh, + useSelector: useAliasSelector, + useToolbarFieldClear: useAliasToolbarFieldClear, + useToolbarFieldClearAll: useAliasToolbarFieldClearAll, + useView: useAliasView +}) => { + const { config, query, viewId } = useAliasView(); + const updatedCategoryFields = config?.toolbar?.filterFields || []; + + const currentCategory = useAliasSelector(({ view }) => view?.filters?.[viewId]?.currentFilterCategory); + const onRefresh = useAliasOnRefresh(); + const clearField = useAliasToolbarFieldClear(); + const clearAllFields = useAliasToolbarFieldClearAll(); + + /** + * Clear a specific filter + * + * @event onClearFilter + * @param {object} params + * @param {*} params.value + * @returns {void} + */ + const onClearFilter = ({ value }) => clearField(value); + + /** + * Clear all active filters. + * + * @event onClearAll + * @returns {void} + */ + const onClearAll = () => clearAllFields(); + + /** + * Set selected options for chip display. + * + * @param {*|string} value + * @returns {Array} + */ + const setSelectedOptions = value => { + const categoryValue = query?.[value]; + return (categoryValue && [t('toolbar.label', { context: ['chip', categoryValue] })]) || []; }; - renderFilterInput() { - const { filterType, filterValue } = this.props; - - if (!filterType) { - return null; - } - - if (filterType.filterType === 'select') { - return ( - - ); - } - - return ( - this.onValueKeyPress(e)} - /> - ); - } - - renderFilter() { - const { filterType, filterFields } = this.props; - - if (_size(filterFields)) { - return ( - - - {this.renderFilterInput()} - - ); - } - - return null; - } - - renderSort() { - const { sortType, sortAscending, sortFields } = this.props; - - if (sortType) { - return ( - - - - this.onToggleCurrentSortDirection()} - /> - - - ); - } - - return null; - } - - renderRefresh() { - const { onRefresh, lastRefresh } = this.props; - - return ( -
- -
- ); - } - - renderCounts() { - const { totalCount, selectedCount, itemsType, itemsTypePlural } = this.props; - - return ( -
- {selectedCount > 0 ? `${selectedCount} of ` : null} - {`${totalCount} ${totalCount === 1 ? itemsType : itemsTypePlural}`} - {selectedCount > 0 ? ' selected' : ''} -
- ); - } - - renderActiveFilters() { - const { activeFilters } = this.props; - - if (_size(activeFilters)) { - return [ - Active Filters:, - - {activeFilters.map(item => ( - - {item.label} - - ))} - , - - ]; - } - - return No Filters; - } - - render() { - const { actions } = this.props; - - return ( - - {this.renderFilter()} - {this.renderSort()} - {this.renderRefresh()} - {actions} - - {this.renderActiveFilters()} - {this.renderCounts()} - + return ( + + + + } breakpoint="lg"> + + {updatedCategoryFields.length > 1 && ( + + + + )} + {updatedCategoryFields.map(({ title, value, component: OptionComponent }) => { + const chipProps = { categoryName: (typeof title === 'function' && title()) || title }; + chipProps.chips = setSelectedOptions(value); + chipProps.deleteChip = () => onClearFilter({ value }); + + return ( + + + + ); + })} + + + + + + + + + + + + {secondaryFields} + + - ); - } -} + + + ); +}; ViewToolbar.propTypes = { - viewType: PropTypes.string, - totalCount: PropTypes.number, - selectedCount: PropTypes.number, - filterFields: PropTypes.array, - sortFields: PropTypes.array, - onRefresh: PropTypes.func, + secondaryFields: PropTypes.node, lastRefresh: PropTypes.number, - actions: PropTypes.node, - itemsType: PropTypes.string, - itemsTypePlural: PropTypes.string, - filterType: PropTypes.object, - filterValue: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - activeFilters: PropTypes.array, - sortType: PropTypes.object, - sortAscending: PropTypes.bool + t: PropTypes.func, + useOnRefresh: PropTypes.func, + useSelector: PropTypes.func, + useToolbarFieldClear: PropTypes.func, + useToolbarFieldClearAll: PropTypes.func, + useView: PropTypes.func }; ViewToolbar.defaultProps = { - viewType: null, - totalCount: 0, - selectedCount: 0, - filterFields: [], - sortFields: [], - onRefresh: helpers.noop, + secondaryFields: [], lastRefresh: 0, - actions: null, - itemsType: '', - itemsTypePlural: '', - filterType: {}, - filterValue: '', - activeFilters: [], - sortType: {}, - sortAscending: true + t: translate, + useSelector: storeHooks.reactRedux.useSelector, + useOnRefresh, + useToolbarFieldClear, + useToolbarFieldClearAll, + useView }; export { ViewToolbar as default, ViewToolbar }; diff --git a/src/components/viewToolbar/viewToolbarContext.js b/src/components/viewToolbar/viewToolbarContext.js new file mode 100644 index 00000000..9aa6a082 --- /dev/null +++ b/src/components/viewToolbar/viewToolbarContext.js @@ -0,0 +1,77 @@ +import { reduxTypes, storeHooks } from '../../redux'; +import { useView } from '../view/viewContext'; + +/** + * Clear a specific toolbar category. + * + * @param {object} options + * @param {Function} options.useDispatch + * @param {Function} options.useView + * @returns {Function} + */ + +const useToolbarFieldClear = ({ + useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, + useView: useAliasView = useView +} = {}) => { + const { viewId } = useAliasView(); + const dispatch = useAliasDispatch(); + + return filter => + dispatch([ + { + type: reduxTypes.view.RESET_PAGE, + viewId + }, + { + type: reduxTypes.view.SET_QUERY, + viewId, + filter, + value: undefined + } + ]); +}; + +/** + * Clear all available toolbar categories. + * + * @param {object} options + * @param {Function} options.useDispatch + * @param {Function} options.useView + * @returns {Function} + */ +const useToolbarFieldClearAll = ({ + useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, + useView: useAliasView = useView +} = {}) => { + const dispatch = useAliasDispatch(); + const { viewId, config } = useAliasView(); + const options = config?.toolbar?.filterFields; + + return () => { + const resetFilters = [ + { + type: reduxTypes.view.RESET_PAGE, + viewId + } + ]; + + options.forEach(({ value: filter }) => { + resetFilters.push({ + type: reduxTypes.view.SET_QUERY, + viewId, + filter, + value: undefined + }); + }); + + dispatch(resetFilters); + }; +}; + +const context = { + useToolbarFieldClear, + useToolbarFieldClearAll +}; + +export { context as default, context, useToolbarFieldClear, useToolbarFieldClearAll }; diff --git a/src/components/viewToolbar/viewToolbarFieldSort.js b/src/components/viewToolbar/viewToolbarFieldSort.js new file mode 100644 index 00000000..3970e21c --- /dev/null +++ b/src/components/viewToolbar/viewToolbarFieldSort.js @@ -0,0 +1,105 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { reduxTypes, storeHooks } from '../../redux'; +import { useView } from '../view/viewContext'; +import { ViewToolbarFieldSortButton } from './viewToolbarFieldSortButton'; +import { DropdownSelect } from '../dropdownSelect/dropdownSelect'; +import { API_QUERY_TYPES } from '../../constants/apiConstants'; +import { translate } from '../i18n/i18n'; + +/** + * On select category for sorting. + * + * @param {object} options + * @param {Function} options.useDispatch + * @param {Function} options.useView + * @returns {Function} + */ +const useOnSelect = ({ + useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, + useView: useAliasView = useView +} = {}) => { + const { viewId } = useAliasView(); + const dispatch = useAliasDispatch(); + + return ({ value }) => + dispatch([ + { + type: reduxTypes.view.SET_QUERY, + viewId, + filter: API_QUERY_TYPES.ORDERING, + value + } + ]); +}; + +/** + * Toolbar sort button wrapper. + * + * @fires onSelectUpdated + * @param {object} props + * @param {Function} props.t + * @param {Function} props.useOnSelect + * @param {Function} props.useView + * @returns {React.ReactNode} + */ +const ViewToolbarFieldSort = ({ t, useOnSelect: useAliasOnSelect, useView: useAliasView }) => { + const onSelect = useAliasOnSelect(); + const { query, config } = useAliasView(); + const selectedOption = query?.[API_QUERY_TYPES.ORDERING]; + const { sortFields } = config?.toolbar || {}; + + /** + * Intercept selected value to apply default dsc/asc + * + * @event onSelectUpdated + * @param {object} event + * @param {*} event.value + * @param {object} event.selected + */ + const onSelectUpdated = ({ value, selected }) => { + let updatedValue = value; + if (selected?.isDefaultDescending && selectedOption !== value) { + updatedValue = `-${value}`; + } + + onSelect({ value: updatedValue }); + }; + + return ( + + + + + ); +}; + +/** + * Prop types + * + * @type {{useView: Function, t: Function, useSelector: Function}} + */ +ViewToolbarFieldSort.propTypes = { + t: PropTypes.func, + useOnSelect: PropTypes.func, + useView: PropTypes.func +}; + +/** + * Default props + * + * @type {{useOnSelect: Function, t: Function, useView: Function}} + */ +ViewToolbarFieldSort.defaultProps = { + t: translate, + useOnSelect, + useView +}; + +export { ViewToolbarFieldSort as default, ViewToolbarFieldSort, useOnSelect }; diff --git a/src/components/viewToolbar/viewToolbarFieldSortButton.js b/src/components/viewToolbar/viewToolbarFieldSortButton.js new file mode 100644 index 00000000..8f26b0d4 --- /dev/null +++ b/src/components/viewToolbar/viewToolbarFieldSortButton.js @@ -0,0 +1,96 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, ButtonVariant } from '@patternfly/react-core'; +import { SortAmountDownAltIcon, SortAmountUpIcon } from '@patternfly/react-icons'; +import { Tooltip } from '../tooltip/tooltip'; +import { reduxTypes, storeHooks } from '../../redux'; +import { useQuery, useView } from '../view/viewContext'; +import { API_QUERY_TYPES } from '../../constants/apiConstants'; +import { translate } from '../i18n/i18n'; + +/** + * On click sorting. + * + * @param {object} options + * @param {Function} options.useDispatch + * @param {Function} options.useView + * @returns {Function} + */ +const useOnClick = ({ + useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, + useView: useAliasView = useView +} = {}) => { + const { viewId } = useAliasView(); + const dispatch = useAliasDispatch(); + + return ({ value }) => { + dispatch([ + { + type: reduxTypes.view.SET_QUERY, + viewId, + filter: API_QUERY_TYPES.ORDERING, + value + } + ]); + }; +}; + +/** + * Toolbar sort button wrapper. + * + * @param {object} props + * @param {Function} props.t + * @param {Function} props.useOnClick + * @param {Function} props.useQuery + * @param {object} props.props + * @returns {React.ReactNode} + */ +const ViewToolbarFieldSortButton = ({ t, useOnClick: useAliasOnClick, useQuery: useAliasQuery, ...props }) => { + const onClick = useAliasOnClick(); + const { [API_QUERY_TYPES.ORDERING]: ordering } = useAliasQuery(); + + const isDescending = /^-/.test(ordering); + const updatedOrdering = ordering?.replace(/^-/, '') || ''; + const isEmpty = updatedOrdering === ''; + const updatedDirection = isDescending ? updatedOrdering : `-${updatedOrdering}`; + + return ( + + + + ); +}; + +/** + * Prop types + * + * @type {{useQuery: Function, viewId: string, useOnClick: Function}} + */ +ViewToolbarFieldSortButton.propTypes = { + t: PropTypes.func, + useOnClick: PropTypes.func, + useQuery: PropTypes.func +}; + +/** + * Default props + * + * @type {{useQuery: Function, useOnClick: Function}} + */ +ViewToolbarFieldSortButton.defaultProps = { + t: translate, + useOnClick, + useQuery +}; + +export { ViewToolbarFieldSortButton as default, ViewToolbarFieldSortButton, useOnClick }; diff --git a/src/components/viewToolbar/viewToolbarSelect.js b/src/components/viewToolbar/viewToolbarSelect.js new file mode 100644 index 00000000..c479bba9 --- /dev/null +++ b/src/components/viewToolbar/viewToolbarSelect.js @@ -0,0 +1,152 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { DropdownSelect, SelectPosition } from '../dropdownSelect/dropdownSelect'; +import { reduxTypes, storeHooks } from '../../redux'; +import { useView } from '../view/viewContext'; +import { API_QUERY_TYPES } from '../../constants/apiConstants'; +import { translate } from '../i18n/i18n'; + +/* + * Credential, and source, field options + * + * @type {{title: Function|string, value: string}[]} + */ +const credentialSourceTypeFieldOptions = [ + { title: () => translate('form-dialog.label', { context: ['option', 'network'] }), value: 'network' }, + { title: () => translate('form-dialog.label', { context: ['option', 'satellite'] }), value: 'satellite' }, + { title: () => translate('form-dialog.label', { context: ['option', 'vcenter'] }), value: 'vcenter' } +]; + +/** + * Available select options + * + * @type {{'[API_QUERY_TYPES.CREDENTIAL_TYPE]': Array, '[API_QUERY_TYPES.SOURCE_TYPE]': Array}} + */ +const SelectFilterVariantOptions = { + [API_QUERY_TYPES.CREDENTIAL_TYPE]: credentialSourceTypeFieldOptions, + [API_QUERY_TYPES.SOURCE_TYPE]: credentialSourceTypeFieldOptions +}; + +/** + * Available select filters + * + * @type {{'[API_QUERY_TYPES.CREDENTIAL_TYPE]': string, '[API_QUERY_TYPES.SOURCE_TYPE]': string}} + */ +const SelectFilterVariant = { + [API_QUERY_TYPES.CREDENTIAL_TYPE]: API_QUERY_TYPES.CREDENTIAL_TYPE, + [API_QUERY_TYPES.SOURCE_TYPE]: API_QUERY_TYPES.SOURCE_TYPE +}; + +/** + * On select + * + * @param {string} filter + * @param {object} options + * @param {Function} options.useDispatch + * @param {Function} options.useView + * @returns {Function} + */ +const useOnSelect = ( + filter, + { useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, useView: useAliasView = useView } = {} +) => { + const { viewId } = useAliasView(); + const dispatch = useAliasDispatch(); + + return ({ value = null }) => { + dispatch([ + { + type: reduxTypes.view.RESET_PAGE, + viewId + }, + { + type: reduxTypes.view.SET_QUERY, + viewId, + filter, + value + } + ]); + }; +}; + +/** + * Display available select options. + * + * @param {object} props + * @param {string} props.filter + * @param {object} props.filterOptions + * @param {string} props.position + * @param {Function} props.t + * @param {Function} props.useOnSelect + * @param {Function} props.useSelector + * @param {string} props.useView + * @returns {React.ReactNode} + */ +const ViewToolbarSelect = ({ + filter, + filterOptions, + position, + t, + useOnSelect: useAliasOnSelect, + useSelector: useAliasSelector, + useView: useAliasView +} = {}) => { + const { viewId } = useAliasView(); + const selectedOption = useAliasSelector(({ view }) => view?.query?.[viewId]?.[filter]); + const onSelect = useAliasOnSelect(filter); + const updatedOptions = filterOptions?.[filter] || []; + + return ( + + ); +}; + +/** + * Prop types + * + * @type {{filter: string, useView: Function, useOnSelect: Function, t: Function, useSelector: Function, position: string, + * filterOptions: object}} + */ +ViewToolbarSelect.propTypes = { + filter: PropTypes.oneOf([...Object.values(SelectFilterVariant)]).isRequired, + filterOptions: PropTypes.shape({ + [API_QUERY_TYPES.CREDENTIAL_TYPE]: PropTypes.array, + [API_QUERY_TYPES.SOURCE_TYPE]: PropTypes.array + }), + position: PropTypes.oneOf([...Object.values(SelectPosition)]), + t: PropTypes.func, + useOnSelect: PropTypes.func, + useSelector: PropTypes.func, + useView: PropTypes.func +}; + +/** + * Default props + * + * @type {{useView: Function, useOnSelect: Function, t: translate, useSelector: Function, position: string, + * filterOptions: {'[API_QUERY_TYPES.CREDENTIAL_TYPE]': Array, '[API_QUERY_TYPES.SOURCE_TYPE]': Array}}} + */ +ViewToolbarSelect.defaultProps = { + filterOptions: SelectFilterVariantOptions, + position: SelectPosition.left, + t: translate, + useOnSelect, + useSelector: storeHooks.reactRedux.useSelector, + useView +}; + +export { + ViewToolbarSelect as default, + ViewToolbarSelect, + SelectFilterVariant, + SelectFilterVariantOptions, + useOnSelect +}; diff --git a/src/components/viewToolbar/viewToolbarSelectCategory.js b/src/components/viewToolbar/viewToolbarSelectCategory.js new file mode 100644 index 00000000..333155a3 --- /dev/null +++ b/src/components/viewToolbar/viewToolbarSelectCategory.js @@ -0,0 +1,102 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useShallowCompareEffect } from 'react-use'; +import { FilterIcon } from '@patternfly/react-icons'; +import { reduxTypes, storeHooks } from '../../redux'; +import { useView } from '../view/viewContext'; +import { DropdownSelect } from '../dropdownSelect/dropdownSelect'; +import { translate } from '../i18n/i18n'; + +/** + * On select update category. + * + * @param {object} options + * @param {Function} options.useDispatch + * @param {Function} options.useView + * @returns {Function} + */ +const useOnSelect = ({ + useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, + useView: useAliasView = useView +} = {}) => { + const { viewId } = useAliasView(); + const dispatch = useAliasDispatch(); + + return ({ value = null } = {}) => { + dispatch([ + { + type: reduxTypes.view.SET_FILTER, + viewId, + currentFilterCategory: value + } + ]); + }; +}; + +/** + * Select available filter categories. + * + * @param {object} props + * @param {Function} props.t + * @param {Function} props.useOnSelect + * @param {Function} props.useSelector + * @param {Function} props.useView + * @returns {React.ReactNode} + */ +const ViewToolbarSelectCategory = ({ + t, + useOnSelect: useAliasOnSelect, + useSelector: useAliasSelector, + useView: useAliasView +}) => { + const { viewId, config } = useAliasView(); + const options = config?.toolbar?.filterFields; + const onSelect = useAliasOnSelect(viewId); + + const selectedOption = useAliasSelector(({ view }) => view?.filters?.[viewId]?.currentFilterCategory); + + useShallowCompareEffect(() => { + const initialCategory = options.find(({ selected }) => selected === true); + + if (!selectedOption && initialCategory?.value) { + onSelect({ value: initialCategory?.value }); + } + }, [options, onSelect, selectedOption]); + + return ( + } + /> + ); +}; + +/** + * Prop types. + * + * @type {{useView: Function, useOnSelect: Function, t: Function, useSelector: Function}} + */ +ViewToolbarSelectCategory.propTypes = { + t: PropTypes.func, + useOnSelect: PropTypes.func, + useSelector: PropTypes.func, + useView: PropTypes.func +}; + +/** + * Default props. + * + * @type {{useView: Function, useOnSelect: Function, t: translate, useSelector: Function}} + */ +ViewToolbarSelectCategory.defaultProps = { + t: translate, + useOnSelect, + useSelector: storeHooks.reactRedux.useSelector, + useView +}; + +export { ViewToolbarSelectCategory as default, ViewToolbarSelectCategory, useOnSelect }; diff --git a/src/components/viewToolbar/viewToolbarTextInput.js b/src/components/viewToolbar/viewToolbarTextInput.js new file mode 100644 index 00000000..9e0b82f8 --- /dev/null +++ b/src/components/viewToolbar/viewToolbarTextInput.js @@ -0,0 +1,194 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { InputGroup } from '@patternfly/react-core'; +import _debounce from 'lodash/debounce'; +import { reduxTypes, storeHooks } from '../../redux'; +import { useView } from '../view/viewContext'; +import { TextInput } from '../form/textInput'; +import { API_QUERY_TYPES } from '../../constants/apiConstants'; +import { translate } from '../i18n/i18n'; + +/** + * Available text input filters + * + * @type {{'[API_QUERY_TYPES.SEARCH_SOURCES_NAME]': string, '[API_QUERY_TYPES.SEARCH_NAME]': string, + * '[API_QUERY_TYPES.SEARCH_CREDENTIALS_NAME]': string}} + */ +const TextInputFilterVariants = { + [API_QUERY_TYPES.SEARCH_CREDENTIALS_NAME]: API_QUERY_TYPES.SEARCH_CREDENTIALS_NAME, + [API_QUERY_TYPES.SEARCH_NAME]: API_QUERY_TYPES.SEARCH_NAME, + [API_QUERY_TYPES.SEARCH_SOURCES_NAME]: API_QUERY_TYPES.SEARCH_SOURCES_NAME +}; + +/** + * On submit input, dispatch type. + * + * @param {string} filter + * @param {object} options + * @param {Function} options.useDispatch + * @param {Function} options.useView + * @returns {Function} + */ +const useOnSubmit = ( + filter, + { useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, useView: useAliasView = useView } = {} +) => { + const { viewId } = useAliasView(); + const dispatch = useAliasDispatch(); + + return ({ value }) => + dispatch([ + { + type: reduxTypes.view.RESET_PAGE, + viewId + }, + { + type: reduxTypes.view.SET_QUERY, + viewId, + filter, + value + } + ]); +}; + +/** + * On clear input, dispatch type. + * + * @param {string} filter + * @param {object} options + * @param {Function} options.useDispatch + * @param {Function} options.useSelector + * @param {Function} options.useView + * @returns {Function} + */ +const useOnClear = ( + filter, + { + useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, + useSelector: useAliasSelector = storeHooks.reactRedux.useSelector, + useView: useAliasView = useView + } = {} +) => { + const { viewId } = useAliasView(); + const currentValue = useAliasSelector(({ view }) => view?.query?.[viewId]?.[filter]); + const dispatch = useAliasDispatch(); + + return () => { + if (currentValue === '' || !currentValue) { + return; + } + + dispatch([ + { + type: reduxTypes.view.RESET_PAGE, + viewId + }, + { + type: reduxTypes.view.SET_QUERY, + viewId, + filter, + value: '' + } + ]); + }; +}; + +/** + * Display an input field for filtering results. + * + * @fires onKeyUp + * @param {object} props + * @param {number} props.debounceTimer + * @param {string} props.filter + * @param {Function} props.t + * @param {Function} props.useOnClear + * @param {Function} props.useOnSubmit + * @param {Function} props.useSelector + * @param {Function} props.useView + * @returns {React.ReactNode} + */ +const ViewToolbarTextInput = ({ + debounceTimer, + filter, + t, + useOnClear: useAliasOnClear, + useOnSubmit: useAliasOnSubmit, + useSelector: useAliasSelector, + useView: useAliasView +}) => { + const { viewId } = useAliasView(); + const currentValue = useAliasSelector(({ view }) => view?.query?.[viewId]?.[filter]); + const onSubmit = useAliasOnSubmit(filter); + const onClear = useAliasOnClear(filter); + + /** + * Set up submit debounce event to allow for bypass. + */ + const debounced = _debounce(onSubmit, debounceTimer); + + /** + * On enter submit value, on type submit value, and on esc ignore (clear value at component level). + * + * @event onKeyUp + * @param {object} event + */ + const onKeyUp = event => { + switch (event.keyCode) { + case 13: + onSubmit(event); + break; + case 27: + break; + default: + debounced(event); + break; + } + }; + + return ( + + + + ); +}; + +/** + * Prop types + * + * @type {{filter: string, useOnSubmit: Function, useView: Function, t: Function, useSelector: Function, debounceTimer: number, + * useOnClear: Function}} + */ +ViewToolbarTextInput.propTypes = { + debounceTimer: PropTypes.number, + filter: PropTypes.oneOf([...Object.values(TextInputFilterVariants)]).isRequired, + t: PropTypes.func, + useOnClear: PropTypes.func, + useOnSubmit: PropTypes.func, + useSelector: PropTypes.func, + useView: PropTypes.func +}; + +/** + * Default props + * + * @type {{useOnSubmit: Function, useView: Function, t: translate, useSelector: Function, debounceTimer: number, + * useOnClear: Function}} + */ +ViewToolbarTextInput.defaultProps = { + debounceTimer: 800, + t: translate, + useOnClear, + useOnSubmit, + useSelector: storeHooks.reactRedux.useSelector, + useView +}; + +export { ViewToolbarTextInput as default, ViewToolbarTextInput, TextInputFilterVariants, useOnClear, useOnSubmit }; diff --git a/src/redux/actions/__tests__/scansActions.test.js b/src/redux/actions/__tests__/scansActions.test.js index a7f47153..5798416d 100644 --- a/src/redux/actions/__tests__/scansActions.test.js +++ b/src/redux/actions/__tests__/scansActions.test.js @@ -1,31 +1,36 @@ import promiseMiddleware from 'redux-promise-middleware'; import { applyMiddleware, combineReducers, legacy_createStore as createStore } from 'redux'; import moxios from 'moxios'; -import { scansReducer, scansEditReducer } from '../../reducers'; +import { multiActionMiddleware } from '../../middleware'; +import { scansReducer, scansEditReducer, sourcesReducer } from '../../reducers'; import { scansActions } from '..'; describe('ScansActions', () => { - const middleware = [promiseMiddleware]; + const middleware = [multiActionMiddleware, promiseMiddleware]; const generateStore = () => createStore( combineReducers({ scans: scansReducer, - scansEdit: scansEditReducer + scansEdit: scansEditReducer, + sources: sourcesReducer }), applyMiddleware(...middleware) ); + const generateDispatch = async dispatcher => { + const store = generateStore(); + return dispatcher(store.dispatch).then(_ => ({ state: store.getState(), value: _ })); + }; + beforeEach(() => { moxios.install(); - moxios.wait(() => { - const request = moxios.requests.mostRecent(); - request.respondWith({ - status: 200, - response: { - test: 'success' - } - }); + moxios.stubRequest(/\/(api).*?/, { + status: 200, + timeout: 1, + response: { + test: 'success' + } }); }); @@ -33,133 +38,81 @@ describe('ScansActions', () => { moxios.uninstall(); }); - it('Should return response content for addScan method', done => { - const store = generateStore(); + it('Should return response content for addScan method', async () => { const dispatcher = scansActions.addScan(); - dispatcher(store.dispatch).then(() => { - const response = store.getState().scansEdit; - - expect(response.add).toEqual(true); - done(); - }); + const { state } = await generateDispatch(dispatcher); + expect(state.scansEdit.add).toEqual(true); }); - it('Should return response content for addStartScan method', done => { - const store = generateStore(); + it('Should return response content for addStartScan method', async () => { const dispatcher = scansActions.addStartScan(); - dispatcher(store.dispatch).then(value => { - expect(value.action.type).toMatchSnapshot('addStartScan'); - done(); - }); + const { value } = await generateDispatch(dispatcher); + expect(value.action.type).toMatchSnapshot('addStartScan'); }); - it('Should return response content for getScans method', done => { - const store = generateStore(); + it('Should return response content for getScans method', async () => { const dispatcher = scansActions.getScans(); - dispatcher(store.dispatch).then(() => { - const response = store.getState().scans; - - expect(response.view.fulfilled).toEqual(true); - done(); - }); + const { state } = await generateDispatch(dispatcher); + expect(state.sources.view.fulfilled).toEqual(true); + expect(state.scans.view.fulfilled).toEqual(true); }); - it('Should return response content for getScanJobs method', done => { - const store = generateStore(); + it('Should return response content for getScanJobs method', async () => { const dispatcher = scansActions.getScanJobs('lorem'); - dispatcher(store.dispatch).then(() => { - const response = store.getState().scans; - - expect(response.jobs.lorem.fulfilled).toEqual(true); - done(); - }); + const { state } = await generateDispatch(dispatcher); + expect(state.scans.jobs.lorem.fulfilled).toEqual(true); }); - it('Should return response content for getScanJob method', done => { - const store = generateStore(); + it('Should return response content for getScanJob method', async () => { const dispatcher = scansActions.getScanJob('lorem'); - dispatcher(store.dispatch).then(() => { - const response = store.getState().scans; - - expect(response.job.lorem.fulfilled).toEqual(true); - done(); - }); + const { state } = await generateDispatch(dispatcher); + expect(state.scans.job.lorem.fulfilled).toEqual(true); }); - it('Should return response content for getConnectionScanResults method', done => { - const store = generateStore(); + it('Should return response content for getConnectionScanResults method', async () => { const dispatcher = scansActions.getConnectionScanResults('lorem'); - dispatcher(store.dispatch).then(() => { - const response = store.getState().scans; - - expect(response.connection.lorem.fulfilled).toEqual(true); - done(); - }); + const { state } = await generateDispatch(dispatcher); + expect(state.scans.connection.lorem.fulfilled).toEqual(true); }); - it('Should return response content for getInspectionScanResults method', done => { - const store = generateStore(); + it('Should return response content for getInspectionScanResults method', async () => { const dispatcher = scansActions.getInspectionScanResults('lorem'); - dispatcher(store.dispatch).then(() => { - const response = store.getState().scans; - - expect(response.inspection.lorem.fulfilled).toEqual(true); - done(); - }); + const { state } = await generateDispatch(dispatcher); + expect(state.scans.inspection.lorem.fulfilled).toEqual(true); }); - it('Should return response content for startScan method', done => { - const store = generateStore(); + it('Should return response content for startScan method', async () => { const dispatcher = scansActions.startScan('lorem'); - dispatcher(store.dispatch).then(() => { - const response = store.getState().scans; - - expect(response.action.lorem.fulfilled).toEqual(true); - done(); - }); + const { state } = await generateDispatch(dispatcher); + expect(state.scans.action.lorem.fulfilled).toEqual(true); }); - it('Should return response content for pauseScan method', done => { - const store = generateStore(); + it('Should return response content for pauseScan method', async () => { const dispatcher = scansActions.pauseScan('lorem'); - dispatcher(store.dispatch).then(() => { - const response = store.getState().scans; - - expect(response.action.lorem.fulfilled).toEqual(true); - done(); - }); + const { state } = await generateDispatch(dispatcher); + expect(state.scans.action.lorem.fulfilled).toEqual(true); }); - it('Should return response content for cancelScan method', done => { - const store = generateStore(); + it('Should return response content for cancelScan method', async () => { const dispatcher = scansActions.cancelScan('lorem'); - dispatcher(store.dispatch).then(() => { - const response = store.getState().scans; - - expect(response.action.lorem.fulfilled).toEqual(true); - done(); - }); + const { state } = await generateDispatch(dispatcher); + expect(state.scans.action.lorem.fulfilled).toEqual(true); }); - it('Should return response content for restartScan method', done => { - const store = generateStore(); + it('Should return response content for restartScan method', async () => { const dispatcher = scansActions.restartScan('lorem'); - dispatcher(store.dispatch).then(() => { - const response = store.getState().scans; - - expect(response.action.lorem.fulfilled).toEqual(true); - done(); - }); + const { state } = await generateDispatch(dispatcher); + expect(state.scans.action.lorem.fulfilled).toEqual(true); }); }); diff --git a/src/redux/actions/__tests__/sourcesActions.test.js b/src/redux/actions/__tests__/sourcesActions.test.js index 85fa8c0b..ec350d33 100644 --- a/src/redux/actions/__tests__/sourcesActions.test.js +++ b/src/redux/actions/__tests__/sourcesActions.test.js @@ -58,18 +58,6 @@ describe('SourcesActions', () => { }); }); - it('Should return response content for getScansSources method', done => { - const store = generateStore(); - const dispatcher = sourcesActions.getScansSources(); - - dispatcher(store.dispatch).then(() => { - const response = store.getState().scans; - - expect(response.empty.fulfilled).toEqual(true); - done(); - }); - }); - it('Should return response content for getSources method', done => { const store = generateStore(); const dispatcher = sourcesActions.getSources(); diff --git a/src/redux/actions/scansActions.js b/src/redux/actions/scansActions.js index 092d32b6..170205a0 100644 --- a/src/redux/actions/scansActions.js +++ b/src/redux/actions/scansActions.js @@ -1,5 +1,5 @@ -import { scansTypes } from '../constants'; -import { scansService } from '../../services'; +import { scansTypes, sourcesTypes } from '../constants'; +import { scansService, sourcesService } from '../../services'; const addScan = data => dispatch => dispatch({ @@ -16,10 +16,18 @@ const addStartScan = id => dispatch => const getScans = (query = {}) => dispatch => - dispatch({ - type: scansTypes.GET_SCANS, - payload: scansService.getScans('', query) - }); + Promise.all( + dispatch([ + { + type: sourcesTypes.GET_SOURCES, + payload: sourcesService.getSources() + }, + { + type: scansTypes.GET_SCANS, + payload: scansService.getScans('', query) + } + ]) + ); const getScanJobs = (id, query = {}) => diff --git a/src/redux/actions/sourcesActions.js b/src/redux/actions/sourcesActions.js index b0d36b29..fa0ddc9d 100644 --- a/src/redux/actions/sourcesActions.js +++ b/src/redux/actions/sourcesActions.js @@ -18,14 +18,6 @@ const deleteSource = id => dispatch => { }); }; -const getScansSources = - (query = {}) => - dispatch => - dispatch({ - type: sourcesTypes.GET_SCANS_SOURCES, - payload: sourcesService.getSources('', query) - }); - const getSources = (query = {}) => dispatch => @@ -43,17 +35,8 @@ const updateSource = (id, data) => dispatch => const sourcesActions = { addSource, deleteSource, - getScansSources, getSources, updateSource }; -export { - sourcesActions as default, - sourcesActions, - addSource, - deleteSource, - getScansSources, - getSources, - updateSource -}; +export { sourcesActions as default, sourcesActions, addSource, deleteSource, getSources, updateSource }; diff --git a/src/redux/constants/__tests__/__snapshots__/index.test.js.snap b/src/redux/constants/__tests__/__snapshots__/index.test.js.snap index e2859847..4c1d4e2b 100644 --- a/src/redux/constants/__tests__/__snapshots__/index.test.js.snap +++ b/src/redux/constants/__tests__/__snapshots__/index.test.js.snap @@ -84,7 +84,6 @@ Object { "DESELECT_SOURCE": "DESELECT_SOURCE", "EDIT_SOURCE_SHOW": "EDIT_SOURCE_SHOW", "EXPANDED_SOURCE": "EXPANDED_SOURCE", - "GET_SCANS_SOURCES": "GET_SCANS_SOURCES", "GET_SOURCES": "GET_SOURCES", "INVALID_SOURCE_WIZARD_STEPTWO": "INVALID_SOURCE_WIZARD_STEPTWO", "NOT_EXPANDED_SOURCE": "NOT_EXPANDED_SOURCE", @@ -112,21 +111,12 @@ Object { }, "view": Object { "CREDENTIALS_VIEW": "CREDENTIALS_VIEW", + "RESET_PAGE": "RESET_PAGE", "SCANS_VIEW": "SCANS_VIEW", + "SET_FILTER": "SET_FILTER", + "SET_QUERY": "SET_QUERY", "SOURCES_VIEW": "SOURCES_VIEW", - }, - "viewPagination": Object { - "SET_PER_PAGE": "SET_PER_PAGE", - "VIEW_PAGE": "VIEW_PAGE", - }, - "viewToolbar": Object { - "ADD_FILTER": "ADD_FILTER", - "CLEAR_FILTERS": "CLEAR_FILTERS", - "REMOVE_FILTER": "REMOVE_FILTER", - "SET_FILTER_TYPE": "SET_FILTER_TYPE", - "SET_FILTER_VALUE": "SET_FILTER_VALUE", - "SET_SORT_TYPE": "SET_SORT_TYPE", - "TOGGLE_SORT_ASCENDING": "TOGGLE_SORT_ASCENDING", + "UPDATE_VIEW": "UPDATE_VIEW", }, }, "factsTypes": Object { @@ -192,7 +182,6 @@ Object { "DESELECT_SOURCE": "DESELECT_SOURCE", "EDIT_SOURCE_SHOW": "EDIT_SOURCE_SHOW", "EXPANDED_SOURCE": "EXPANDED_SOURCE", - "GET_SCANS_SOURCES": "GET_SCANS_SOURCES", "GET_SOURCES": "GET_SOURCES", "INVALID_SOURCE_WIZARD_STEPTWO": "INVALID_SOURCE_WIZARD_STEPTWO", "NOT_EXPANDED_SOURCE": "NOT_EXPANDED_SOURCE", @@ -220,21 +209,12 @@ Object { }, "view": Object { "CREDENTIALS_VIEW": "CREDENTIALS_VIEW", + "RESET_PAGE": "RESET_PAGE", "SCANS_VIEW": "SCANS_VIEW", + "SET_FILTER": "SET_FILTER", + "SET_QUERY": "SET_QUERY", "SOURCES_VIEW": "SOURCES_VIEW", - }, - "viewPagination": Object { - "SET_PER_PAGE": "SET_PER_PAGE", - "VIEW_PAGE": "VIEW_PAGE", - }, - "viewToolbar": Object { - "ADD_FILTER": "ADD_FILTER", - "CLEAR_FILTERS": "CLEAR_FILTERS", - "REMOVE_FILTER": "REMOVE_FILTER", - "SET_FILTER_TYPE": "SET_FILTER_TYPE", - "SET_FILTER_VALUE": "SET_FILTER_VALUE", - "SET_SORT_TYPE": "SET_SORT_TYPE", - "TOGGLE_SORT_ASCENDING": "TOGGLE_SORT_ASCENDING", + "UPDATE_VIEW": "UPDATE_VIEW", }, }, "reportsTypes": Object { @@ -271,7 +251,6 @@ Object { "DESELECT_SOURCE": "DESELECT_SOURCE", "EDIT_SOURCE_SHOW": "EDIT_SOURCE_SHOW", "EXPANDED_SOURCE": "EXPANDED_SOURCE", - "GET_SCANS_SOURCES": "GET_SCANS_SOURCES", "GET_SOURCES": "GET_SOURCES", "INVALID_SOURCE_WIZARD_STEPTWO": "INVALID_SOURCE_WIZARD_STEPTWO", "NOT_EXPANDED_SOURCE": "NOT_EXPANDED_SOURCE", @@ -297,23 +276,14 @@ Object { "USER_LOCALE": "USER_LOCALE", "USER_LOGOUT": "USER_LOGOUT", }, - "viewPaginationTypes": Object { - "SET_PER_PAGE": "SET_PER_PAGE", - "VIEW_PAGE": "VIEW_PAGE", - }, - "viewToolbarTypes": Object { - "ADD_FILTER": "ADD_FILTER", - "CLEAR_FILTERS": "CLEAR_FILTERS", - "REMOVE_FILTER": "REMOVE_FILTER", - "SET_FILTER_TYPE": "SET_FILTER_TYPE", - "SET_FILTER_VALUE": "SET_FILTER_VALUE", - "SET_SORT_TYPE": "SET_SORT_TYPE", - "TOGGLE_SORT_ASCENDING": "TOGGLE_SORT_ASCENDING", - }, "viewTypes": Object { "CREDENTIALS_VIEW": "CREDENTIALS_VIEW", + "RESET_PAGE": "RESET_PAGE", "SCANS_VIEW": "SCANS_VIEW", + "SET_FILTER": "SET_FILTER", + "SET_QUERY": "SET_QUERY", "SOURCES_VIEW": "SOURCES_VIEW", + "UPDATE_VIEW": "UPDATE_VIEW", }, } `; @@ -379,7 +349,6 @@ Object { "DESELECT_SOURCE": "DESELECT_SOURCE", "EDIT_SOURCE_SHOW": "EDIT_SOURCE_SHOW", "EXPANDED_SOURCE": "EXPANDED_SOURCE", - "GET_SCANS_SOURCES": "GET_SCANS_SOURCES", "GET_SOURCES": "GET_SOURCES", "INVALID_SOURCE_WIZARD_STEPTWO": "INVALID_SOURCE_WIZARD_STEPTWO", "NOT_EXPANDED_SOURCE": "NOT_EXPANDED_SOURCE", @@ -407,21 +376,12 @@ Object { }, "view": Object { "CREDENTIALS_VIEW": "CREDENTIALS_VIEW", + "RESET_PAGE": "RESET_PAGE", "SCANS_VIEW": "SCANS_VIEW", + "SET_FILTER": "SET_FILTER", + "SET_QUERY": "SET_QUERY", "SOURCES_VIEW": "SOURCES_VIEW", - }, - "viewPagination": Object { - "SET_PER_PAGE": "SET_PER_PAGE", - "VIEW_PAGE": "VIEW_PAGE", - }, - "viewToolbar": Object { - "ADD_FILTER": "ADD_FILTER", - "CLEAR_FILTERS": "CLEAR_FILTERS", - "REMOVE_FILTER": "REMOVE_FILTER", - "SET_FILTER_TYPE": "SET_FILTER_TYPE", - "SET_FILTER_VALUE": "SET_FILTER_VALUE", - "SET_SORT_TYPE": "SET_SORT_TYPE", - "TOGGLE_SORT_ASCENDING": "TOGGLE_SORT_ASCENDING", + "UPDATE_VIEW": "UPDATE_VIEW", }, } `; diff --git a/src/redux/constants/index.js b/src/redux/constants/index.js index 44c9bd76..bfbcf0d6 100644 --- a/src/redux/constants/index.js +++ b/src/redux/constants/index.js @@ -9,8 +9,6 @@ import { statusTypes } from './statusConstants'; import { toastNotificationTypes } from './toasNotificationConstants'; import { userTypes } from './userConstants'; import { viewTypes } from './viewConstants'; -import { viewPaginationTypes } from './viewPaginationConstants'; -import { viewToolbarTypes } from './viewToolbarConstants'; const reduxTypes = { aboutModal: aboutModalTypes, @@ -23,9 +21,7 @@ const reduxTypes = { status: statusTypes, toastNotifications: toastNotificationTypes, user: userTypes, - view: viewTypes, - viewPagination: viewPaginationTypes, - viewToolbar: viewToolbarTypes + view: viewTypes }; export { @@ -41,7 +37,5 @@ export { statusTypes, toastNotificationTypes, userTypes, - viewTypes, - viewPaginationTypes, - viewToolbarTypes + viewTypes }; diff --git a/src/redux/constants/sourcesConstants.js b/src/redux/constants/sourcesConstants.js index fe0ac98a..653f97d1 100644 --- a/src/redux/constants/sourcesConstants.js +++ b/src/redux/constants/sourcesConstants.js @@ -1,7 +1,6 @@ const ADD_SOURCE = 'ADD_SOURCE'; const DELETE_SOURCE = 'DELETE_SOURCE'; const DELETE_SOURCES = 'DELETE_SOURCES'; -const GET_SCANS_SOURCES = 'GET_SCANS_SOURCES'; const GET_SOURCES = 'GET_SOURCES'; const UPDATE_SOURCES = 'UPDATE_SOURCES'; const UPDATE_SOURCE = 'UPDATE_SOURCE'; @@ -20,7 +19,6 @@ const sourcesTypes = { ADD_SOURCE, DELETE_SOURCE, DELETE_SOURCES, - GET_SCANS_SOURCES, GET_SOURCES, UPDATE_SOURCES, UPDATE_SOURCE, @@ -42,7 +40,6 @@ export { ADD_SOURCE, DELETE_SOURCE, DELETE_SOURCES, - GET_SCANS_SOURCES, GET_SOURCES, UPDATE_SOURCES, UPDATE_SOURCE, diff --git a/src/redux/constants/viewConstants.js b/src/redux/constants/viewConstants.js index 73e8a2f9..976f7ccb 100644 --- a/src/redux/constants/viewConstants.js +++ b/src/redux/constants/viewConstants.js @@ -2,10 +2,29 @@ const SOURCES_VIEW = 'SOURCES_VIEW'; const SCANS_VIEW = 'SCANS_VIEW'; const CREDENTIALS_VIEW = 'CREDENTIALS_VIEW'; +const RESET_PAGE = 'RESET_PAGE'; +const SET_FILTER = 'SET_FILTER'; +const SET_QUERY = 'SET_QUERY'; +const UPDATE_VIEW = 'UPDATE_VIEW'; + const viewTypes = { SOURCES_VIEW, SCANS_VIEW, - CREDENTIALS_VIEW + CREDENTIALS_VIEW, + RESET_PAGE, + SET_FILTER, + SET_QUERY, + UPDATE_VIEW }; -export { viewTypes as default, viewTypes, SOURCES_VIEW, SCANS_VIEW, CREDENTIALS_VIEW }; +export { + viewTypes as default, + viewTypes, + SOURCES_VIEW, + SCANS_VIEW, + CREDENTIALS_VIEW, + RESET_PAGE, + SET_FILTER, + SET_QUERY, + UPDATE_VIEW +}; diff --git a/src/redux/constants/viewPaginationConstants.js b/src/redux/constants/viewPaginationConstants.js deleted file mode 100644 index ce1bd0df..00000000 --- a/src/redux/constants/viewPaginationConstants.js +++ /dev/null @@ -1,9 +0,0 @@ -const VIEW_PAGE = 'VIEW_PAGE'; -const SET_PER_PAGE = 'SET_PER_PAGE'; - -const viewPaginationTypes = { - SET_PER_PAGE, - VIEW_PAGE -}; - -export { viewPaginationTypes as default, viewPaginationTypes, SET_PER_PAGE, VIEW_PAGE }; diff --git a/src/redux/constants/viewToolbarConstants.js b/src/redux/constants/viewToolbarConstants.js deleted file mode 100644 index e7e1cf55..00000000 --- a/src/redux/constants/viewToolbarConstants.js +++ /dev/null @@ -1,29 +0,0 @@ -const SET_FILTER_TYPE = 'SET_FILTER_TYPE'; -const SET_FILTER_VALUE = 'SET_FILTER_VALUE'; -const ADD_FILTER = 'ADD_FILTER'; -const REMOVE_FILTER = 'REMOVE_FILTER'; -const CLEAR_FILTERS = 'CLEAR_FILTERS'; -const SET_SORT_TYPE = 'SET_SORT_TYPE'; -const TOGGLE_SORT_ASCENDING = 'TOGGLE_SORT_ASCENDING'; - -const viewToolbarTypes = { - SET_FILTER_TYPE, - SET_FILTER_VALUE, - ADD_FILTER, - REMOVE_FILTER, - CLEAR_FILTERS, - SET_SORT_TYPE, - TOGGLE_SORT_ASCENDING -}; - -export { - viewToolbarTypes as default, - viewToolbarTypes, - SET_FILTER_TYPE, - SET_FILTER_VALUE, - ADD_FILTER, - REMOVE_FILTER, - CLEAR_FILTERS, - SET_SORT_TYPE, - TOGGLE_SORT_ASCENDING -}; diff --git a/src/redux/reducers/__tests__/__snapshots__/scansReducer.test.js.snap b/src/redux/reducers/__tests__/__snapshots__/scansReducer.test.js.snap index 0d821fe5..a9fab011 100644 --- a/src/redux/reducers/__tests__/__snapshots__/scansReducer.test.js.snap +++ b/src/redux/reducers/__tests__/__snapshots__/scansReducer.test.js.snap @@ -15,7 +15,6 @@ Object { "update": false, }, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -48,7 +47,6 @@ Object { "pending": false, "update": false, }, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -71,7 +69,6 @@ Object { "result": Object { "action": Object {}, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object { "error": true, @@ -104,7 +101,6 @@ Object { "result": Object { "action": Object {}, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object { @@ -137,7 +133,6 @@ Object { "result": Object { "action": Object {}, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -170,7 +165,6 @@ Object { "result": Object { "action": Object {}, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -198,39 +192,6 @@ Object { } `; -exports[`ScansReducer should handle all defined error types: rejected types GET_SCANS_SOURCES 1`] = ` -Object { - "result": Object { - "action": Object {}, - "connection": Object {}, - "empty": Object { - "error": true, - "errorMessage": "ERROR", - "errorStatus": 0, - "fulfilled": false, - "metaData": undefined, - "metaId": undefined, - "metaQuery": undefined, - "pending": false, - "update": false, - }, - "expanded": Object {}, - "inspection": Object {}, - "job": Object {}, - "jobs": Object {}, - "mergeDialog": Object { - "details": false, - "scans": Array [], - "show": false, - }, - "selected": Object {}, - "update": 0, - "view": Object {}, - }, - "type": "GET_SCANS_SOURCES_REJECTED", -} -`; - exports[`ScansReducer should handle all defined error types: rejected types PAUSE_SCAN 1`] = ` Object { "result": Object { @@ -246,7 +207,6 @@ Object { "update": false, }, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -279,7 +239,6 @@ Object { "update": false, }, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -312,7 +271,6 @@ Object { "update": false, }, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -348,7 +306,6 @@ Object { "update": false, }, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -384,7 +341,6 @@ Object { "pending": false, "update": false, }, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -407,7 +363,6 @@ Object { "result": Object { "action": Object {}, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object { "data": Object { @@ -443,7 +398,6 @@ Object { "result": Object { "action": Object {}, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object { @@ -479,7 +433,6 @@ Object { "result": Object { "action": Object {}, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -515,7 +468,6 @@ Object { "result": Object { "action": Object {}, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -546,42 +498,6 @@ Object { } `; -exports[`ScansReducer should handle all defined fulfilled types: fulfilled types GET_SCANS_SOURCES 1`] = ` -Object { - "result": Object { - "action": Object {}, - "connection": Object {}, - "empty": Object { - "data": Object { - "test": "success", - }, - "date": undefined, - "error": false, - "errorMessage": "", - "fulfilled": true, - "metaData": undefined, - "metaId": undefined, - "metaQuery": undefined, - "pending": false, - "update": false, - }, - "expanded": Object {}, - "inspection": Object {}, - "job": Object {}, - "jobs": Object {}, - "mergeDialog": Object { - "details": false, - "scans": Array [], - "show": false, - }, - "selected": Object {}, - "update": 0, - "view": Object {}, - }, - "type": "GET_SCANS_SOURCES_FULFILLED", -} -`; - exports[`ScansReducer should handle all defined fulfilled types: fulfilled types PAUSE_SCAN 1`] = ` Object { "result": Object { @@ -600,7 +516,6 @@ Object { "update": false, }, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -636,7 +551,6 @@ Object { "update": false, }, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -672,7 +586,6 @@ Object { "update": false, }, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -704,7 +617,6 @@ Object { "update": false, }, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -736,7 +648,6 @@ Object { "pending": true, "update": false, }, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -759,7 +670,6 @@ Object { "result": Object { "action": Object {}, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object { "error": false, @@ -791,7 +701,6 @@ Object { "result": Object { "action": Object {}, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object { @@ -823,7 +732,6 @@ Object { "result": Object { "action": Object {}, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -855,7 +763,6 @@ Object { "result": Object { "action": Object {}, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -882,38 +789,6 @@ Object { } `; -exports[`ScansReducer should handle all defined pending types: pending types GET_SCANS_SOURCES 1`] = ` -Object { - "result": Object { - "action": Object {}, - "connection": Object {}, - "empty": Object { - "error": false, - "errorMessage": "", - "fulfilled": false, - "metaData": undefined, - "metaId": undefined, - "metaQuery": undefined, - "pending": true, - "update": false, - }, - "expanded": Object {}, - "inspection": Object {}, - "job": Object {}, - "jobs": Object {}, - "mergeDialog": Object { - "details": false, - "scans": Array [], - "show": false, - }, - "selected": Object {}, - "update": 0, - "view": Object {}, - }, - "type": "GET_SCANS_SOURCES_PENDING", -} -`; - exports[`ScansReducer should handle all defined pending types: pending types PAUSE_SCAN 1`] = ` Object { "result": Object { @@ -928,7 +803,6 @@ Object { "update": false, }, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -960,7 +834,6 @@ Object { "update": false, }, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -992,7 +865,6 @@ Object { "update": false, }, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -1015,7 +887,6 @@ Object { "result": Object { "action": Object {}, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -1040,7 +911,6 @@ Object { "result": Object { "action": Object {}, "connection": Object {}, - "empty": Object {}, "expanded": Object { "undefined": undefined, }, @@ -1065,7 +935,6 @@ Object { "result": Object { "action": Object {}, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -1088,7 +957,6 @@ Object { "result": Object { "action": Object {}, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -1111,7 +979,6 @@ Object { "result": Object { "action": Object {}, "connection": Object {}, - "empty": Object {}, "expanded": Object { "undefined": null, }, @@ -1136,7 +1003,6 @@ Object { "result": Object { "action": Object {}, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, @@ -1161,7 +1027,6 @@ Object { "result": Object { "action": Object {}, "connection": Object {}, - "empty": Object {}, "expanded": Object {}, "inspection": Object {}, "job": Object {}, diff --git a/src/redux/reducers/__tests__/__snapshots__/viewOptionsReducer.test.js.snap b/src/redux/reducers/__tests__/__snapshots__/viewOptionsReducer.test.js.snap deleted file mode 100644 index f69a9d1c..00000000 --- a/src/redux/reducers/__tests__/__snapshots__/viewOptionsReducer.test.js.snap +++ /dev/null @@ -1,603 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`viewOptionsReducer should handle all defined fulfilled types: fulfilled types GET_CREDENTIALS 1`] = ` -Object { - "result": Object { - "CREDENTIALS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": undefined, - "totalPages": NaN, - }, - "SCANS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SOURCES_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - }, - "type": "GET_CREDENTIALS_FULFILLED", -} -`; - -exports[`viewOptionsReducer should handle all defined fulfilled types: fulfilled types GET_SCANS 1`] = ` -Object { - "result": Object { - "CREDENTIALS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SCANS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": undefined, - "totalPages": NaN, - }, - "SOURCES_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - }, - "type": "GET_SCANS_FULFILLED", -} -`; - -exports[`viewOptionsReducer should handle all defined fulfilled types: fulfilled types GET_SOURCES 1`] = ` -Object { - "result": Object { - "CREDENTIALS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SCANS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SOURCES_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": undefined, - "totalPages": NaN, - }, - }, - "type": "GET_SOURCES_FULFILLED", -} -`; - -exports[`viewOptionsReducer should handle specific defined types: defined type ADD_FILTER 1`] = ` -Object { - "result": Object { - "CREDENTIALS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SCANS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SOURCES_VIEW": Object { - "activeFilters": Array [ - undefined, - ], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - }, - "type": "ADD_FILTER", -} -`; - -exports[`viewOptionsReducer should handle specific defined types: defined type CLEAR_FILTERS 1`] = ` -Object { - "result": Object { - "CREDENTIALS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SCANS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SOURCES_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - }, - "type": "CLEAR_FILTERS", -} -`; - -exports[`viewOptionsReducer should handle specific defined types: defined type REMOVE_FILTER 1`] = ` -Object { - "result": Object { - "CREDENTIALS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SCANS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SOURCES_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - }, - "type": "REMOVE_FILTER", -} -`; - -exports[`viewOptionsReducer should handle specific defined types: defined type SET_FILTER_TYPE 1`] = ` -Object { - "result": Object { - "CREDENTIALS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SCANS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SOURCES_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": undefined, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - }, - "type": "SET_FILTER_TYPE", -} -`; - -exports[`viewOptionsReducer should handle specific defined types: defined type SET_FILTER_VALUE 1`] = ` -Object { - "result": Object { - "CREDENTIALS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SCANS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SOURCES_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": undefined, - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - }, - "type": "SET_FILTER_VALUE", -} -`; - -exports[`viewOptionsReducer should handle specific defined types: defined type SET_PER_PAGE 1`] = ` -Object { - "result": Object { - "CREDENTIALS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SCANS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SOURCES_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": undefined, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - }, - "type": "SET_PER_PAGE", -} -`; - -exports[`viewOptionsReducer should handle specific defined types: defined type SET_SORT_TYPE 1`] = ` -Object { - "result": Object { - "CREDENTIALS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SCANS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SOURCES_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": undefined, - "sortType": undefined, - "totalCount": 0, - "totalPages": 0, - }, - }, - "type": "SET_SORT_TYPE", -} -`; - -exports[`viewOptionsReducer should handle specific defined types: defined type TOGGLE_SORT_ASCENDING 1`] = ` -Object { - "result": Object { - "CREDENTIALS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SCANS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SOURCES_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": false, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - }, - "type": "TOGGLE_SORT_ASCENDING", -} -`; - -exports[`viewOptionsReducer should handle specific defined types: defined type VIEW_PAGE 1`] = ` -Object { - "result": Object { - "CREDENTIALS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SCANS_VIEW": Object { - "activeFilters": Array [], - "currentPage": 1, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - "SOURCES_VIEW": Object { - "activeFilters": Array [], - "currentPage": undefined, - "expandedItems": Array [], - "filterType": null, - "filterValue": "", - "pageSize": 10, - "selectedItems": Array [], - "sortAscending": true, - "sortField": "name", - "sortType": null, - "totalCount": 0, - "totalPages": 0, - }, - }, - "type": "VIEW_PAGE", -} -`; diff --git a/src/redux/reducers/__tests__/__snapshots__/viewReducer.test.js.snap b/src/redux/reducers/__tests__/__snapshots__/viewReducer.test.js.snap new file mode 100644 index 00000000..fbc83738 --- /dev/null +++ b/src/redux/reducers/__tests__/__snapshots__/viewReducer.test.js.snap @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`viewReducer should handle specific defined types: defined type RESET_PAGE 1`] = ` +Object { + "result": Object { + "filters": Object {}, + "query": Object { + "test_id": Object { + "page": 1, + }, + }, + "update": Object {}, + }, + "type": "RESET_PAGE", +} +`; + +exports[`viewReducer should handle specific defined types: defined type SET_FILTER 1`] = ` +Object { + "result": Object { + "filters": Object { + "test_id": Object { + "currentFilterCategory": undefined, + }, + }, + "query": Object {}, + "update": Object {}, + }, + "type": "SET_FILTER", +} +`; + +exports[`viewReducer should handle specific defined types: defined type SET_QUERY 1`] = ` +Object { + "result": Object { + "filters": Object {}, + "query": Object { + "test_id": Object { + "undefined": undefined, + }, + }, + "update": Object {}, + }, + "type": "SET_QUERY", +} +`; + +exports[`viewReducer should handle specific defined types: defined type UPDATE_VIEW 1`] = ` +Object { + "result": Object { + "filters": Object {}, + "query": Object {}, + "update": Object { + "test_id": 1654041600000, + }, + }, + "type": "UPDATE_VIEW", +} +`; diff --git a/src/redux/reducers/__tests__/scansReducer.test.js b/src/redux/reducers/__tests__/scansReducer.test.js index 51495c2a..b6b168f1 100644 --- a/src/redux/reducers/__tests__/scansReducer.test.js +++ b/src/redux/reducers/__tests__/scansReducer.test.js @@ -1,5 +1,5 @@ import { scansReducer } from '../scansReducer'; -import { scansTypes as types, sourcesTypes } from '../../constants'; +import { scansTypes as types } from '../../constants'; import { reduxHelpers } from '../../common'; describe('ScansReducer', () => { @@ -31,7 +31,6 @@ describe('ScansReducer', () => { it('should handle all defined error types', () => { const specificTypes = [ - sourcesTypes.GET_SCANS_SOURCES, types.GET_SCAN_CONNECTION_RESULTS, types.GET_SCAN_INSPECTION_RESULTS, types.GET_SCAN_JOB, @@ -69,7 +68,6 @@ describe('ScansReducer', () => { it('should handle all defined pending types', () => { const specificTypes = [ - sourcesTypes.GET_SCANS_SOURCES, types.GET_SCAN_CONNECTION_RESULTS, types.GET_SCAN_INSPECTION_RESULTS, types.GET_SCAN_JOB, @@ -96,7 +94,6 @@ describe('ScansReducer', () => { it('should handle all defined fulfilled types', () => { const specificTypes = [ - sourcesTypes.GET_SCANS_SOURCES, types.GET_SCAN_CONNECTION_RESULTS, types.GET_SCAN_INSPECTION_RESULTS, types.GET_SCAN_JOB, diff --git a/src/redux/reducers/__tests__/viewOptionsReducer.test.js b/src/redux/reducers/__tests__/viewOptionsReducer.test.js deleted file mode 100644 index 2543e552..00000000 --- a/src/redux/reducers/__tests__/viewOptionsReducer.test.js +++ /dev/null @@ -1,62 +0,0 @@ -import viewOptionsReducer from '../viewOptionsReducer'; -import { - viewTypes, - viewPaginationTypes, - viewToolbarTypes, - credentialsTypes, - scansTypes, - sourcesTypes -} from '../../constants'; -import { reduxHelpers } from '../../common/reduxHelpers'; - -describe('viewOptionsReducer', () => { - it('should return the initial state', () => { - expect(viewOptionsReducer.initialState).toBeDefined(); - }); - - it('should handle specific defined types', () => { - const specificTypes = [ - viewToolbarTypes.SET_FILTER_TYPE, - viewToolbarTypes.SET_FILTER_VALUE, - viewToolbarTypes.ADD_FILTER, - viewToolbarTypes.REMOVE_FILTER, - viewToolbarTypes.CLEAR_FILTERS, - viewToolbarTypes.SET_SORT_TYPE, - viewToolbarTypes.TOGGLE_SORT_ASCENDING, - viewPaginationTypes.VIEW_PAGE, - viewPaginationTypes.SET_PER_PAGE - ]; - - specificTypes.forEach(value => { - const dispatched = { - type: value, - viewType: viewTypes.SOURCES_VIEW - }; - - const resultState = viewOptionsReducer(undefined, dispatched); - - expect({ type: value, result: resultState }).toMatchSnapshot(`defined type ${value}`); - }); - }); - - it('should handle all defined fulfilled types', () => { - const specificTypes = [credentialsTypes.GET_CREDENTIALS, sourcesTypes.GET_SOURCES, scansTypes.GET_SCANS]; - - specificTypes.forEach(value => { - const dispatched = { - type: reduxHelpers.FULFILLED_ACTION(value), - payload: { - data: { - test: 'success' - } - } - }; - - const resultState = viewOptionsReducer(undefined, dispatched); - - expect({ type: reduxHelpers.FULFILLED_ACTION(value), result: resultState }).toMatchSnapshot( - `fulfilled types ${value}` - ); - }); - }); -}); diff --git a/src/redux/reducers/__tests__/viewReducer.test.js b/src/redux/reducers/__tests__/viewReducer.test.js new file mode 100644 index 00000000..52c587f5 --- /dev/null +++ b/src/redux/reducers/__tests__/viewReducer.test.js @@ -0,0 +1,23 @@ +import viewReducer from '../viewReducer'; +import { viewTypes as types } from '../../constants'; + +describe('viewReducer', () => { + it('should return the initial state', () => { + expect(viewReducer.initialState).toBeDefined(); + }); + + it('should handle specific defined types', () => { + const specificTypes = [types.SET_FILTER, types.UPDATE_VIEW, types.SET_QUERY, types.RESET_PAGE]; + + specificTypes.forEach(value => { + const dispatched = { + type: value, + viewId: 'test_id' + }; + + const resultState = viewReducer(undefined, dispatched); + + expect({ type: value, result: resultState }).toMatchSnapshot(`defined type ${value}`); + }); + }); +}); diff --git a/src/redux/reducers/index.js b/src/redux/reducers/index.js index b34d8983..06e4a017 100644 --- a/src/redux/reducers/index.js +++ b/src/redux/reducers/index.js @@ -1,5 +1,4 @@ import { combineReducers } from 'redux'; - import aboutModalReducer from './aboutModalReducer'; import addSourceWizardReducer from './addSourceWizardReducer'; import confirmationModalReducer from './confirmationModalReducer'; @@ -12,7 +11,7 @@ import sourcesReducer from './sourcesReducer'; import statusReducer from './statusReducer'; import toastNotificationsReducer from './toastNotificationsReducer'; import userReducer from './userReducer'; -import viewOptionsReducer from './viewOptionsReducer'; +import viewReducer from './viewReducer'; const reducers = { aboutModal: aboutModalReducer, @@ -27,7 +26,7 @@ const reducers = { status: statusReducer, toastNotifications: toastNotificationsReducer, user: userReducer, - viewOptions: viewOptionsReducer + view: viewReducer }; const reduxReducers = combineReducers(reducers); @@ -47,5 +46,5 @@ export { statusReducer, toastNotificationsReducer, userReducer, - viewOptionsReducer + viewReducer }; diff --git a/src/redux/reducers/scansReducer.js b/src/redux/reducers/scansReducer.js index 3b4cad1f..28f2b5f2 100644 --- a/src/redux/reducers/scansReducer.js +++ b/src/redux/reducers/scansReducer.js @@ -1,4 +1,4 @@ -import { scansTypes, sourcesTypes } from '../constants'; +import { scansTypes } from '../constants'; import { reduxHelpers } from '../common'; import { helpers } from '../../common'; @@ -8,7 +8,6 @@ const initialState = { scans: [], details: false }, - empty: {}, connection: {}, inspection: {}, job: {}, @@ -114,7 +113,6 @@ const scansReducer = (state = initialState, action) => { default: return reduxHelpers.generatedPromiseActionReducer( [ - { ref: 'empty', type: sourcesTypes.GET_SCANS_SOURCES }, { ref: 'connection', type: scansTypes.GET_SCAN_CONNECTION_RESULTS }, { ref: 'inspection', type: scansTypes.GET_SCAN_INSPECTION_RESULTS }, { ref: 'job', type: scansTypes.GET_SCAN_JOB }, diff --git a/src/redux/reducers/viewOptionsReducer.js b/src/redux/reducers/viewOptionsReducer.js deleted file mode 100644 index 95f57202..00000000 --- a/src/redux/reducers/viewOptionsReducer.js +++ /dev/null @@ -1,153 +0,0 @@ -import _get from 'lodash/get'; -import { reduxHelpers } from '../common'; -import { - viewTypes, - viewPaginationTypes, - viewToolbarTypes, - credentialsTypes, - scansTypes, - sourcesTypes -} from '../constants'; -import { apiTypes } from '../../constants/apiConstants'; - -const initialState = {}; - -const INITAL_VIEW_STATE = { - currentPage: 1, - pageSize: 10, - totalCount: 0, - totalPages: 0, - filterType: null, - filterValue: '', - activeFilters: [], - sortType: null, - sortField: 'name', - sortAscending: true, - selectedItems: [], - expandedItems: [] -}; - -initialState[viewTypes.SOURCES_VIEW] = Object.assign(INITAL_VIEW_STATE); -initialState[viewTypes.SCANS_VIEW] = Object.assign(INITAL_VIEW_STATE); -initialState[viewTypes.CREDENTIALS_VIEW] = Object.assign(INITAL_VIEW_STATE); - -const viewOptionsReducer = (state = initialState, action) => { - const updateState = {}; - - const updatePageCounts = (viewType, itemsCount) => { - let totalCount = itemsCount; - - // TODO: Remove this when we get decent data back in development mode - if (process.env.NODE_ENV === 'development') { - totalCount = Math.abs(itemsCount) % 1000; - } - - const totalPages = Math.ceil(totalCount / state[viewType].pageSize); - - updateState[viewType] = { - ...state[viewType], - totalCount, - totalPages, - currentPage: Math.min(state[viewType].currentPage, totalPages || 1) - }; - }; - - switch (action.type) { - case viewToolbarTypes.SET_FILTER_TYPE: - if (state[action.viewType].filterType === action.filterType) { - return state; - } - - updateState[action.viewType] = { ...state[action.viewType], filterType: action.filterType, filterValue: '' }; - return { ...state, ...updateState }; - - case viewToolbarTypes.SET_FILTER_VALUE: - updateState[action.viewType] = { ...state[action.viewType], filterValue: action.filterValue }; - return { ...state, ...updateState }; - - case viewToolbarTypes.ADD_FILTER: - const index = state[action.viewType].activeFilters.findIndex( - filter => filter?.field?.id === action.filter?.field?.id - ); - const updatedFilters = [...state[action.viewType].activeFilters]; - - if (index < 0) { - updatedFilters.push(action.filter); - } else { - updatedFilters[index] = action.filter; - } - - updateState[action.viewType] = { - ...state[action.viewType], - activeFilters: updatedFilters, - currentPage: 1 - }; - - return { ...state, ...updateState }; - - case viewToolbarTypes.REMOVE_FILTER: - const remainingFilters = state[action.viewType].activeFilters.filter( - filter => filter?.field?.id !== action.filter?.field?.id - ); - updateState[action.viewType] = { - ...state[action.viewType], - activeFilters: remainingFilters, - currentPage: 1 - }; - return { ...state, ...updateState }; - - case viewToolbarTypes.CLEAR_FILTERS: - updateState[action.viewType] = { ...state[action.viewType], activeFilters: [], currentPage: 1 }; - return { ...state, ...updateState }; - - case viewToolbarTypes.SET_SORT_TYPE: - if (state[action.viewType].sortType === action.sortType) { - return state; - } - - updateState[action.viewType] = { - ...state[action.viewType], - sortType: action.sortType, - sortField: action.sortType && action.sortType.id, - sortAscending: _get(action, 'sortType.sortAscending', true), - currentPage: 1 - }; - - return { ...state, ...updateState }; - - case viewToolbarTypes.TOGGLE_SORT_ASCENDING: - updateState[action.viewType] = { - ...state[action.viewType], - sortAscending: !state[action.viewType].sortAscending, - currentPage: 1 - }; - return { ...state, ...updateState }; - - case viewPaginationTypes.VIEW_PAGE: - updateState[action.viewType] = { ...state[action.viewType], currentPage: action.currentPage }; - return { ...state, ...updateState }; - - case viewPaginationTypes.SET_PER_PAGE: - updateState[action.viewType] = { ...state[action.viewType], pageSize: action.pageSize, currentPage: 1 }; - return { ...state, ...updateState }; - - case reduxHelpers.FULFILLED_ACTION(credentialsTypes.GET_CREDENTIALS): - updatePageCounts(viewTypes.CREDENTIALS_VIEW, action.payload.data[apiTypes.API_RESPONSE_CREDENTIALS_COUNT]); - return { ...state, ...updateState }; - - case reduxHelpers.FULFILLED_ACTION(sourcesTypes.GET_SOURCES): - updatePageCounts(viewTypes.SOURCES_VIEW, action.payload.data[apiTypes.API_RESPONSE_SOURCES_COUNT]); - return { ...state, ...updateState }; - - case reduxHelpers.FULFILLED_ACTION(scansTypes.GET_SCANS): - updatePageCounts(viewTypes.SCANS_VIEW, action.payload.data[apiTypes.API_RESPONSE_SCANS_COUNT]); - return { ...state, ...updateState }; - - default: - return state; - } -}; - -viewOptionsReducer.initialState = initialState; - -export { viewOptionsReducer as default, initialState, viewOptionsReducer }; diff --git a/src/redux/reducers/viewReducer.js b/src/redux/reducers/viewReducer.js new file mode 100644 index 00000000..6ea3a630 --- /dev/null +++ b/src/redux/reducers/viewReducer.js @@ -0,0 +1,88 @@ +import { reduxHelpers } from '../common'; +import { reduxTypes } from '../constants'; +import { API_QUERY_TYPES } from '../../constants/apiConstants'; +import { helpers } from '../../common'; + +/** + * Initial state. + */ +const initialState = { + filters: {}, + query: {}, + update: {} +}; + +/** + * Apply user observer/reducer logic for views to state, against actions. + * + * @param {object} state + * @param {object} action + * @returns {object|{}} + */ +const viewReducer = (state = initialState, action) => { + switch (action.type) { + case reduxTypes.view.SET_FILTER: + return reduxHelpers.setStateProp( + 'filters', + { + [action.viewId]: { + ...state.filters[action.viewId], + currentFilterCategory: action.currentFilterCategory + } + }, + { + state, + reset: false + } + ); + + case reduxTypes.view.UPDATE_VIEW: + return reduxHelpers.setStateProp( + 'update', + { + [action.viewId]: helpers.getCurrentDate().getTime() + }, + { + state, + reset: false + } + ); + + case reduxTypes.view.SET_QUERY: + return reduxHelpers.setStateProp( + 'query', + { + [action.viewId]: { + ...state.query[action.viewId], + [action.filter]: action.value + } + }, + { + state, + reset: false + } + ); + + case reduxTypes.view.RESET_PAGE: + return reduxHelpers.setStateProp( + 'query', + { + [action.viewId]: { + ...state.query[action.viewId], + [API_QUERY_TYPES.PAGE]: 1 + } + }, + { + state, + reset: false + } + ); + + default: + return state; + } +}; + +viewReducer.initialState = initialState; + +export { viewReducer as default, initialState, viewReducer }; diff --git a/src/setupTests.js b/src/setupTests.js index f6738cf4..9bfcc557 100644 --- a/src/setupTests.js +++ b/src/setupTests.js @@ -18,6 +18,11 @@ jest.mock('i18next', () => { return new Test(); }); +/** + * Emulate for component checks + */ +jest.mock('lodash/debounce', () => jest.fn); + /** * We currently use a wrapper for useSelector, emulate for component checks */ diff --git a/tests/__snapshots__/code.test.js.snap b/tests/__snapshots__/code.test.js.snap index a2ccb3b7..9954f395 100644 --- a/tests/__snapshots__/code.test.js.snap +++ b/tests/__snapshots__/code.test.js.snap @@ -5,6 +5,6 @@ Array [ "common/helpers.js:63: console.warn('Copy to clipboard failed.', e.message);", "redux/common/reduxHelpers.js:15: console.error(\`Error: Property \${prop} does not exist within the passed state.\`, state);", "redux/common/reduxHelpers.js:19: console.warn(\`Warning: Property \${prop} does not exist within the passed initialState.\`, initialState);", - "setupTests.js:159: console.error = (message, ...args) => {", + "setupTests.js:164: console.error = (message, ...args) => {", ] `; diff --git a/tests/__snapshots__/dist.test.js.snap b/tests/__snapshots__/dist.test.js.snap index 9597ff92..e4f7684e 100644 --- a/tests/__snapshots__/dist.test.js.snap +++ b/tests/__snapshots__/dist.test.js.snap @@ -650,6 +650,12 @@ Array [ "./dist/client/static/css/45*chunk.css", "./dist/client/static/css/46*chunk*map", "./dist/client/static/css/46*chunk.css", + "./dist/client/static/css/47*chunk*map", + "./dist/client/static/css/47*chunk.css", + "./dist/client/static/css/48*chunk*map", + "./dist/client/static/css/48*chunk.css", + "./dist/client/static/css/49*chunk*map", + "./dist/client/static/css/49*chunk.css", "./dist/client/static/css/5*chunk*map", "./dist/client/static/css/5*chunk.css", "./dist/client/static/css/6*chunk*map", @@ -743,6 +749,12 @@ Array [ "./dist/client/static/js/45*chunk.js", "./dist/client/static/js/46*chunk*map", "./dist/client/static/js/46*chunk.js", + "./dist/client/static/js/47*chunk*map", + "./dist/client/static/js/47*chunk.js", + "./dist/client/static/js/48*chunk*map", + "./dist/client/static/js/48*chunk.js", + "./dist/client/static/js/49*chunk*map", + "./dist/client/static/js/49*chunk.js", "./dist/client/static/js/5*chunk*map", "./dist/client/static/js/5*chunk.js", "./dist/client/static/js/6*chunk*map",