From edd63e2e8f915a3bcc762ad45703af2585702147 Mon Sep 17 00:00:00 2001 From: Aleksandr Voznesenskii Date: Mon, 20 Feb 2023 15:02:26 +0100 Subject: [PATCH 01/12] feat(invntory groups): added new group column behind feature flag --- .../InventoryTable/hooks/useColumns.js | 16 +- src/store/entities.js | 290 ++++++++++++++---- 2 files changed, 246 insertions(+), 60 deletions(-) diff --git a/src/components/InventoryTable/hooks/useColumns.js b/src/components/InventoryTable/hooks/useColumns.js index 612d8d12c..62fa6bd9d 100644 --- a/src/components/InventoryTable/hooks/useColumns.js +++ b/src/components/InventoryTable/hooks/useColumns.js @@ -1,13 +1,16 @@ import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { mergeArraysByKey } from '@redhat-cloud-services/frontend-components-utilities/helpers/helpers'; -import { defaultColumns } from '../../../store/entities'; +import { defaultColumns, newDefaultColumns } from '../../../store/entities'; +import useFeatureFlag from '../../../Utilities/useFeatureFlag'; const isColumnEnabled = (key, disableColumns, showTags) => (key === 'tags' && showTags) || (key !== 'tags' && (Array.isArray(disableColumns) && !(disableColumns).includes(key))); const useColumns = (columnsProp, disableDefaultColumns, showTags, columnsCounter) => { + const groupsColumnEnabled = useFeatureFlag('hbi.ui.inventory-groups'); + const columnsRedux = useSelector( ({ entities: { columns } }) => columns, (next, prev) => next.every( @@ -15,10 +18,15 @@ const useColumns = (columnsProp, disableDefaultColumns, showTags, columnsCounter ) ); const disabledColumns = Array.isArray(disableDefaultColumns) ? disableDefaultColumns : []; + //condition for the newDefaultColumns should be removed after inventory groups is released const defaultColumnsFiltered = useMemo(() => (disableDefaultColumns === true) ? - [] : defaultColumns().filter(({ key }) => - isColumnEnabled(key, disabledColumns, showTags) - ), [disabledColumns, disableDefaultColumns, showTags]); + [] : groupsColumnEnabled ? + newDefaultColumns().filter(({ key }) => + isColumnEnabled(key, disabledColumns, showTags) + ) : + defaultColumns().filter(({ key }) => + isColumnEnabled(key, disabledColumns, showTags) + ), [disabledColumns, disableDefaultColumns, showTags]); return useMemo(() => { if (typeof columnsProp === 'function') { diff --git a/src/store/entities.js b/src/store/entities.js index 554751b1d..3ce3acd13 100644 --- a/src/store/entities.js +++ b/src/store/entities.js @@ -35,7 +35,98 @@ export const defaultState = { } }; -export const defaultColumns = () => ([ +export const newDefaultColumns = () => [ + { + key: 'display_name', + sortKey: 'display_name', + title: 'Name', + renderFunc: TitleColumn + }, + { + key: 'groups', + sortKey: 'groups', + title: 'Groups', + props: { width: 10 }, + renderFunc: () => + + N/A + + }, + { + key: 'tags', + title: 'Tags', + props: { width: 10, isStatic: true }, + // eslint-disable-next-line react/display-name + renderFunc: (value, systemId) => ( + + ) + }, + { + key: 'system_profile', + sortKey: 'operating_system', + dataLabel: 'OS', + title: ( + Operating system}> + OS + + ), + // eslint-disable-next-line react/display-name + renderFunc: (systemProfile) => ( + + ), + props: { width: 10 } + }, + { + key: 'updated', + sortKey: 'updated', + title: 'Last seen', + // eslint-disable-next-line react/display-name + renderFunc: ( + value, + _id, + { + culled_timestamp: culled, + stale_warning_timestamp: staleWarn, + stale_timestamp: stale, + per_reporter_staleness: perReporterStaleness + } + ) => { + return CullingInformation ? ( + ( + + +
{msg}
+ Last seen:{` `} +
+ } + /> + {verifyCulledInsightsClient(perReporterStaleness) && ( + + )} + + )} + > + {' '} + {' '} +
+ ) : ( + new Date(value).toLocaleString() + ); + }, + props: { width: 10 } + } +]; + +export const defaultColumns = () => [ { key: 'display_name', sortKey: 'display_name', @@ -47,15 +138,25 @@ export const defaultColumns = () => ([ title: 'Tags', props: { width: 10, isStatic: true }, // eslint-disable-next-line react/display-name - renderFunc: (value, systemId) => + renderFunc: (value, systemId) => ( + + ) }, { key: 'system_profile', sortKey: 'operating_system', dataLabel: 'OS', - title: Operating system}>OS, + title: ( + Operating system}> + OS + + ), // eslint-disable-next-line react/display-name - renderFunc: (systemProfile) => , + renderFunc: (systemProfile) => ( + + ), props: { width: 10 } }, { @@ -67,36 +168,60 @@ export const defaultColumns = () => ([ value, _id, { - culled_timestamp: culled, stale_warning_timestamp: staleWarn, stale_timestamp: stale, + culled_timestamp: culled, + stale_warning_timestamp: staleWarn, + stale_timestamp: stale, per_reporter_staleness: perReporterStaleness - }) => { - return CullingInformation ? - { + return CullingInformation ? ( + ( -
{ msg }
- Last seen:{` `} + +
{msg}
+ Last seen:{` `} +
+ } + /> + {verifyCulledInsightsClient(perReporterStaleness) && ( + + )}
- ) }/> - {verifyCulledInsightsClient(perReporterStaleness) && } - - } - >
: new Date(value).toLocaleString(); + )} + > + {' '} + {' '} + + ) : ( + new Date(value).toLocaleString() + ); }, props: { width: 10 } } -]); +]; function entitiesPending(state, { meta }) { return { ...state, - ...state.columns && { columns: mergeArraysByKey([ - defaultColumns().filter(({ key }) => key !== 'tags' || meta?.showTags), - state.columns - ], 'key') } || {}, + ...((state.columns && { + columns: mergeArraysByKey( + [ + defaultColumns().filter( + ({ key }) => key !== 'tags' || meta?.showTags + ), + state.columns + ], + 'key' + ) + }) || + {}), rows: [], loaded: false, lastDateRequest: meta.lastDateRequest @@ -111,7 +236,21 @@ function clearFilters(state) { } // eslint-disable-next-line camelcase -function entitiesLoaded(state, { payload: { results, per_page: perPage, page, count, total, loaded, filters }, meta }) { +function entitiesLoaded( + state, + { + payload: { + results, + per_page: perPage, + page, + count, + total, + loaded, + filters + }, + meta + } +) { // Older requests should not rewrite the state if (meta.lastDateRequest < state.lastDateRequest) { return state; @@ -127,7 +266,9 @@ function entitiesLoaded(state, { payload: { results, per_page: perPage, page, co activeFilters: filters || [], loaded: loaded === undefined || loaded, // filter data only if we are loaded - rows: mergeArraysByKey([state.rows, results]).filter(item => !loaded ? true : item.created), + rows: mergeArraysByKey([state.rows, results]).filter((item) => + !loaded ? true : item.created + ), perPage: perPage !== undefined ? perPage : state.perPage, page: page !== undefined ? page : state.page, count: count !== undefined ? count : state.count, @@ -146,11 +287,11 @@ function selectEntity(state, { payload }) { const rows = [...state.rows]; const toSelect = [].concat(payload); toSelect.forEach(({ id, selected }) => { - const entity = rows.find(entity => entity.id === id); + const entity = rows.find((entity) => entity.id === id); if (entity) { entity.selected = selected; } else { - rows.forEach(item => item.selected = selected); + rows.forEach((item) => (item.selected = selected)); } }); return { @@ -162,10 +303,13 @@ function selectEntity(state, { payload }) { function versionsLoaded(state, { payload: { results } }) { return { ...state, - operatingSystems: results.map(entry => { + operatingSystems: results.map((entry) => { const { name, major, minor } = entry.value; const versionStringified = `${major}.${minor}`; - return { label: `${name} ${versionStringified}`, value: versionStringified }; + return { + label: `${name} ${versionStringified}`, + value: versionStringified + }; }), operatingSystemsLoaded: true }; @@ -181,21 +325,33 @@ function changeSort(state, { payload: { key, direction } }) { }; } -function selectFilter(state, { payload: { item: { items, ...item }, selected } }) { +function selectFilter( + state, + { + payload: { + item: { items, ...item }, + selected + } + } +) { let { activeFilters = [] } = state; if (selected) { - activeFilters = [ - ...activeFilters, - item, - ...items ? items : [] - ]; - const values = activeFilters.map(active => active.value); - activeFilters = activeFilters.filter((filter, key) => values.lastIndexOf(filter.value) === key); + activeFilters = [...activeFilters, item, ...(items ? items : [])]; + const values = activeFilters.map((active) => active.value); + activeFilters = activeFilters.filter( + (filter, key) => values.lastIndexOf(filter.value) === key + ); } else { - activeFilters.splice(activeFilters.map(active => active.value).indexOf(item.value), 1); + activeFilters.splice( + activeFilters.map((active) => active.value).indexOf(item.value), + 1 + ); if (items) { - items.forEach(subItem => { - activeFilters.splice(activeFilters.map(active => active.value).indexOf(subItem.value), 1); + items.forEach((subItem) => { + activeFilters.splice( + activeFilters.map((active) => active.value).indexOf(subItem.value), + 1 + ); }); } } @@ -207,7 +363,9 @@ function selectFilter(state, { payload: { item: { items, ...item }, selected } } } export function showTags(state, { payload, meta }) { - const { tags, ...activeSystemTag } = state.rows ? state.rows.find(({ id }) => meta.systemId === id) : state.entity || {}; + const { tags, ...activeSystemTag } = state.rows + ? state.rows.find(({ id }) => meta.systemId === id) + : state.entity || {}; return { ...state, tagModalLoaded: true, @@ -223,7 +381,9 @@ export function showTags(state, { payload, meta }) { } export function showTagsPending(state, { meta }) { - const { tags, ...activeSystemTag } = state.rows ? state.rows.find(({ id }) => meta.systemId === id) : state.entity || {}; + const { tags, ...activeSystemTag } = state.rows + ? state.rows.find(({ id }) => meta.systemId === id) + : state.entity || {}; return { ...state, tagModalLoaded: false, @@ -243,7 +403,13 @@ export function toggleTagModalReducer(state, { payload: { isOpen } }) { }; } -export function allTags(state, { payload: { results, total, page, per_page: perPage }, meta: { lastDateRequestTags } }) { +export function allTags( + state, + { + payload: { results, total, page, per_page: perPage }, + meta: { lastDateRequestTags } + } +) { // only the latest request can change state if (lastDateRequestTags < state.lastDateRequestTags) { return state; @@ -251,7 +417,9 @@ export function allTags(state, { payload: { results, total, page, per_page: perP return { ...state, - allTags: Object.entries(groupBy(results, ({ tag: { namespace } }) => namespace)).map(([key, value]) => ({ + allTags: Object.entries( + groupBy(results, ({ tag: { namespace } }) => namespace) + ).map(([key, value]) => ({ name: key, tags: value })), @@ -268,29 +436,39 @@ export function allTags(state, { payload: { results, total, page, per_page: perP export default { [ACTION_TYPES.ALL_TAGS_FULFILLED]: allTags, - [ACTION_TYPES.ALL_TAGS_PENDING]: (state, { meta }) => ( - { ...state, allTagsLoaded: false, tagModalLoaded: false, lastDateRequestTags: meta.lastDateRequestTags } - ), + [ACTION_TYPES.ALL_TAGS_PENDING]: (state, { meta }) => ({ + ...state, + allTagsLoaded: false, + tagModalLoaded: false, + lastDateRequestTags: meta.lastDateRequestTags + }), [ACTION_TYPES.LOAD_ENTITIES_PENDING]: entitiesPending, [ACTION_TYPES.LOAD_ENTITIES_FULFILLED]: entitiesLoaded, [ACTION_TYPES.LOAD_ENTITIES_REJECTED]: loadingRejected, [ACTION_TYPES.LOAD_TAGS_PENDING]: showTagsPending, [ACTION_TYPES.LOAD_TAGS_FULFILLED]: showTags, [ACTION_TYPES.ALL_TAGS_REJECTED]: loadingRejected, - [ACTION_TYPES.OPERATING_SYSTEMS_PENDING]: (state) => ({ ...state, operatingSystemsLoaded: false }), + [ACTION_TYPES.OPERATING_SYSTEMS_PENDING]: (state) => ({ + ...state, + operatingSystemsLoaded: false + }), [ACTION_TYPES.OPERATING_SYSTEMS_FULFILLED]: versionsLoaded, [UPDATE_ENTITIES]: entitiesLoaded, - [SHOW_ENTITIES]: (state, action) => entitiesLoaded(state, { - payload: { - ...action.payload, - loaded: false - } - }), + [SHOW_ENTITIES]: (state, action) => + entitiesLoaded(state, { + payload: { + ...action.payload, + loaded: false + } + }), [FILTER_SELECT]: selectFilter, [SELECT_ENTITY]: selectEntity, [CHANGE_SORT]: changeSort, [CLEAR_FILTERS]: clearFilters, - [ENTITIES_LOADING]: (state, { payload: { isLoading } }) => ({ ...state, loaded: !isLoading }), + [ENTITIES_LOADING]: (state, { payload: { isLoading } }) => ({ + ...state, + loaded: !isLoading + }), [TOGGLE_TAG_MODAL]: toggleTagModalReducer, [CONFIG_CHANGED]: (state, { payload }) => ({ ...state, invConfig: payload }) }; From 367040b0c7ed0d364601907f8a50019a12cb25eb Mon Sep 17 00:00:00 2001 From: Aleksandr Voznesenskii Date: Tue, 28 Feb 2023 14:20:21 +0100 Subject: [PATCH 02/12] feat(inventory groups): groups filter and chips --- src/Utilities/constants.js | 1 + .../InventoryTable/EntityTableToolbar.js | 34 +++-- src/components/filters/index.js | 1 + src/components/filters/useGroupFilter.js | 117 ++++++++++++++++++ src/store/entities.js | 1 + 5 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 src/components/filters/useGroupFilter.js diff --git a/src/Utilities/constants.js b/src/Utilities/constants.js index ed6147e0f..246afd26c 100644 --- a/src/Utilities/constants.js +++ b/src/Utilities/constants.js @@ -10,6 +10,7 @@ export const REGISTERED_CHIP = 'registered_with'; export const OS_CHIP = 'operating_system'; export const RHCD_FILTER_KEY = 'rhc_client_id'; export const UPDATE_METHOD_KEY = 'system_update_method'; +export const HOST_GROUP_CHIP = 'host_group'; export const staleness = [ { label: 'Fresh', value: 'fresh' }, diff --git a/src/components/InventoryTable/EntityTableToolbar.js b/src/components/InventoryTable/EntityTableToolbar.js index 95f6b486f..9ed58f610 100644 --- a/src/components/InventoryTable/EntityTableToolbar.js +++ b/src/components/InventoryTable/EntityTableToolbar.js @@ -19,7 +19,8 @@ import { TAG_CHIP, arrayToSelection, RHCD_FILTER_KEY, - UPDATE_METHOD_KEY + UPDATE_METHOD_KEY, + HOST_GROUP_CHIP } from '../../Utilities/index'; import { onDeleteFilter, onDeleteTag } from './helpers'; import { @@ -40,10 +41,12 @@ import { rhcdFilterReducer, rhcdFilterState, updateMethodFilterReducer, - updateMethodFilterState + updateMethodFilterState, + groupFilterReducer } from '../filters'; import useOperatingSystemFilter from '../filters/useOperatingSystemFilter'; import useFeatureFlag from '../../Utilities/useFeatureFlag'; +import useGroupFilter from '../filters/useGroupFilter'; /** * Table toolbar used at top of inventory table. @@ -81,7 +84,8 @@ const EntityTableToolbar = ({ tagsFilterReducer, operatingSystemFilterReducer, rhcdFilterReducer, - updateMethodFilterReducer + updateMethodFilterReducer, + groupFilterReducer ]), { ...textFilterState, ...stalenessFilterState, @@ -100,6 +104,7 @@ const EntityTableToolbar = ({ const [rhcdFilterConfig, rhcdFilterChips, rhcdFilterValue, setRhcdFilterValue] = useRhcdFilter(reducer); const [osFilterConfig, osFilterChips, osFilterValue, setOsFilterValue] = useOperatingSystemFilter(); const [updateMethodConfig, updateMethodChips, updateMethodValue, setUpdateMethodValue] = useUpdateMethodFilter(reducer); + const [hostGroupChips, hostGroupConfig, hostGroupValue, setHostGroupValue] = useGroupFilter(); const isUpdateMethodEnabled = useFeatureFlag('hbi.ui.system-update-method'); @@ -118,7 +123,7 @@ const EntityTableToolbar = ({ const debounceGetAllTags = useCallback(debounce((config, options) => { if (showTags && !hasItems && hasAccess) { dispatch(fetchAllTags(config, { - ...options?.pagination + ...options?.paginationhideFilters }, getTags)); } }, 800), [customFilters?.tags]); @@ -133,7 +138,8 @@ const EntityTableToolbar = ({ //hides the filter untill API is ready. JIRA: RHIF-169 updateMethodFilter: isUpdateMethodEnabled && !(hideFilters.all && hideFilters.updateMethodFilter !== false) - && !hideFilters.updateMethodFilter + && !hideFilters.updateMethodFilter, + hostGroupFilter: true }; /** @@ -171,7 +177,7 @@ const EntityTableToolbar = ({ */ useEffect(() => { const { - textFilter, tagFilters, staleFilter, registeredWithFilter, osFilter, rhcdFilter, updateMethodFilter + textFilter, tagFilters, staleFilter, registeredWithFilter, osFilter, rhcdFilter, updateMethodFilter, groupFilter } = reduceFilters([...filters || [], ...customFilters?.filters || []]); debouncedRefresh(); @@ -182,6 +188,7 @@ const EntityTableToolbar = ({ enabledFilters.operatingSystem && setOsFilterValue(osFilter); enabledFilters.rhcdFilter && setRhcdFilterValue(rhcdFilter); enabledFilters.updateMethodFilter && setUpdateMethodValue(updateMethodFilter); + enabledFilters.hostGroupFilter && setHostGroupValue(groupFilter); }, []); /** @@ -267,6 +274,12 @@ const EntityTableToolbar = ({ } }, [updateMethodValue]); + useEffect(() => { + if (shouldReload && enabledFilters.hostGroupFilter) { + onSetFilter(hostGroupValue, 'hostGroupFilter', debouncedRefresh); + } + }, [hostGroupValue]); + /** * Mapper to simplify removing of any filter. */ @@ -285,7 +298,8 @@ const EntityTableToolbar = ({ ), [OS_CHIP]: (deleted) => setOsFilterValue(xor(osFilterValue, deleted.chips.map(({ value }) => value))), [RHCD_FILTER_KEY]: (deleted) => setRhcdFilterValue(onDeleteFilter(deleted, rhcdFilterValue)), - [UPDATE_METHOD_KEY]: (deleted) => setUpdateMethodValue(onDeleteFilter(deleted, updateMethodValue)) + [UPDATE_METHOD_KEY]: (deleted) => setUpdateMethodValue(onDeleteFilter(deleted, updateMethodValue)), + [HOST_GROUP_CHIP]: (deleted) => setHostGroupValue(hostGroupValue, deleted.chips.map(({ value }) => value)) }; /** * Function to reset all filters with 'Reset Filter' is clicked @@ -298,6 +312,7 @@ const EntityTableToolbar = ({ enabledFilters.operatingSystem && setOsFilterValue([]); enabledFilters.rhcdFilter && setRhcdFilterValue([]); enabledFilters.updateMethodFilter && setUpdateMethodValue([]); + enabledFilters.hostGroupFilter && setHostGroupValue([]); dispatch(setFilter([])); updateData({ page: 1, filters: [] }); }; @@ -316,6 +331,7 @@ const EntityTableToolbar = ({ ...!hasItems && enabledFilters.operatingSystem ? osFilterChips : [], ...!hasItems && enabledFilters.rhcdFilter ? rhcdFilterChips : [], ...!hasItems && enabledFilters.updateMethodFilter ? updateMethodChips : [], + ...!hasItems && enabledFilters.hostGroupFilter ? hostGroupChips : [], ...activeFiltersConfig?.filters || [] ], onDelete: (e, [deleted, ...restDeleted], isAll) => { @@ -341,7 +357,8 @@ const EntityTableToolbar = ({ ...enabledFilters.registeredWith ? [registeredFilter] : [], ...enabledFilters.rhcdFilter ? [rhcdFilterConfig] : [], ...enabledFilters.updateMethodFilter ? [updateMethodConfig] : [], - ...showTags && enabledFilters.tags ? [tagsFilter] : [] + ...showTags && enabledFilters.tags ? [tagsFilter] : [], + ...enabledFilters && enabledFilters.hostGroupFilter ? [hostGroupConfig] : [] ] : [], ...filterConfig?.items || [] ]; @@ -431,6 +448,7 @@ EntityTableToolbar.propTypes = { operatingSystem: PropTypes.bool, rhcdFilter: PropTypes.bool, updateMethodFilter: PropTypes.bool, + hostGroupFilter: PropTypes.bool, all: PropTypes.bool }), paginationProps: PropTypes.object, diff --git a/src/components/filters/index.js b/src/components/filters/index.js index 4c9fae5db..bf205739f 100644 --- a/src/components/filters/index.js +++ b/src/components/filters/index.js @@ -5,6 +5,7 @@ export * from './useTagsFilter'; export * from './useOperatingSystemFilter'; export * from './useRhcdFilter'; export * from './useUpdateMethodFilter'; +export * from './useGroupFilter'; export const filtersReducer = (reducersList) => (state, action) => reducersList.reduce((acc, curr) => ({ ...acc, ...curr?.(state, action) diff --git a/src/components/filters/useGroupFilter.js b/src/components/filters/useGroupFilter.js new file mode 100644 index 000000000..6618706c5 --- /dev/null +++ b/src/components/filters/useGroupFilter.js @@ -0,0 +1,117 @@ +/* eslint-disable camelcase */ +import { useEffect, useMemo, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { fetchGroups } from '../../store/inventory-actions'; +import { HOST_GROUP_CHIP } from '../../Utilities/index'; + +/* export const onHostGroupsChange = (event, selection) => { + adding chips doesn't work yet + const newSelection = Object.assign({}, selection); + return newSelection; +}; */ + +export const GROUP_FILTER = 'GROUP_FILTER'; +export const groupFilterReducer = (_state, { type, payload }) => ({ + ...type === GROUP_FILTER && { + groupFilter: payload + } +}); + +const mockApiResponse = [ + { + created_at: '2023-02-28T12:35:20.071Z', + host_ids: [ + 'bA6deCFc19564430AB814bf8F70E8cEf' + ], + id: 'bA6deCFc19564430AB814bf8F70E8cEf', + name: 'sre-group0', + org_id: '000102', + updated_at: '2023-02-28T12:35:20.071Z' + }, + { + created_at: '2023-02-28T12:35:20.071Z', + host_ids: [ + 'bA6deCFc19564430AB814bf8F70E8cEf' + ], + id: 'bA6deCFc19564430AB814bf8F70E8cEf', + name: 'sre-group1', + org_id: '000102', + updated_at: '2023-02-28T12:35:20.071Z' + }, + { + created_at: '2023-02-28T12:35:20.071Z', + host_ids: [ + 'bA6deCFc19564430AB814bf8F70E8cEf' + ], + id: 'bA6deCFc19564430AB814bf8F70E8cEf', + name: 'sre-group2', + org_id: '000102', + updated_at: '2023-02-28T12:35:20.071Z' + }, + { + created_at: '2023-02-28T12:35:20.071Z', + host_ids: [ + 'bA6deCFc19564430AB814bf8F70E8cEf' + ], + id: 'bA6deCFc19564430AB814bf8F70E8cEf', + name: 'sre-group3', + org_id: '000102', + updated_at: '2023-02-28T12:35:20.071Z' + } +]; + +//receive the array of selected groups and return chips based on the name of selected groups +export const buildHostGroupChips = (selectedGroups = []) => { + const chips = selectedGroups?.filter((value) => value); + return chips?.length > 0 + ? [ + { + category: 'Group', + type: HOST_GROUP_CHIP, + chips + } + ] + : []; +}; + +const useGroupFilter = (apiParams = []) => { + //we need to load all groups and then show their names + const [selected, setSelected] = useState([]); + const [hostGroupValue, setHostGroupValue] = useState([]); + + const dispatch = useDispatch(); + const hostGroupsLoaded = useSelector(({ entities }) => entities?.groups); + // we need to extract names from hostGroupsLoaded and populate the filter with them + + useEffect(() => { + setHostGroupValue(hostGroupsLoaded); + }, []); + + const buildHostGroupsValues = mockApiResponse.reduce((acc, group) => { + acc.push({ label: group.name, value: group.name }); + return acc; + }, []); + + const chips = useMemo(() => buildHostGroupChips(selected), [selected, hostGroupsLoaded]); + + useEffect(() => { + dispatch(fetchGroups(apiParams)); + }, []); + + const hostGroupConfig = useMemo(() => ({ + label: 'Group', + value: 'host-groups-filter', + type: 'checkbox', + filterValues: { + onChange: (event, value) => { + setSelected(value); + }, + selected, + items: buildHostGroupsValues + } + }), [selected, hostGroupValue]); + + return [chips, hostGroupConfig, hostGroupValue, setHostGroupValue]; +}; + +export default useGroupFilter; diff --git a/src/store/entities.js b/src/store/entities.js index 3ce3acd13..087ab00e9 100644 --- a/src/store/entities.js +++ b/src/store/entities.js @@ -28,6 +28,7 @@ export const defaultState = { allTagsLoaded: false, operatingSystems: [], operatingSystemsLoaded: false, + groups: [], invConfig: {}, sortBy: { key: 'updated', From 75ac4ab4f84473e5f13deb437b7037292285749a Mon Sep 17 00:00:00 2001 From: Aleksandr Voznesenskii Date: Wed, 1 Mar 2023 16:19:02 +0100 Subject: [PATCH 03/12] feat(inventory groups): working on the filter chips --- src/components/filters/useGroupFilter.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/components/filters/useGroupFilter.js b/src/components/filters/useGroupFilter.js index 6618706c5..4f2cef7c3 100644 --- a/src/components/filters/useGroupFilter.js +++ b/src/components/filters/useGroupFilter.js @@ -4,11 +4,10 @@ import { useDispatch, useSelector } from 'react-redux'; import { fetchGroups } from '../../store/inventory-actions'; import { HOST_GROUP_CHIP } from '../../Utilities/index'; -/* export const onHostGroupsChange = (event, selection) => { - adding chips doesn't work yet - const newSelection = Object.assign({}, selection); +export const onHostGroupsChange = (event, selection) => { + const newSelection = [...selection]; return newSelection; -}; */ +}; export const GROUP_FILTER = 'GROUP_FILTER'; export const groupFilterReducer = (_state, { type, payload }) => ({ @@ -62,7 +61,7 @@ const mockApiResponse = [ //receive the array of selected groups and return chips based on the name of selected groups export const buildHostGroupChips = (selectedGroups = []) => { - const chips = selectedGroups?.filter((value) => value); + const chips = selectedGroups?.map((group) => ({ name: group, value: group })); return chips?.length > 0 ? [ { @@ -81,8 +80,6 @@ const useGroupFilter = (apiParams = []) => { const dispatch = useDispatch(); const hostGroupsLoaded = useSelector(({ entities }) => entities?.groups); - // we need to extract names from hostGroupsLoaded and populate the filter with them - useEffect(() => { setHostGroupValue(hostGroupsLoaded); }, []); @@ -103,8 +100,8 @@ const useGroupFilter = (apiParams = []) => { value: 'host-groups-filter', type: 'checkbox', filterValues: { - onChange: (event, value) => { - setSelected(value); + onChange: (event, value, clickedGroup) => { + setSelected(onHostGroupsChange(event, value, clickedGroup)); }, selected, items: buildHostGroupsValues From 8b40d51e971cefaba9439d5121899b5cd56c2c05 Mon Sep 17 00:00:00 2001 From: Aleksandr Voznesenskii Date: Thu, 2 Mar 2023 10:51:06 +0100 Subject: [PATCH 04/12] feat(inventory groups): fixing the chips remove --- src/components/filters/useGroupFilter.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/filters/useGroupFilter.js b/src/components/filters/useGroupFilter.js index 4f2cef7c3..05802ebc7 100644 --- a/src/components/filters/useGroupFilter.js +++ b/src/components/filters/useGroupFilter.js @@ -100,15 +100,15 @@ const useGroupFilter = (apiParams = []) => { value: 'host-groups-filter', type: 'checkbox', filterValues: { - onChange: (event, value, clickedGroup) => { - setSelected(onHostGroupsChange(event, value, clickedGroup)); + onChange: (event, value) => { + setSelected(onHostGroupsChange(event, value)); }, selected, items: buildHostGroupsValues } }), [selected, hostGroupValue]); - return [chips, hostGroupConfig, hostGroupValue, setHostGroupValue]; + return [chips, hostGroupConfig, selected, setHostGroupValue]; }; export default useGroupFilter; From 90f04685c7267e719967c953b8807c6db88e4805 Mon Sep 17 00:00:00 2001 From: Aleksandr Voznesenskii Date: Mon, 6 Mar 2023 23:42:31 +0100 Subject: [PATCH 05/12] feat(inventory groups): updated the group filter --- src/Utilities/constants.js | 13 +++++- src/api/api.js | 3 +- .../InventoryTable/EntityTableToolbar.js | 13 +++--- src/components/filters/useGroupFilter.js | 45 ++++++++++--------- src/constants.js | 16 ++++++- src/routes/InventoryTable.js | 19 ++++++-- src/store/entities.js | 2 +- src/store/inventory-actions.js | 6 ++- 8 files changed, 80 insertions(+), 37 deletions(-) diff --git a/src/Utilities/constants.js b/src/Utilities/constants.js index 246afd26c..56624f503 100644 --- a/src/Utilities/constants.js +++ b/src/Utilities/constants.js @@ -106,7 +106,15 @@ export const reloadWrapper = (event, callback) => { export const isEmpty = (check) => !check || check?.length === 0; -export const generateFilter = (status, source, tagsFilter, filterbyName, operatingSystem, rhcdFilter, updateMethodFilter) => ([ +export const generateFilter = (status, + source, + tagsFilter, + filterbyName, + operatingSystem, + rhcdFilter, + updateMethodFilter, + hostGroup +) => ([ !isEmpty(status) && { staleFilter: Array.isArray(status) ? status : [status] }, @@ -134,6 +142,9 @@ export const generateFilter = (status, source, tagsFilter, filterbyName, operati }, !isEmpty(updateMethodFilter) && { updateMethodFilter: Array.isArray(updateMethodFilter) ? updateMethodFilter : [updateMethodFilter] + }, + !isEmpty(hostGroup) && { + hostGroupFilter: Array.isArray(hostGroup) ? hostGroup : [hostGroup] } ].filter(Boolean)); diff --git a/src/api/api.js b/src/api/api.js index 222db6b94..09a457a47 100644 --- a/src/api/api.js +++ b/src/api/api.js @@ -98,7 +98,8 @@ export const filtersReducer = (acc, filter = {}) => ({ ...'registeredWithFilter' in filter && { registeredWithFilter: filter.registeredWithFilter }, ...'osFilter' in filter && { osFilter: filter.osFilter }, ...'rhcdFilter' in filter && { rhcdFilter: filter.rhcdFilter }, - ...'updateMethodFilter' in filter && { updateMethodFilter: filter.updateMethodFilter } + ...'updateMethodFilter' in filter && { updateMethodFilter: filter.updateMethodFilter }, + ...'groupHostFilter' in filter && { groupHostFilter: filter.groupHostFilter } }); export async function getEntities(items, { diff --git a/src/components/InventoryTable/EntityTableToolbar.js b/src/components/InventoryTable/EntityTableToolbar.js index 9ed58f610..c94cee13f 100644 --- a/src/components/InventoryTable/EntityTableToolbar.js +++ b/src/components/InventoryTable/EntityTableToolbar.js @@ -107,6 +107,7 @@ const EntityTableToolbar = ({ const [hostGroupChips, hostGroupConfig, hostGroupValue, setHostGroupValue] = useGroupFilter(); const isUpdateMethodEnabled = useFeatureFlag('hbi.ui.system-update-method'); + const groupsEnabled = useFeatureFlag('hbi.ui.inventory-groups'); const { tagsFilter, @@ -139,7 +140,7 @@ const EntityTableToolbar = ({ updateMethodFilter: isUpdateMethodEnabled && !(hideFilters.all && hideFilters.updateMethodFilter !== false) && !hideFilters.updateMethodFilter, - hostGroupFilter: true + hostGroupFilter: groupsEnabled }; /** @@ -188,7 +189,7 @@ const EntityTableToolbar = ({ enabledFilters.operatingSystem && setOsFilterValue(osFilter); enabledFilters.rhcdFilter && setRhcdFilterValue(rhcdFilter); enabledFilters.updateMethodFilter && setUpdateMethodValue(updateMethodFilter); - enabledFilters.hostGroupFilter && setHostGroupValue(groupFilter); + groupsEnabled && setHostGroupValue(groupFilter); }, []); /** @@ -275,7 +276,7 @@ const EntityTableToolbar = ({ }, [updateMethodValue]); useEffect(() => { - if (shouldReload && enabledFilters.hostGroupFilter) { + if (shouldReload && groupsEnabled) { onSetFilter(hostGroupValue, 'hostGroupFilter', debouncedRefresh); } }, [hostGroupValue]); @@ -312,7 +313,7 @@ const EntityTableToolbar = ({ enabledFilters.operatingSystem && setOsFilterValue([]); enabledFilters.rhcdFilter && setRhcdFilterValue([]); enabledFilters.updateMethodFilter && setUpdateMethodValue([]); - enabledFilters.hostGroupFilter && setHostGroupValue([]); + groupsEnabled && setHostGroupValue([]); dispatch(setFilter([])); updateData({ page: 1, filters: [] }); }; @@ -331,7 +332,7 @@ const EntityTableToolbar = ({ ...!hasItems && enabledFilters.operatingSystem ? osFilterChips : [], ...!hasItems && enabledFilters.rhcdFilter ? rhcdFilterChips : [], ...!hasItems && enabledFilters.updateMethodFilter ? updateMethodChips : [], - ...!hasItems && enabledFilters.hostGroupFilter ? hostGroupChips : [], + ...!hasItems && groupsEnabled ? hostGroupChips : [], ...activeFiltersConfig?.filters || [] ], onDelete: (e, [deleted, ...restDeleted], isAll) => { @@ -358,7 +359,7 @@ const EntityTableToolbar = ({ ...enabledFilters.rhcdFilter ? [rhcdFilterConfig] : [], ...enabledFilters.updateMethodFilter ? [updateMethodConfig] : [], ...showTags && enabledFilters.tags ? [tagsFilter] : [], - ...enabledFilters && enabledFilters.hostGroupFilter ? [hostGroupConfig] : [] + ...enabledFilters && groupsEnabled ? [hostGroupConfig] : [] ] : [], ...filterConfig?.items || [] ]; diff --git a/src/components/filters/useGroupFilter.js b/src/components/filters/useGroupFilter.js index 05802ebc7..b5610924c 100644 --- a/src/components/filters/useGroupFilter.js +++ b/src/components/filters/useGroupFilter.js @@ -4,11 +4,8 @@ import { useDispatch, useSelector } from 'react-redux'; import { fetchGroups } from '../../store/inventory-actions'; import { HOST_GROUP_CHIP } from '../../Utilities/index'; -export const onHostGroupsChange = (event, selection) => { - const newSelection = [...selection]; - return newSelection; -}; - +//for attaching this filter to the redux +export const groupFilterState = { groupHostFilter: null }; export const GROUP_FILTER = 'GROUP_FILTER'; export const groupFilterReducer = (_state, { type, payload }) => ({ ...type === GROUP_FILTER && { @@ -61,7 +58,9 @@ const mockApiResponse = [ //receive the array of selected groups and return chips based on the name of selected groups export const buildHostGroupChips = (selectedGroups = []) => { - const chips = selectedGroups?.map((group) => ({ name: group, value: group })); + //we use new Set to make sure that chips are unique + const uniqueGroups = [...new Set(selectedGroups)]; + const chips = uniqueGroups?.map((group) => ({ name: group, value: group })); return chips?.length > 0 ? [ { @@ -74,34 +73,40 @@ export const buildHostGroupChips = (selectedGroups = []) => { }; const useGroupFilter = (apiParams = []) => { - //we need to load all groups and then show their names - const [selected, setSelected] = useState([]); - const [hostGroupValue, setHostGroupValue] = useState([]); - - const dispatch = useDispatch(); - const hostGroupsLoaded = useSelector(({ entities }) => entities?.groups); - useEffect(() => { - setHostGroupValue(hostGroupsLoaded); - }, []); - + //currently mockApiResponse is replacing a proper API response + //buildHostGroupsValues build an array of objects to populate dropdown const buildHostGroupsValues = mockApiResponse.reduce((acc, group) => { acc.push({ label: group.name, value: group.name }); return acc; }, []); + //selected are the groups we selected + const [selected, setSelected] = useState([]); + //host group values are all of the host group values available to the user + const [hostGroupValue, setHostGroupValue] = useState(buildHostGroupsValues); - const chips = useMemo(() => buildHostGroupChips(selected), [selected, hostGroupsLoaded]); + const onHostGroupsChange = (event, selection) => { + return setSelected(selected => [...selected, selection].flat(1)); + }; + const hostGroupsLoaded = useSelector(({ entities }) => entities?.groups); useEffect(() => { - dispatch(fetchGroups(apiParams)); + setHostGroupValue(hostGroupsLoaded); }, []); + const chips = useMemo(() => buildHostGroupChips(selected), [selected]); + /* + const dispatch = useDispatch(); + useEffect(() => { + dispatch(fetchGroups(apiParams)); + }, []); */ + //hostGroupConfig is a config that we use in EntityTableToolbar.js const hostGroupConfig = useMemo(() => ({ label: 'Group', - value: 'host-groups-filter', + value: 'group-host-filter', type: 'checkbox', filterValues: { onChange: (event, value) => { - setSelected(onHostGroupsChange(event, value)); + onHostGroupsChange(event, value); }, selected, items: buildHostGroupsValues diff --git a/src/constants.js b/src/constants.js index 3970ee0a5..347269765 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import { RHCD_FILTER_KEY, UPDATE_METHOD_KEY } from './Utilities/constants'; +import { RHCD_FILTER_KEY, UPDATE_METHOD_KEY, HOST_GROUP_CHIP } from './Utilities/constants'; export const tagsMapper = (acc, curr) => { let [namespace, keyValue] = curr.split('/'); @@ -123,9 +123,21 @@ export const getSearchParams = () => { const operatingSystem = searchParams.getAll('operating_system'); const rhcdFilter = searchParams.getAll(RHCD_FILTER_KEY); const updateMethodFilter = searchParams.getAll(UPDATE_METHOD_KEY); + const groupHostsFilter = searchParams.getAll(HOST_GROUP_CHIP); const page = searchParams.getAll('page'); const perPage = searchParams.getAll('per_page'); - return { status, source, tagsFilter, filterbyName, operatingSystem, rhcdFilter, updateMethodFilter, page, perPage }; + return { + status, + source, + tagsFilter, + filterbyName, + operatingSystem, + rhcdFilter, + updateMethodFilter, + groupHostsFilter, + page, + perPage + }; }; export const TABLE_DEFAULT_PAGINATION = 50; // from UX table audit diff --git a/src/routes/InventoryTable.js b/src/routes/InventoryTable.js index 5f6f2e3ba..c9fbd38e2 100644 --- a/src/routes/InventoryTable.js +++ b/src/routes/InventoryTable.js @@ -49,7 +49,9 @@ const filterMapper = { ), rhcdFilter: ({ rhcdFilter }, searchParams) => rhcdFilter?.forEach(item => searchParams.append(RHCD_FILTER_KEY, item)), updateMethodFilter: ({ updateMethodFilter }, searchParams) => - updateMethodFilter?.forEach(item => searchParams.append(UPDATE_METHOD_KEY, item)) + updateMethodFilter?.forEach(item => searchParams.append(UPDATE_METHOD_KEY, item)), + groupHostFilter: ({ groupHostFilter }, searchParams) => groupHostFilter + ?.forEach(item => searchParams.append('host_group', item)) }; const calculateFilters = (searchParams, filters = []) => { @@ -81,7 +83,8 @@ const Inventory = ({ page, perPage, initialLoading, - hasAccess + hasAccess, + groupHostsFilter }) => { const history = useHistory(); const chrome = useChrome(); @@ -89,7 +92,14 @@ const Inventory = ({ const [isModalOpen, handleModalToggle] = useState(false); const [currentSytem, activateSystem] = useState({}); const [filters, onSetfilters] = useState( - generateFilter(status, source, tagsFilter, filterbyName, operatingSystem, rhcdFilter, updateMethodFilter) + generateFilter(status, + source, + tagsFilter, + filterbyName, + operatingSystem, + rhcdFilter, + updateMethodFilter, + groupHostsFilter) ); const [ediOpen, onEditOpen] = useState(false); const [globalFilter, setGlobalFilter] = useState(); @@ -289,7 +299,8 @@ Inventory.propTypes = { initialLoading: PropTypes.bool, rhcdFilter: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.string), PropTypes.string]), updateMethodFilter: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.string), PropTypes.string]), - hasAccess: PropTypes.bool + hasAccess: PropTypes.bool, + groupHostsFilter: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.string), PropTypes.string]) }; Inventory.defaultProps = { diff --git a/src/store/entities.js b/src/store/entities.js index 087ab00e9..8e79f6b5a 100644 --- a/src/store/entities.js +++ b/src/store/entities.js @@ -45,7 +45,7 @@ export const newDefaultColumns = () => [ }, { key: 'groups', - sortKey: 'groups', + sortKey: 'host_group', title: 'Groups', props: { width: 10 }, renderFunc: () => diff --git a/src/store/inventory-actions.js b/src/store/inventory-actions.js index 57052c366..ae1d97b56 100644 --- a/src/store/inventory-actions.js +++ b/src/store/inventory-actions.js @@ -37,11 +37,13 @@ export const loadEntities = (items = [], { filters, ...config }, { showTags } = ...filters.length === 0 && { registeredWithFilter: [] }, ...(isFilterDisabled('stale') && { staleFilter: undefined }), ...(isFilterDisabled('registeredWith') && { registeredWithFilter: undefined }), - ...(isFilterDisabled('operating_system') && { osFilter: undefined }) + ...(isFilterDisabled('operating_system') && { osFilter: undefined }), + ...(isFilterDisabled('host_group')) && { groupHostFilter: undefined } }) : { ...(isFilterDisabled('stale') && { staleFilter: undefined }), ...(isFilterDisabled('registeredWith') && { registeredWithFilter: undefined }), - ...(isFilterDisabled('operating_system') && { osFilter: undefined }) + ...(isFilterDisabled('operating_system') && { osFilter: undefined }), + ...(isFilterDisabled('host_group')) && { groupHostFilter: undefined } }; const orderBy = config.orderBy || 'updated'; From 5211d0dfcc62340d2e85b4ee50758a44cddd1ac6 Mon Sep 17 00:00:00 2001 From: Aleksandr Voznesenskii Date: Wed, 8 Mar 2023 10:50:24 +0100 Subject: [PATCH 06/12] feat(inventory groups): fixed chips deletion and dropdown checkbox --- src/components/InventoryGroups/utils/api.js | 2 ++ .../InventoryTable/EntityTableToolbar.js | 14 ++++----- src/components/filters/useGroupFilter.js | 30 ++++++++++++------- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/components/InventoryGroups/utils/api.js b/src/components/InventoryGroups/utils/api.js index 4e28e0031..98dd896a9 100644 --- a/src/components/InventoryGroups/utils/api.js +++ b/src/components/InventoryGroups/utils/api.js @@ -4,6 +4,8 @@ import { TABLE_DEFAULT_PAGINATION } from '../../../constants'; import PropTypes from 'prop-types'; export const getGroups = (search = {}, pagination = { page: 1, perPage: TABLE_DEFAULT_PAGINATION }) => { + console.log(search) + console.log(pagination) const parameters = new URLSearchParams({ ...search, ...pagination diff --git a/src/components/InventoryTable/EntityTableToolbar.js b/src/components/InventoryTable/EntityTableToolbar.js index c94cee13f..3d1d346e8 100644 --- a/src/components/InventoryTable/EntityTableToolbar.js +++ b/src/components/InventoryTable/EntityTableToolbar.js @@ -108,7 +108,6 @@ const EntityTableToolbar = ({ const isUpdateMethodEnabled = useFeatureFlag('hbi.ui.system-update-method'); const groupsEnabled = useFeatureFlag('hbi.ui.inventory-groups'); - const { tagsFilter, tagsChip, @@ -140,7 +139,8 @@ const EntityTableToolbar = ({ updateMethodFilter: isUpdateMethodEnabled && !(hideFilters.all && hideFilters.updateMethodFilter !== false) && !hideFilters.updateMethodFilter, - hostGroupFilter: groupsEnabled + hostGroupFilter: groupsEnabled && !(hideFilters.all && hideFilters.hostGroupFilter !== false) + && !hideFilters.hostGroupFilter }; /** @@ -189,7 +189,7 @@ const EntityTableToolbar = ({ enabledFilters.operatingSystem && setOsFilterValue(osFilter); enabledFilters.rhcdFilter && setRhcdFilterValue(rhcdFilter); enabledFilters.updateMethodFilter && setUpdateMethodValue(updateMethodFilter); - groupsEnabled && setHostGroupValue(groupFilter); + enabledFilters.hostGroupFilter && setHostGroupValue(groupFilter); }, []); /** @@ -276,7 +276,7 @@ const EntityTableToolbar = ({ }, [updateMethodValue]); useEffect(() => { - if (shouldReload && groupsEnabled) { + if (shouldReload && enabledFilters.hostGroupFilter) { onSetFilter(hostGroupValue, 'hostGroupFilter', debouncedRefresh); } }, [hostGroupValue]); @@ -313,7 +313,7 @@ const EntityTableToolbar = ({ enabledFilters.operatingSystem && setOsFilterValue([]); enabledFilters.rhcdFilter && setRhcdFilterValue([]); enabledFilters.updateMethodFilter && setUpdateMethodValue([]); - groupsEnabled && setHostGroupValue([]); + enabledFilters.hostGroupFilter && setHostGroupValue([]); dispatch(setFilter([])); updateData({ page: 1, filters: [] }); }; @@ -332,7 +332,7 @@ const EntityTableToolbar = ({ ...!hasItems && enabledFilters.operatingSystem ? osFilterChips : [], ...!hasItems && enabledFilters.rhcdFilter ? rhcdFilterChips : [], ...!hasItems && enabledFilters.updateMethodFilter ? updateMethodChips : [], - ...!hasItems && groupsEnabled ? hostGroupChips : [], + ...!hasItems && enabledFilters.hostGroupFilter ? hostGroupChips : [], ...activeFiltersConfig?.filters || [] ], onDelete: (e, [deleted, ...restDeleted], isAll) => { @@ -359,7 +359,7 @@ const EntityTableToolbar = ({ ...enabledFilters.rhcdFilter ? [rhcdFilterConfig] : [], ...enabledFilters.updateMethodFilter ? [updateMethodConfig] : [], ...showTags && enabledFilters.tags ? [tagsFilter] : [], - ...enabledFilters && groupsEnabled ? [hostGroupConfig] : [] + ...enabledFilters.hostGroupFilter ? [hostGroupConfig] : [] ] : [], ...filterConfig?.items || [] ]; diff --git a/src/components/filters/useGroupFilter.js b/src/components/filters/useGroupFilter.js index b5610924c..29784d33b 100644 --- a/src/components/filters/useGroupFilter.js +++ b/src/components/filters/useGroupFilter.js @@ -1,15 +1,17 @@ /* eslint-disable camelcase */ +import { union } from 'lodash'; import { useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { fetchGroups } from '../../store/inventory-actions'; import { HOST_GROUP_CHIP } from '../../Utilities/index'; +import remove from 'lodash/remove'; //for attaching this filter to the redux export const groupFilterState = { groupHostFilter: null }; export const GROUP_FILTER = 'GROUP_FILTER'; export const groupFilterReducer = (_state, { type, payload }) => ({ ...type === GROUP_FILTER && { - groupFilter: payload + groupHostFilter: payload } }); @@ -59,8 +61,7 @@ const mockApiResponse = [ //receive the array of selected groups and return chips based on the name of selected groups export const buildHostGroupChips = (selectedGroups = []) => { //we use new Set to make sure that chips are unique - const uniqueGroups = [...new Set(selectedGroups)]; - const chips = uniqueGroups?.map((group) => ({ name: group, value: group })); + const chips = [...selectedGroups]?.map((group) => ({ name: group, value: group })); return chips?.length > 0 ? [ { @@ -79,13 +80,20 @@ const useGroupFilter = (apiParams = []) => { acc.push({ label: group.name, value: group.name }); return acc; }, []); + const dispatch = useDispatch(); + //selected are the groups we selected const [selected, setSelected] = useState([]); //host group values are all of the host group values available to the user const [hostGroupValue, setHostGroupValue] = useState(buildHostGroupsValues); + const [hostGroupsFetched, setHostGroupsFetched] = useState([]); + + useEffect(() => { + setHostGroupsFetched(dispatch(fetchGroups(apiParams))); + }, []); const onHostGroupsChange = (event, selection) => { - return setSelected(selected => [...selected, selection].flat(1)); + setSelected(union(selected, selection)); }; const hostGroupsLoaded = useSelector(({ entities }) => entities?.groups); @@ -93,11 +101,6 @@ const useGroupFilter = (apiParams = []) => { setHostGroupValue(hostGroupsLoaded); }, []); const chips = useMemo(() => buildHostGroupChips(selected), [selected]); - /* - const dispatch = useDispatch(); - useEffect(() => { - dispatch(fetchGroups(apiParams)); - }, []); */ //hostGroupConfig is a config that we use in EntityTableToolbar.js const hostGroupConfig = useMemo(() => ({ @@ -108,12 +111,17 @@ const useGroupFilter = (apiParams = []) => { onChange: (event, value) => { onHostGroupsChange(event, value); }, - selected, + value: selected, items: buildHostGroupsValues } }), [selected, hostGroupValue]); - return [chips, hostGroupConfig, selected, setHostGroupValue]; + const setSelectedValues = (currentValue = [], valueToRemove) => { + const newValues = remove(currentValue, valueToRemove); + setSelected(newValues); + }; + + return [chips, hostGroupConfig, selected, setSelectedValues]; }; export default useGroupFilter; From 502b610ac795f947cb84d99caf431e65cc7a1a15 Mon Sep 17 00:00:00 2001 From: Aleksandr Voznesenskii Date: Wed, 8 Mar 2023 13:02:25 +0100 Subject: [PATCH 07/12] feat(inventory groups): added tests and fixed a couple of inconsistencies --- src/components/InventoryGroups/utils/api.js | 2 - .../InventoryTable/EntityTableToolbar.js | 4 +- .../__snapshots__/useGroupFilter.test.js.snap | 86 ++++++++++++++ src/components/filters/useGroupFilter.js | 20 +--- src/components/filters/useGroupFilter.test.js | 111 ++++++++++++++++++ 5 files changed, 204 insertions(+), 19 deletions(-) create mode 100644 src/components/filters/__snapshots__/useGroupFilter.test.js.snap create mode 100644 src/components/filters/useGroupFilter.test.js diff --git a/src/components/InventoryGroups/utils/api.js b/src/components/InventoryGroups/utils/api.js index 98dd896a9..4e28e0031 100644 --- a/src/components/InventoryGroups/utils/api.js +++ b/src/components/InventoryGroups/utils/api.js @@ -4,8 +4,6 @@ import { TABLE_DEFAULT_PAGINATION } from '../../../constants'; import PropTypes from 'prop-types'; export const getGroups = (search = {}, pagination = { page: 1, perPage: TABLE_DEFAULT_PAGINATION }) => { - console.log(search) - console.log(pagination) const parameters = new URLSearchParams({ ...search, ...pagination diff --git a/src/components/InventoryTable/EntityTableToolbar.js b/src/components/InventoryTable/EntityTableToolbar.js index e3d6182ca..f924f7661 100644 --- a/src/components/InventoryTable/EntityTableToolbar.js +++ b/src/components/InventoryTable/EntityTableToolbar.js @@ -115,7 +115,7 @@ const EntityTableToolbar = ({ toValidator, onFromChange, onToChange, endDate, startDate, rangeValidator] = useLastSeenFilter(reducer); const [osFilterConfig, osFilterChips, osFilterValue, setOsFilterValue] = useOperatingSystemFilter(); const [updateMethodConfig, updateMethodChips, updateMethodValue, setUpdateMethodValue] = useUpdateMethodFilter(reducer); - const [hostGroupChips, hostGroupConfig, hostGroupValue, setHostGroupValue] = useGroupFilter(); + const [hostGroupConfig, hostGroupChips, hostGroupValue, setHostGroupValue] = useGroupFilter(); const isUpdateMethodEnabled = useFeatureFlag('hbi.ui.system-update-method'); const groupsEnabled = useFeatureFlag('hbi.ui.inventory-groups'); @@ -328,7 +328,7 @@ const EntityTableToolbar = ({ [RHCD_FILTER_KEY]: (deleted) => setRhcdFilterValue(onDeleteFilter(deleted, rhcdFilterValue)), [LAST_SEEN_CHIP]: (deleted) => setLastSeenFilterValue(onDeleteFilter(deleted, [lastSeenFilterValue.mark])), [UPDATE_METHOD_KEY]: (deleted) => setUpdateMethodValue(onDeleteFilter(deleted, updateMethodValue)), - [HOST_GROUP_CHIP]: (deleted) => setHostGroupValue(hostGroupValue, deleted.chips.map(({ value }) => value)) + [HOST_GROUP_CHIP]: (deleted) => setHostGroupValue(onDeleteFilter(deleted, hostGroupValue)) }; /** * Function to reset all filters with 'Reset Filter' is clicked diff --git a/src/components/filters/__snapshots__/useGroupFilter.test.js.snap b/src/components/filters/__snapshots__/useGroupFilter.test.js.snap new file mode 100644 index 000000000..351e7d7aa --- /dev/null +++ b/src/components/filters/__snapshots__/useGroupFilter.test.js.snap @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`useGroupFilter with groups loaded should match snapshot 1`] = ` +Array [ + Object { + "filterValues": Object { + "items": Array [ + Object { + "label": "sre-group0", + "value": "sre-group0", + }, + Object { + "label": "sre-group1", + "value": "sre-group1", + }, + Object { + "label": "sre-group2", + "value": "sre-group2", + }, + Object { + "label": "sre-group3", + "value": "sre-group3", + }, + ], + "onChange": [Function], + "value": Array [], + }, + "label": "Group", + "type": "checkbox", + "value": "group-host-filter", + }, + Array [], + Array [], + [Function], +] +`; + +exports[`useGroupFilter with groups loaded should return correct chips array, current value and value setter 1`] = ` +Array [ + Object { + "category": "Group", + "chips": Array [ + Object { + "name": "Group1", + "value": "Group1", + }, + ], + "type": "host_group", + }, +] +`; + +exports[`useGroupFilter with groups yet not loaded should return empty state value 1`] = ` +Array [ + Object { + "filterValues": Object { + "items": Array [ + Object { + "label": "sre-group0", + "value": "sre-group0", + }, + Object { + "label": "sre-group1", + "value": "sre-group1", + }, + Object { + "label": "sre-group2", + "value": "sre-group2", + }, + Object { + "label": "sre-group3", + "value": "sre-group3", + }, + ], + "onChange": [Function], + "value": Array [], + }, + "label": "Group", + "type": "checkbox", + "value": "group-host-filter", + }, + Array [], + Array [], + [Function], +] +`; diff --git a/src/components/filters/useGroupFilter.js b/src/components/filters/useGroupFilter.js index 29784d33b..8291c0f79 100644 --- a/src/components/filters/useGroupFilter.js +++ b/src/components/filters/useGroupFilter.js @@ -1,10 +1,8 @@ /* eslint-disable camelcase */ import { union } from 'lodash'; import { useEffect, useMemo, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { fetchGroups } from '../../store/inventory-actions'; +import { useSelector } from 'react-redux'; import { HOST_GROUP_CHIP } from '../../Utilities/index'; -import remove from 'lodash/remove'; //for attaching this filter to the redux export const groupFilterState = { groupHostFilter: null }; @@ -73,24 +71,17 @@ export const buildHostGroupChips = (selectedGroups = []) => { : []; }; -const useGroupFilter = (apiParams = []) => { +const useGroupFilter = () => { //currently mockApiResponse is replacing a proper API response //buildHostGroupsValues build an array of objects to populate dropdown const buildHostGroupsValues = mockApiResponse.reduce((acc, group) => { acc.push({ label: group.name, value: group.name }); return acc; }, []); - const dispatch = useDispatch(); - //selected are the groups we selected const [selected, setSelected] = useState([]); //host group values are all of the host group values available to the user const [hostGroupValue, setHostGroupValue] = useState(buildHostGroupsValues); - const [hostGroupsFetched, setHostGroupsFetched] = useState([]); - - useEffect(() => { - setHostGroupsFetched(dispatch(fetchGroups(apiParams))); - }, []); const onHostGroupsChange = (event, selection) => { setSelected(union(selected, selection)); @@ -116,12 +107,11 @@ const useGroupFilter = (apiParams = []) => { } }), [selected, hostGroupValue]); - const setSelectedValues = (currentValue = [], valueToRemove) => { - const newValues = remove(currentValue, valueToRemove); - setSelected(newValues); + const setSelectedValues = (currentValue = []) => { + setSelected(currentValue); }; - return [chips, hostGroupConfig, selected, setSelectedValues]; + return [hostGroupConfig, chips, selected, setSelectedValues]; }; export default useGroupFilter; diff --git a/src/components/filters/useGroupFilter.test.js b/src/components/filters/useGroupFilter.test.js new file mode 100644 index 000000000..6c9b2e119 --- /dev/null +++ b/src/components/filters/useGroupFilter.test.js @@ -0,0 +1,111 @@ +import { waitFor } from '@testing-library/react'; +import { act, renderHook } from '@testing-library/react-hooks'; +import { Provider } from 'react-redux'; +import configureStore from 'redux-mock-store'; +import { createPromise as promiseMiddleware } from 'redux-promise-middleware'; +import { mockSystemProfile } from '../../__mocks__/hostApi'; +import useGroupFilter from './useGroupFilter'; + +describe('useGroupFilter', () => { + const mockStore = configureStore([promiseMiddleware()]); + beforeEach(() => { + mockSystemProfile.onGet().replyOnce(200); + }); + + describe('with groups yet not loaded', () => { + const wrapper = ({ children }) => ( + + {children} + + ); + + it('should initiate an API request', async () => { + renderHook(useGroupFilter, { wrapper }); + await waitFor(() => { + expect(mockSystemProfile.history.get.length).toBe(1); + }); + mockSystemProfile.resetHistory(); + }); + + it('should return empty state value', () => { + const { result } = renderHook(useGroupFilter, { wrapper }); + expect(result.current).toMatchSnapshot(); + }); + }); + + describe('with groups loaded', () => { + const wrapper = ({ children }) => ( + + {children} + + ); + + it('should match snapshot', () => { + const { result } = renderHook(useGroupFilter, { wrapper }); + expect(result.current).toMatchSnapshot(); + }); + + /* + This test should be rewritten when I connect group filters to the redux store + it('should return correct filter config', () => { + const { result } = renderHook(useGroupFilter, { wrapper }); + const [config] = result.current; + expect(config.filterValues.items).toBe([ + { + label: 'sre-group0', + value: 'sre-group0' + }, + { + label: 'sre-group1', + value: 'sre-group1' + }, + { + label: 'sre-group2', + value: 'sre-group2' + }, + { + label: 'sre-group3', + value: 'sre-group3' + } + ]); + expect(config.label).toBe('Groups'); + }); */ + + it('should return correct chips array, current value and value setter', () => { + const { result } = renderHook(useGroupFilter, { wrapper }); + const [, chips, value, setValue] = result.current; + expect(chips.length).toBe(0); + expect(value.length).toBe(0); + act(() => { + setValue(['Group1']); + }); + const [, chipsUpdated, valueUpdated] = result.current; + expect(chipsUpdated.length).toBe(1); + expect(valueUpdated).toEqual(['Group1']); + expect(chipsUpdated).toMatchSnapshot(); + }); + }); +}); From b6b54aea7a2ce1bfc9995d888b784286373687bb Mon Sep 17 00:00:00 2001 From: Aleksandr Voznesenskii Date: Thu, 9 Mar 2023 12:48:57 +0100 Subject: [PATCH 08/12] feat(inventory groups): adding fetch and redux store --- .../__snapshots__/useGroupFilter.test.js.snap | 56 +++--- src/components/filters/useGroupFilter.js | 79 +++------ src/components/filters/useGroupFilter.test.js | 164 +++++++++++++----- 3 files changed, 162 insertions(+), 137 deletions(-) diff --git a/src/components/filters/__snapshots__/useGroupFilter.test.js.snap b/src/components/filters/__snapshots__/useGroupFilter.test.js.snap index 351e7d7aa..80b3d630d 100644 --- a/src/components/filters/__snapshots__/useGroupFilter.test.js.snap +++ b/src/components/filters/__snapshots__/useGroupFilter.test.js.snap @@ -4,24 +4,7 @@ exports[`useGroupFilter with groups loaded should match snapshot 1`] = ` Array [ Object { "filterValues": Object { - "items": Array [ - Object { - "label": "sre-group0", - "value": "sre-group0", - }, - Object { - "label": "sre-group1", - "value": "sre-group1", - }, - Object { - "label": "sre-group2", - "value": "sre-group2", - }, - Object { - "label": "sre-group3", - "value": "sre-group3", - }, - ], + "items": undefined, "onChange": [Function], "value": Array [], }, @@ -50,28 +33,29 @@ Array [ ] `; +exports[`useGroupFilter with groups loaded should return empty state value if no groups obtained 1`] = ` +Array [ + Object { + "filterValues": Object { + "items": undefined, + "onChange": [Function], + "value": Array [], + }, + "label": "Group", + "type": "checkbox", + "value": "group-host-filter", + }, + Array [], + Array [], + [Function], +] +`; + exports[`useGroupFilter with groups yet not loaded should return empty state value 1`] = ` Array [ Object { "filterValues": Object { - "items": Array [ - Object { - "label": "sre-group0", - "value": "sre-group0", - }, - Object { - "label": "sre-group1", - "value": "sre-group1", - }, - Object { - "label": "sre-group2", - "value": "sre-group2", - }, - Object { - "label": "sre-group3", - "value": "sre-group3", - }, - ], + "items": undefined, "onChange": [Function], "value": Array [], }, diff --git a/src/components/filters/useGroupFilter.js b/src/components/filters/useGroupFilter.js index 8291c0f79..b5bcc60d6 100644 --- a/src/components/filters/useGroupFilter.js +++ b/src/components/filters/useGroupFilter.js @@ -1,7 +1,8 @@ /* eslint-disable camelcase */ import { union } from 'lodash'; import { useEffect, useMemo, useState } from 'react'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; +import { fetchGroups } from '../../store/inventory-actions'; import { HOST_GROUP_CHIP } from '../../Utilities/index'; //for attaching this filter to the redux @@ -13,49 +14,6 @@ export const groupFilterReducer = (_state, { type, payload }) => ({ } }); -const mockApiResponse = [ - { - created_at: '2023-02-28T12:35:20.071Z', - host_ids: [ - 'bA6deCFc19564430AB814bf8F70E8cEf' - ], - id: 'bA6deCFc19564430AB814bf8F70E8cEf', - name: 'sre-group0', - org_id: '000102', - updated_at: '2023-02-28T12:35:20.071Z' - }, - { - created_at: '2023-02-28T12:35:20.071Z', - host_ids: [ - 'bA6deCFc19564430AB814bf8F70E8cEf' - ], - id: 'bA6deCFc19564430AB814bf8F70E8cEf', - name: 'sre-group1', - org_id: '000102', - updated_at: '2023-02-28T12:35:20.071Z' - }, - { - created_at: '2023-02-28T12:35:20.071Z', - host_ids: [ - 'bA6deCFc19564430AB814bf8F70E8cEf' - ], - id: 'bA6deCFc19564430AB814bf8F70E8cEf', - name: 'sre-group2', - org_id: '000102', - updated_at: '2023-02-28T12:35:20.071Z' - }, - { - created_at: '2023-02-28T12:35:20.071Z', - host_ids: [ - 'bA6deCFc19564430AB814bf8F70E8cEf' - ], - id: 'bA6deCFc19564430AB814bf8F70E8cEf', - name: 'sre-group3', - org_id: '000102', - updated_at: '2023-02-28T12:35:20.071Z' - } -]; - //receive the array of selected groups and return chips based on the name of selected groups export const buildHostGroupChips = (selectedGroups = []) => { //we use new Set to make sure that chips are unique @@ -71,27 +29,31 @@ export const buildHostGroupChips = (selectedGroups = []) => { : []; }; -const useGroupFilter = () => { - //currently mockApiResponse is replacing a proper API response - //buildHostGroupsValues build an array of objects to populate dropdown - const buildHostGroupsValues = mockApiResponse.reduce((acc, group) => { - acc.push({ label: group.name, value: group.name }); - return acc; +const useGroupFilter = (apiParams = []) => { + const dispatch = useDispatch(); + useEffect(() => { + dispatch(fetchGroups(apiParams)); }, []); + //fetched values + const fetchedValues = useSelector(({ groups }) => groups?.data?.results); //selected are the groups we selected const [selected, setSelected] = useState([]); - //host group values are all of the host group values available to the user - const [hostGroupValue, setHostGroupValue] = useState(buildHostGroupsValues); - + //buildHostGroupsValues build an array of objects to populate dropdown + const [buildHostGroupsValues, setBuildHostGroupsValues] = useState([]); + //hostGroupValue is used for config items + useEffect(() => { + setBuildHostGroupsValues(fetchedValues?.reduce((acc, group) => { + acc.push({ label: group.name, value: group.name }); + return acc; + }, [])); + }, [fetchedValues]); + //this is used in the filter config as a way to select values onChange const onHostGroupsChange = (event, selection) => { setSelected(union(selected, selection)); }; - const hostGroupsLoaded = useSelector(({ entities }) => entities?.groups); - useEffect(() => { - setHostGroupValue(hostGroupsLoaded); - }, []); const chips = useMemo(() => buildHostGroupChips(selected), [selected]); + //chips that is built for the filter config //hostGroupConfig is a config that we use in EntityTableToolbar.js const hostGroupConfig = useMemo(() => ({ @@ -105,8 +67,9 @@ const useGroupFilter = () => { value: selected, items: buildHostGroupsValues } - }), [selected, hostGroupValue]); + }), [selected, buildHostGroupsValues]); + //setSelectedValues is used for selecting and deleting values const setSelectedValues = (currentValue = []) => { setSelected(currentValue); }; diff --git a/src/components/filters/useGroupFilter.test.js b/src/components/filters/useGroupFilter.test.js index 6c9b2e119..4c5ac9349 100644 --- a/src/components/filters/useGroupFilter.test.js +++ b/src/components/filters/useGroupFilter.test.js @@ -1,3 +1,4 @@ +/* eslint-disable camelcase */ import { waitFor } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react-hooks'; import { Provider } from 'react-redux'; @@ -38,24 +39,104 @@ describe('useGroupFilter', () => { @@ -68,32 +149,6 @@ describe('useGroupFilter', () => { expect(result.current).toMatchSnapshot(); }); - /* - This test should be rewritten when I connect group filters to the redux store - it('should return correct filter config', () => { - const { result } = renderHook(useGroupFilter, { wrapper }); - const [config] = result.current; - expect(config.filterValues.items).toBe([ - { - label: 'sre-group0', - value: 'sre-group0' - }, - { - label: 'sre-group1', - value: 'sre-group1' - }, - { - label: 'sre-group2', - value: 'sre-group2' - }, - { - label: 'sre-group3', - value: 'sre-group3' - } - ]); - expect(config.label).toBe('Groups'); - }); */ - it('should return correct chips array, current value and value setter', () => { const { result } = renderHook(useGroupFilter, { wrapper }); const [, chips, value, setValue] = result.current; @@ -107,5 +162,28 @@ describe('useGroupFilter', () => { expect(valueUpdated).toEqual(['Group1']); expect(chipsUpdated).toMatchSnapshot(); }); + + it('should return empty state value if no groups obtained', () => { + const wrapper = ({ children }) => ( + + {children} + + ); + const { result } = renderHook(useGroupFilter, { wrapper }); + expect(result.current).toMatchSnapshot(); + }); }); }); From 3b521fb117c6b7f4be49d510846ca039cabd6f48 Mon Sep 17 00:00:00 2001 From: Aleksandr Voznesenskii Date: Fri, 10 Mar 2023 13:14:06 +0100 Subject: [PATCH 09/12] feat(inventory groups): fixed tests --- src/components/InventoryTable/EntityTableToolbar.test.js | 4 ++-- src/components/InventoryTable/InventoryTable.test.js | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/InventoryTable/EntityTableToolbar.test.js b/src/components/InventoryTable/EntityTableToolbar.test.js index cc13cb835..8a6b2fcc0 100644 --- a/src/components/InventoryTable/EntityTableToolbar.test.js +++ b/src/components/InventoryTable/EntityTableToolbar.test.js @@ -330,7 +330,7 @@ describe('EntityTableToolbar', () => { ); wrapper.find('.ins-c-chip-filters button.pf-m-link').last().simulate('click'); const actions = store.getActions(); - expect(actions.length).toBe(3); + expect(actions.length).toBe(4); expect(actions[actions.length - 2]).toMatchObject({ type: 'CLEAR_FILTERS' }); expect(onRefreshData).toHaveBeenCalledWith({ filters: [], page: 1 }); }); @@ -355,7 +355,7 @@ describe('EntityTableToolbar', () => { const wrapper = mount( { ); expect(wrapper.find(ConditionalFilter).props().items.map(({ label }) => label)).toEqual( - ['Status', 'Operating System', 'Data Collector', 'RHC status', 'System Update Method', 'Last seen', 'Tags'] + ['Status', + 'Operating System', + 'Data Collector', + 'RHC status', + 'System Update Method', + 'Last seen', + 'Group', + 'Tags'] ); }); From ea15674a1d7a0ac5aab376b11a1f0d909021fc57 Mon Sep 17 00:00:00 2001 From: Aleksandr Voznesenskii Date: Tue, 14 Mar 2023 15:45:58 +0100 Subject: [PATCH 10/12] feat(inventory groups): update after review --- .../__snapshots__/useGroupFilter.test.js.snap | 21 +- src/components/filters/useGroupFilter.js | 12 +- src/components/filters/useGroupFilter.test.js | 217 ++++++++---------- 3 files changed, 125 insertions(+), 125 deletions(-) diff --git a/src/components/filters/__snapshots__/useGroupFilter.test.js.snap b/src/components/filters/__snapshots__/useGroupFilter.test.js.snap index 80b3d630d..cbd401a0a 100644 --- a/src/components/filters/__snapshots__/useGroupFilter.test.js.snap +++ b/src/components/filters/__snapshots__/useGroupFilter.test.js.snap @@ -4,7 +4,20 @@ exports[`useGroupFilter with groups loaded should match snapshot 1`] = ` Array [ Object { "filterValues": Object { - "items": undefined, + "items": Array [ + Object { + "label": "nisi ut consequat ad", + "value": "nisi ut consequat ad", + }, + Object { + "label": "nisi ut consequat ad1", + "value": "nisi ut consequat ad1", + }, + Object { + "label": "nisi ut consequat ad2", + "value": "nisi ut consequat ad2", + }, + ], "onChange": [Function], "value": Array [], }, @@ -24,8 +37,8 @@ Array [ "category": "Group", "chips": Array [ Object { - "name": "Group1", - "value": "Group1", + "name": "nisi ut consequat ad1", + "value": "nisi ut consequat ad1", }, ], "type": "host_group", @@ -37,7 +50,7 @@ exports[`useGroupFilter with groups loaded should return empty state value if no Array [ Object { "filterValues": Object { - "items": undefined, + "items": Array [], "onChange": [Function], "value": Array [], }, diff --git a/src/components/filters/useGroupFilter.js b/src/components/filters/useGroupFilter.js index b5bcc60d6..59903fd7f 100644 --- a/src/components/filters/useGroupFilter.js +++ b/src/components/filters/useGroupFilter.js @@ -1,5 +1,5 @@ /* eslint-disable camelcase */ -import { union } from 'lodash'; +import union from 'lodash/union'; import { useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { fetchGroups } from '../../store/inventory-actions'; @@ -46,10 +46,10 @@ const useGroupFilter = (apiParams = []) => { acc.push({ label: group.name, value: group.name }); return acc; }, [])); - }, [fetchedValues]); + }, [fetchedValues, selected]); //this is used in the filter config as a way to select values onChange - const onHostGroupsChange = (event, selection) => { - setSelected(union(selected, selection)); + const onHostGroupsChange = (event, selection, item) => { + setSelected(union(selection, item)); }; const chips = useMemo(() => buildHostGroupChips(selected), [selected]); @@ -61,8 +61,8 @@ const useGroupFilter = (apiParams = []) => { value: 'group-host-filter', type: 'checkbox', filterValues: { - onChange: (event, value) => { - onHostGroupsChange(event, value); + onChange: (event, value, item) => { + onHostGroupsChange(event, value, item); }, value: selected, items: buildHostGroupsValues diff --git a/src/components/filters/useGroupFilter.test.js b/src/components/filters/useGroupFilter.test.js index 4c5ac9349..0c7e6880a 100644 --- a/src/components/filters/useGroupFilter.test.js +++ b/src/components/filters/useGroupFilter.test.js @@ -1,5 +1,4 @@ /* eslint-disable camelcase */ -import { waitFor } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react-hooks'; import { Provider } from 'react-redux'; import configureStore from 'redux-mock-store'; @@ -20,14 +19,6 @@ describe('useGroupFilter', () => { ); - it('should initiate an API request', async () => { - renderHook(useGroupFilter, { wrapper }); - await waitFor(() => { - expect(mockSystemProfile.history.get.length).toBe(1); - }); - mockSystemProfile.resetHistory(); - }); - it('should return empty state value', () => { const { result } = renderHook(useGroupFilter, { wrapper }); expect(result.current).toMatchSnapshot(); @@ -38,104 +29,102 @@ describe('useGroupFilter', () => { const wrapper = ({ children }) => ( { expect(chips.length).toBe(0); expect(value.length).toBe(0); act(() => { - setValue(['Group1']); + setValue(['nisi ut consequat ad1']); }); const [, chipsUpdated, valueUpdated] = result.current; expect(chipsUpdated.length).toBe(1); - expect(valueUpdated).toEqual(['Group1']); + expect(valueUpdated).toEqual(['nisi ut consequat ad1']); expect(chipsUpdated).toMatchSnapshot(); }); @@ -167,14 +156,12 @@ describe('useGroupFilter', () => { const wrapper = ({ children }) => ( Date: Wed, 15 Mar 2023 11:18:41 +0100 Subject: [PATCH 11/12] Use empty array when failed to select groups --- src/components/GroupSystems/GroupSystems.cy.js | 8 ++++++++ src/components/filters/useGroupFilter.js | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/GroupSystems/GroupSystems.cy.js b/src/components/GroupSystems/GroupSystems.cy.js index 16c7a3e7d..07c072f57 100644 --- a/src/components/GroupSystems/GroupSystems.cy.js +++ b/src/components/GroupSystems/GroupSystems.cy.js @@ -21,6 +21,7 @@ import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; import { featureFlagsInterceptors, + groupsInterceptors, hostsInterceptors, systemProfileInterceptors } from '../../../cypress/support/interceptors'; @@ -76,6 +77,7 @@ describe('renders correctly', () => { hostsInterceptors.successful(); featureFlagsInterceptors.successful(); systemProfileInterceptors['operating system, successful empty'](); + groupsInterceptors['successful with some items'](); mountTable(); cy.wait('@getHosts'); @@ -99,6 +101,7 @@ describe('defaults', () => { hostsInterceptors.successful(); featureFlagsInterceptors.successful(); systemProfileInterceptors['operating system, successful empty'](); + groupsInterceptors['successful with some items'](); mountTable(); cy.wait('@getHosts'); @@ -121,6 +124,7 @@ describe('pagination', () => { hostsInterceptors.successful(); featureFlagsInterceptors.successful(); systemProfileInterceptors['operating system, successful empty'](); + groupsInterceptors['successful with some items'](); mountTable(); cy.wait('@getHosts'); @@ -155,6 +159,7 @@ describe('sorting', () => { hostsInterceptors.successful(); featureFlagsInterceptors.successful(); systemProfileInterceptors['operating system, successful empty'](); + groupsInterceptors['successful with some items'](); mountTable(); cy.wait('@getHosts'); @@ -194,6 +199,7 @@ describe('filtering', () => { hostsInterceptors.successful(); featureFlagsInterceptors.successful(); systemProfileInterceptors['operating system, successful empty'](); + groupsInterceptors['successful with some items'](); mountTable(); cy.wait('@getHosts'); @@ -243,6 +249,7 @@ describe('selection and bulk selection', () => { hostsInterceptors.successful(); featureFlagsInterceptors.successful(); systemProfileInterceptors['operating system, successful empty'](); + groupsInterceptors['successful with some items'](); mountTable(); cy.wait('@getHosts'); @@ -301,6 +308,7 @@ describe('edge cases', () => { hostsInterceptors['successful empty'](); featureFlagsInterceptors.successful(); systemProfileInterceptors['operating system, successful empty'](); + groupsInterceptors['successful with some items'](); mountTable(); cy.wait('@getHosts'); diff --git a/src/components/filters/useGroupFilter.js b/src/components/filters/useGroupFilter.js index 59903fd7f..44a3d2b98 100644 --- a/src/components/filters/useGroupFilter.js +++ b/src/components/filters/useGroupFilter.js @@ -42,7 +42,7 @@ const useGroupFilter = (apiParams = []) => { const [buildHostGroupsValues, setBuildHostGroupsValues] = useState([]); //hostGroupValue is used for config items useEffect(() => { - setBuildHostGroupsValues(fetchedValues?.reduce((acc, group) => { + setBuildHostGroupsValues((fetchedValues || []).reduce((acc, group) => { acc.push({ label: group.name, value: group.name }); return acc; }, [])); From 5f51b37df57fd0130bf39cf1dedde7512a592baf Mon Sep 17 00:00:00 2001 From: Georgii Karataev Date: Wed, 15 Mar 2023 12:59:53 +0100 Subject: [PATCH 12/12] Update snapshots --- .../filters/__snapshots__/useGroupFilter.test.js.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/filters/__snapshots__/useGroupFilter.test.js.snap b/src/components/filters/__snapshots__/useGroupFilter.test.js.snap index cbd401a0a..eed91ba77 100644 --- a/src/components/filters/__snapshots__/useGroupFilter.test.js.snap +++ b/src/components/filters/__snapshots__/useGroupFilter.test.js.snap @@ -68,7 +68,7 @@ exports[`useGroupFilter with groups yet not loaded should return empty state val Array [ Object { "filterValues": Object { - "items": undefined, + "items": Array [], "onChange": [Function], "value": Array [], },