diff --git a/config/overrideChrome.js b/config/overrideChrome.js
index 3040a159a..6e034ebe3 100644
--- a/config/overrideChrome.js
+++ b/config/overrideChrome.js
@@ -3,7 +3,7 @@ const chromeMock = {
isBeta: () => false,
appAction: () => {},
appObjectId: () => {},
- on: () => {},
+ on: () => () => {},
getApp: () => 'inventory',
getBundle: () => 'insights',
getUserPermissions: () => [{ permission: 'inventory:*:*' }],
@@ -25,6 +25,7 @@ const chromeMock = {
},
}),
},
+ hideGlobalFilter: () => {},
};
export default () => chromeMock;
diff --git a/config/setupTests.js b/config/setupTests.js
index c207562e4..c304b0552 100644
--- a/config/setupTests.js
+++ b/config/setupTests.js
@@ -43,6 +43,7 @@ jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
getUserPermissions: () => Promise.resolve(['inventory:*:*']),
getApp: jest.fn(),
getBundle: jest.fn(),
+ hideGlobalFilter: jest.fn(),
}),
}));
diff --git a/src/components/GroupSystems/GroupSystems.js b/src/components/GroupSystems/GroupSystems.js
index 6e6280d29..af4614465 100644
--- a/src/components/GroupSystems/GroupSystems.js
+++ b/src/components/GroupSystems/GroupSystems.js
@@ -20,6 +20,8 @@ import { clearEntitiesAction } from '../../store/actions';
import { useBulkSelectConfig } from '../../Utilities/hooks/useBulkSelectConfig';
import difference from 'lodash/difference';
import map from 'lodash/map';
+import useGlobalFilter from '../filters/useGlobalFilter';
+
export const prepareColumns = (
initialColumns,
hideGroupColumn,
@@ -74,6 +76,7 @@ export const prepareColumns = (
const GroupSystems = ({ groupName, groupId }) => {
const dispatch = useDispatch();
+ const globalFilter = useGlobalFilter();
const [removeHostsFromGroupModalOpen, setRemoveHostsFromGroupModalOpen] =
useState(false);
const [currentSystem, setCurrentSystem] = useState([]);
@@ -104,7 +107,7 @@ const GroupSystems = ({ groupName, groupId }) => {
const bulkSelectConfig = useBulkSelectConfig(
selected,
- null,
+ globalFilter,
total,
rows,
true,
@@ -220,6 +223,8 @@ const GroupSystems = ({ groupName, groupId }) => {
showTags
ref={inventory}
showCentosVersions
+ customFilters={{ globalFilter }}
+ autoRefresh
/>
)}
diff --git a/src/components/InventoryGroupDetail/index.js b/src/components/InventoryGroupDetail/index.js
index c9f32209b..fb3643d2c 100644
--- a/src/components/InventoryGroupDetail/index.js
+++ b/src/components/InventoryGroupDetail/index.js
@@ -1,15 +1,9 @@
-import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
-import React, { useEffect } from 'react';
+import React from 'react';
import { useParams } from 'react-router-dom';
import InventoryGroupDetail from './InventoryGroupDetail';
const InventoryGroupDetailWrapper = () => {
const { groupId } = useParams();
- const chrome = useChrome();
-
- useEffect(() => {
- chrome?.hideGlobalFilter?.();
- }, []);
return ;
};
diff --git a/src/components/InventoryGroups/Modals/CreateGroupModal.js b/src/components/InventoryGroups/Modals/CreateGroupModal.js
index 87458538c..9df6c760f 100644
--- a/src/components/InventoryGroups/Modals/CreateGroupModal.js
+++ b/src/components/InventoryGroups/Modals/CreateGroupModal.js
@@ -7,7 +7,11 @@ import { createGroup, validateGroupName } from '../utils/api';
import { useDispatch } from 'react-redux';
import awesomeDebouncePromise from 'awesome-debounce-promise';
-export const validate = async (value) => {
+export const validate = async (value = '') => {
+ if (value.length === 0) {
+ return undefined; // the input is empty
+ }
+
const results = await validateGroupName(value.trim());
if (results === true) {
throw 'Group name already exists';
diff --git a/src/components/InventoryGroups/Modals/CreateGroupModal.test.js b/src/components/InventoryGroups/Modals/CreateGroupModal.test.js
index 9ae4a7ad0..1fcb0d123 100644
--- a/src/components/InventoryGroups/Modals/CreateGroupModal.test.js
+++ b/src/components/InventoryGroups/Modals/CreateGroupModal.test.js
@@ -1,25 +1,144 @@
+import { render, screen, waitFor } from '@testing-library/react';
+import React from 'react';
import { validateGroupName } from '../utils/api';
-import { validate } from './CreateGroupModal';
+import CreateGroupModal, { validate } from './CreateGroupModal';
+import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
jest.mock('../utils/api');
+jest.mock('react-redux');
describe('validate function', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
it('works with basic input', async () => {
const result = await validate('test');
- expect(result).toBe(undefined);
+ expect(result).toBeUndefined();
expect(validateGroupName).toHaveBeenCalledWith('test');
});
it('trims input', async () => {
const result = await validate(' test ');
- expect(result).toBe(undefined);
+ expect(result).toBeUndefined();
expect(validateGroupName).toHaveBeenCalledWith('test');
});
it('throws error if the name is present', async () => {
validateGroupName.mockResolvedValue(true);
+
await expect(validate('test')).rejects.toBe('Group name already exists');
});
+
+ it('does not check on undefined input', async () => {
+ const result = await validate(undefined);
+
+ expect(result).toBeUndefined();
+ expect(validateGroupName).not.toHaveBeenCalled();
+ });
+
+ it('does not check on empty input', async () => {
+ const result = await validate('');
+
+ expect(result).toBeUndefined();
+ expect(validateGroupName).not.toHaveBeenCalled();
+ });
+});
+
+describe('create group modal', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const setIsModalOpen = jest.fn();
+ const reloadData = jest.fn();
+
+ it('create button is initially disabled', () => {
+ render(
+
+ );
+
+ expect(
+ screen.getByRole('button', {
+ name: /create/i,
+ })
+ ).toBeDisabled();
+ });
+
+ it('can create a group with new name', async () => {
+ validateGroupName.mockResolvedValue(false);
+
+ render(
+
+ );
+
+ await userEvent.type(
+ screen.getByRole('textbox', {
+ name: /group name/i,
+ }),
+ '_abc'
+ );
+
+ await waitFor(() => {
+ expect(
+ screen.getByRole('button', {
+ name: /create/i,
+ })
+ ).toBeEnabled();
+ });
+ });
+
+ it('cannot create a group with incorrect name', async () => {
+ render(
+
+ );
+
+ expect(
+ screen.getByRole('button', {
+ name: /create/i,
+ })
+ ).toBeDisabled();
+
+ await userEvent.type(
+ screen.getByRole('textbox', {
+ name: /group name/i,
+ }),
+ '###'
+ );
+
+ expect(
+ screen.getByRole('button', {
+ name: /create/i,
+ })
+ ).toBeDisabled();
+
+ await userEvent.click(
+ screen.getByRole('button', {
+ name: /create/i,
+ })
+ ); // must change focus for the hint to appear (DDF implementation)
+
+ await waitFor(() => {
+ expect(
+ screen.getByText(
+ 'Valid characters include letters, numbers, spaces, hyphens ( - ), and underscores ( _ ).'
+ )
+ ).toBeVisible();
+ });
+ });
});
diff --git a/src/components/InventoryGroups/Modals/ModalSchemas/schemes.js b/src/components/InventoryGroups/Modals/ModalSchemas/schemes.js
index 2068c9d5a..f4509ee40 100644
--- a/src/components/InventoryGroups/Modals/ModalSchemas/schemes.js
+++ b/src/components/InventoryGroups/Modals/ModalSchemas/schemes.js
@@ -13,7 +13,7 @@ export const createGroupSchema = (namePresenceValidator) => ({
name: 'name',
label: 'Group name',
helperText:
- 'Can only contain letters, numbers, spaces, hyphens ( - ), and underscores( _ ).',
+ 'Can only contain letters, numbers, spaces, hyphens ( - ), and underscores ( _ ).',
isRequired: true,
autoFocus: true,
validate: [
diff --git a/src/components/InventoryGroups/helpers/validate.js b/src/components/InventoryGroups/helpers/validate.js
index 98f41211d..bef6ab301 100644
--- a/src/components/InventoryGroups/helpers/validate.js
+++ b/src/components/InventoryGroups/helpers/validate.js
@@ -2,7 +2,7 @@ import validatorTypes from '@data-driven-forms/react-form-renderer/validator-typ
export const nameValidator = {
type: validatorTypes.PATTERN,
- pattern: /^[A-Za-z0-9]+[A-Za-z0-9_\-\s]*$/,
+ pattern: /^[A-Za-z0-9_\-\s]+[A-Za-z0-9_\-\s]*$/,
message:
- 'Must start with a letter or number. Valid characters include lowercase letters, numbers, hyphens ( - ), and underscores ( _ ).',
+ 'Valid characters include letters, numbers, spaces, hyphens ( - ), and underscores ( _ ).',
};
diff --git a/src/components/InventoryTabs/ConventionalSystems/ConventionalSystemsTab.js b/src/components/InventoryTabs/ConventionalSystems/ConventionalSystemsTab.js
index b28bfcfb1..c27ab650c 100644
--- a/src/components/InventoryTabs/ConventionalSystems/ConventionalSystemsTab.js
+++ b/src/components/InventoryTabs/ConventionalSystems/ConventionalSystemsTab.js
@@ -9,7 +9,6 @@ import { generateFilter } from '../../../Utilities/constants';
import { InventoryTable as InventoryTableCmp } from '../../InventoryTable';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import AddSelectedHostsToGroupModal from '../../InventoryGroups/Modals/AddSelectedHostsToGroupModal';
-import useFeatureFlag from '../../../Utilities/useFeatureFlag';
import { useBulkSelectConfig } from '../../../Utilities/hooks/useBulkSelectConfig';
import RemoveHostsFromGroupModal from '../../InventoryGroups/Modals/RemoveHostsFromGroupModal';
import {
@@ -27,6 +26,7 @@ import uniq from 'lodash/uniq';
import useInsightsNavigate from '@redhat-cloud-services/frontend-components-utilities/useInsightsNavigate/useInsightsNavigate';
import useTableActions from './useTableActions';
import { calculateFilters, calculatePagination } from './Utilities';
+import useGlobalFilter from '../../filters/useGlobalFilter';
const BulkDeleteButton = ({ selectedSystems, ...props }) => {
const requiredPermissions = selectedSystems.map(({ groups }) =>
@@ -87,7 +87,7 @@ const ConventionalSystemsTab = ({
const [addHostGroupModalOpen, setAddHostGroupModalOpen] = useState(false);
const [removeHostsFromGroupModalOpen, setRemoveHostsFromGroupModalOpen] =
useState(false);
- const [globalFilter, setGlobalFilter] = useState();
+ const globalFilter = useGlobalFilter();
const rows = useSelector(({ entities }) => entities?.rows, shallowEqual);
const loaded = useSelector(({ entities }) => entities?.loaded);
const selected = useSelector(({ entities }) => entities?.selected);
@@ -118,37 +118,10 @@ const ConventionalSystemsTab = ({
}
};
- const EdgeParityFilterDeviceEnabled = useFeatureFlag(
- 'edgeParity.inventory-list-filter'
- );
-
useEffect(() => {
chrome.updateDocumentTitle('Systems | Red Hat Insights');
- chrome?.hideGlobalFilter?.(false);
chrome.appAction('system-list');
chrome.appObjectId();
- chrome.on('GLOBAL_FILTER_UPDATE', ({ data }) => {
- const [workloads, SID, tags] = chrome.mapGlobalFilter(data, false, true);
- setGlobalFilter({
- tags,
- filter: {
- ...globalFilter?.filter,
- system_profile: {
- ...globalFilter?.filter?.system_profile,
- ...(workloads?.SAP?.isSelected && { sap_system: true }),
- ...(workloads &&
- workloads['Ansible Automation Platform']?.isSelected && {
- ansible: 'not_nil',
- }),
- ...(workloads?.['Microsoft SQL']?.isSelected && {
- mssql: 'not_nil',
- }),
- ...(EdgeParityFilterDeviceEnabled && { host_type: 'nil' }),
- ...(SID?.length > 0 && { sap_sids: SID }),
- },
- },
- });
- });
dispatch(actions.clearNotifications());
if (perPage || page) {
diff --git a/src/components/filters/useGlobalFilter.js b/src/components/filters/useGlobalFilter.js
new file mode 100644
index 000000000..ce7b505f4
--- /dev/null
+++ b/src/components/filters/useGlobalFilter.js
@@ -0,0 +1,44 @@
+import { useEffect, useState } from 'react';
+import useFeatureFlag from '../../Utilities/useFeatureFlag';
+import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
+
+const useGlobalFilter = () => {
+ const chrome = useChrome();
+ const edgeParityFilterDeviceEnabled = useFeatureFlag(
+ 'edgeParity.inventory-list-filter'
+ );
+ const [globalFilter, setGlobalFilter] = useState();
+
+ useEffect(() => {
+ chrome.hideGlobalFilter(false);
+ const unlisten = chrome.on('GLOBAL_FILTER_UPDATE', ({ data }) => {
+ const [workloads, SID, tags] = chrome.mapGlobalFilter(data, false, true);
+
+ setGlobalFilter({
+ tags,
+ filter: {
+ ...globalFilter?.filter,
+ system_profile: {
+ ...globalFilter?.filter?.system_profile,
+ ...(workloads?.SAP?.isSelected && { sap_system: true }),
+ ...(workloads &&
+ workloads['Ansible Automation Platform']?.isSelected && {
+ ansible: 'not_nil',
+ }),
+ ...(workloads?.['Microsoft SQL']?.isSelected && {
+ mssql: 'not_nil',
+ }),
+ ...(edgeParityFilterDeviceEnabled && { host_type: 'nil' }),
+ ...(SID?.length > 0 && { sap_sids: SID }),
+ },
+ },
+ });
+ });
+
+ return () => unlisten();
+ }, [edgeParityFilterDeviceEnabled]);
+
+ return globalFilter;
+};
+
+export default useGlobalFilter;