diff --git a/assets/js/components/ChecksSelection/ChecksSelectionHeader.jsx b/assets/js/components/ChecksSelection/ChecksSelectionHeader.jsx new file mode 100644 index 0000000000..015cb695cc --- /dev/null +++ b/assets/js/components/ChecksSelection/ChecksSelectionHeader.jsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { EOS_PLAY_CIRCLE } from 'eos-icons-react'; +import classNames from 'classnames'; + +import Button from '@components/Button'; +import Tooltip from '@components/Tooltip'; + +import { canStartExecution } from '@components/ChecksSelection'; + +function ChecksSelectionHeader({ + targetID, + targetName, + backTo, + pageHeader, + isSavingSelection, + savedSelection, + selection, + onSaveSelection = () => {}, + onStartExecution = () => {}, +}) { + const isAbleToStartExecution = canStartExecution( + savedSelection, + isSavingSelection + ); + return ( +
+ {backTo} +
+
+ {pageHeader} +
+
+
+ + + + +
+
+
+
+ ); +} + +export default ChecksSelectionHeader; diff --git a/assets/js/components/ChecksSelection/ChecksSelectionHeader.stories.jsx b/assets/js/components/ChecksSelection/ChecksSelectionHeader.stories.jsx new file mode 100644 index 0000000000..e0934995ec --- /dev/null +++ b/assets/js/components/ChecksSelection/ChecksSelectionHeader.stories.jsx @@ -0,0 +1,170 @@ +import React from 'react'; +import { faker } from '@faker-js/faker'; +import { MemoryRouter } from 'react-router-dom'; + +import PageHeader from '@components/PageHeader'; +import BackButton from '@components/BackButton'; +import ChecksSelectionHeader from './ChecksSelectionHeader'; + +export default { + title: 'ChecksSelectionHeader', + component: ChecksSelectionHeader, + decorators: [ + (Story) => ( + + + + ), + ], + argTypes: { + targetID: { + control: 'string', + description: 'The target identifier', + table: { + type: { summary: 'string' }, + }, + }, + targetName: { + control: 'string', + description: 'The target name', + table: { + type: { summary: 'string' }, + }, + }, + backTo: { + description: + 'A Component that renders the back button to the target details', + }, + pageHeader: { + description: + 'A Component that renders the page header for the specific target', + }, + selection: { + control: 'array', + description: 'The check selection currently displayed', + }, + savedSelection: { + control: 'array', + description: 'The last saved check selection for the target', + }, + isSavingSelection: { + control: 'boolean', + description: + 'Whether Save Checks Selection button is enabled or disabled', + table: { + type: { summary: 'boolean' }, + }, + }, + onSaveSelection: { + description: 'Updates the selected checks on save', + table: { + type: { summary: 'function' }, + }, + }, + onStartExecution: { + description: 'Starts the host checks execution', + table: { + type: { summary: 'function' }, + }, + }, + }, +}; + +const targetID = faker.datatype.uuid(); +const targetName = faker.lorem.word(7); +const selection = [faker.datatype.uuid()]; +const savedSelection = [faker.datatype.uuid()]; + +export const Default = { + args: { + targetID, + targetName, + backTo: ( + + Back to Target Details + + ), + pageHeader: ( + + Target Settings for {targetName} + + ), + selection, + savedSelection, + isSavingSelection: false, + }, +}; + +export const ClusterChecksSelection = { + args: { + targetID, + targetName, + backTo: ( + + Back to Cluster Details + + ), + pageHeader: ( + + Cluster Settings for {targetName} + + ), + selection, + savedSelection, + isSavingSelection: false, + }, +}; + +export const HostChecksSelection = { + args: { + targetID, + targetName, + backTo: ( + Back to Host Details + ), + pageHeader: ( + + Check Settings for {targetName} + + ), + selection, + savedSelection, + isSavingSelection: false, + }, +}; + +export const SavedSelectionDisabled = { + args: { + targetID, + targetName, + backTo: ( + Back to Host Details + ), + pageHeader: ( + + Check Settings for {targetName} + + ), + selection, + savedSelection, + isSavingSelection: true, + }, +}; + +export const CannotStartExecution = { + args: { + targetID, + targetName, + backTo: ( + Back to Host Details + ), + pageHeader: ( + + Check Settings for {targetName} + + ), + selection, + savedSelection: [], + isSavingSelection: false, + }, +}; diff --git a/assets/js/components/ChecksSelection/ChecksSelectionHeader.test.jsx b/assets/js/components/ChecksSelection/ChecksSelectionHeader.test.jsx new file mode 100644 index 0000000000..544175db4d --- /dev/null +++ b/assets/js/components/ChecksSelection/ChecksSelectionHeader.test.jsx @@ -0,0 +1,142 @@ +import React from 'react'; + +import { screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; + +import { faker } from '@faker-js/faker'; + +import userEvent from '@testing-library/user-event'; +import ChecksSelectionHeader from './ChecksSelectionHeader'; +import { renderWithRouter } from '../../lib/test-utils'; + +describe('ChecksSelectionHeader component', () => { + it('should render a target checks selection header', async () => { + const user = userEvent.setup(); + + const targetID = faker.datatype.uuid(); + const targetName = faker.lorem.word(); + const selection = [faker.datatype.uuid(), faker.datatype.uuid()]; + const savedSelection = [faker.datatype.uuid()]; + const onSaveSelection = jest.fn(); + const onStartExecution = jest.fn(); + + renderWithRouter( + Back to Target Details} + pageHeader={
Target Check Settings
} + isSavingSelection={false} + selection={selection} + savedSelection={savedSelection} + onSaveSelection={onSaveSelection} + onStartExecution={onStartExecution} + /> + ); + + expect(screen.getByText('Target Check Settings')).toBeVisible(); + expect( + screen.getByRole('button', { name: 'Back to Target Details' }) + ).toBeVisible(); + expect( + screen.getByRole('button', { name: 'Save Checks Selection' }) + ).toBeVisible(); + expect( + screen.getByRole('button', { name: 'Start Execution' }) + ).toBeVisible(); + expect( + screen.queryByText( + 'Click Start Execution or wait for Trento to periodically run checks.' + ) + ).toBeVisible(); + + // Saving a selection + await user.click(screen.getByText('Save Checks Selection')); + + expect(onSaveSelection).toHaveBeenCalledWith( + selection, + targetID, + targetName + ); + + // Starting an execution + await user.click(screen.getByText('Start Execution')); + + expect(onStartExecution).toHaveBeenCalled(); + }); + + it('should not allow saving a selection', async () => { + const user = userEvent.setup(); + + const targetID = faker.datatype.uuid(); + const targetName = faker.lorem.word(); + const selection = [faker.datatype.uuid(), faker.datatype.uuid()]; + const onSaveSelection = jest.fn(); + + renderWithRouter( + Back to Target Details} + pageHeader={
Target Check Settings
} + isSavingSelection + selection={selection} + savedSelection={selection} + onSaveSelection={onSaveSelection} + onStartExecution={() => {}} + /> + ); + + expect(screen.getByText('Save Checks Selection')).toBeDisabled(); + + await user.click(screen.getByText('Save Checks Selection')); + + expect(onSaveSelection).not.toHaveBeenCalled(); + }); + + const executionDisallowedScenarios = [ + { + savedSelection: [], + isSavingSelection: false, + }, + { + savedSelection: [faker.datatype.uuid()], + isSavingSelection: true, + }, + { + savedSelection: [], + isSavingSelection: true, + }, + ]; + + it.each(executionDisallowedScenarios)( + 'should not allow starting an execution', + async ({ savedSelection, isSavingSelection }) => { + const user = userEvent.setup(); + + const targetID = faker.datatype.uuid(); + const targetName = faker.lorem.word(); + const onStartExecution = jest.fn(); + + renderWithRouter( + Back to Target Details} + pageHeader={
Target Check Settings
} + isSavingSelection={isSavingSelection} + selection={savedSelection} + savedSelection={savedSelection} + onSaveSelection={() => {}} + onStartExecution={onStartExecution} + /> + ); + + expect(screen.getByText('Start Execution')).toBeDisabled(); + + await user.click(screen.getByText('Start Execution')); + + expect(onStartExecution).not.toHaveBeenCalled(); + } + ); +}); diff --git a/assets/js/components/ChecksSelection/FailAlert.jsx b/assets/js/components/ChecksSelection/FailAlert.jsx deleted file mode 100644 index daa2663a22..0000000000 --- a/assets/js/components/ChecksSelection/FailAlert.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; - -import { EOS_CANCEL } from 'eos-icons-react'; - -function FailAlert({ onClose = () => {}, children }) { - return ( -
- {children} - -
- ); -} - -export default FailAlert; diff --git a/assets/js/components/ClusterSettingsPage/ClusterSettingsPage.jsx b/assets/js/components/ClusterSettingsPage/ClusterSettingsPage.jsx index f8e5aeca67..3a90d96119 100644 --- a/assets/js/components/ClusterSettingsPage/ClusterSettingsPage.jsx +++ b/assets/js/components/ClusterSettingsPage/ClusterSettingsPage.jsx @@ -1,7 +1,6 @@ -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate, useParams } from 'react-router-dom'; -import { EOS_PLAY_CIRCLE } from 'eos-icons-react'; import { clusterChecksSelected } from '@state/checksSelection'; import { @@ -15,18 +14,17 @@ import { getCatalog } from '@state/selectors/catalog'; import { isSaving } from '@state/selectors/checksSelection'; import { executionRequested } from '@state/actions/lastExecutions'; -import Button from '@components/Button'; -import { ClusterInfoBox } from '@components/ClusterDetails'; -import ChecksSelection, { - canStartExecution, -} from '@components/ChecksSelection'; +import { UNKNOWN_PROVIDER, VMWARE_PROVIDER, TARGET_CLUSTER } from '@lib/model'; + import PageHeader from '@components/PageHeader'; import BackButton from '@components/BackButton'; + +import { ClusterInfoBox } from '@components/ClusterDetails'; import LoadingBox from '@components/LoadingBox'; import WarningBanner from '@components/Banners/WarningBanner'; -import Tooltip from '@components/Tooltip'; +import ChecksSelection from '@components/ChecksSelection'; -import { UNKNOWN_PROVIDER, VMWARE_PROVIDER, TARGET_CLUSTER } from '@lib/model'; +import ChecksSelectionHeader from '@components/ChecksSelection/ChecksSelectionHeader'; const catalogWarningBanner = { [UNKNOWN_PROVIDER]: ( @@ -49,12 +47,12 @@ function ClusterSettingsPage() { const navigate = useNavigate(); const dispatch = useDispatch(); const { clusterID } = useParams(); + const [selection, setSelection] = useState([]); const cluster = useSelector(getCluster(clusterID)); const clusterHosts = useSelector((state) => getClusterHosts(state, clusterID) ); - const saving = useSelector(isSaving(TARGET_CLUSTER, clusterID)); const selectedChecks = useSelector((state) => getClusterSelectedChecks(state, clusterID) ); @@ -66,24 +64,12 @@ function ClusterSettingsPage() { loading: catalogLoading, } = useSelector(getCatalog()); - const [selection, setSelection] = useState(selectedChecks); + const saving = useSelector(isSaving(TARGET_CLUSTER, clusterID)); useEffect(() => { setSelection(selectedChecks); }, [selectedChecks]); - const saveSelection = useCallback( - () => - dispatch( - clusterChecksSelected({ - clusterID, - clusterName, - checks: selection, - }) - ), - [clusterID, clusterName, selection] - ); - if (!cluster) { return ; } @@ -98,57 +84,43 @@ function ClusterSettingsPage() { }) ); - const requestExecution = () => { - dispatch(executionRequested(clusterID, clusterHosts, selection, navigate)); + const saveSelection = (newSelection, targetID, targetName) => + dispatch( + clusterChecksSelected({ + clusterID: targetID, + clusterName: targetName, + checks: newSelection, + }) + ); + + const requestChecksExecution = () => { + dispatch( + executionRequested(clusterID, clusterHosts, selectedChecks, navigate) + ); }; return ( -
- - Back to Cluster Details - - -
-
+ <> + + Back to Cluster Details + + } + pageHeader={ Cluster Settings for{' '} {clusterName} -
-
-
- - - - -
-
-
+ } + isSavingSelection={saving} + savedSelection={selectedChecks} + selection={selection} + onSaveSelection={saveSelection} + onStartExecution={requestChecksExecution} + /> {catalogWarningBanner[provider]} -
+ ); } diff --git a/assets/js/components/HostDetails/HostChecksSelection.jsx b/assets/js/components/HostDetails/HostChecksSelection.jsx deleted file mode 100644 index 8aa8f64cc7..0000000000 --- a/assets/js/components/HostDetails/HostChecksSelection.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { EOS_PLAY_CIRCLE } from 'eos-icons-react'; - -import PageHeader from '@components/PageHeader'; -import BackButton from '@components/BackButton'; -import Button from '@components/Button'; -import ChecksSelection from '@components/ChecksSelection'; -import Tooltip from '@components/Tooltip'; - -import HostInfoBox from './HostInfoBox'; - -const defaultSavedSelection = []; - -function HostChecksSelection({ - hostID, - hostName, - provider, - agentVersion, - catalog, - catalogError, - catalogLoading, - onUpdateCatalog, - isSavingSelection, - onSaveSelection, - hostChecksExecutionEnabled, - onStartExecution = () => {}, - savedHostSelection = defaultSavedSelection, -}) { - const [selection, setSelection] = useState([]); - useEffect(() => { - setSelection(savedHostSelection); - }, [savedHostSelection]); - - return ( -
- Back to Host Details - -
-
- - Check Settings for {hostName} - -
-
-
- - 0} - > - - -
-
-
- - - onUpdateCatalog()} - onChange={setSelection} - /> -
- ); -} - -export default HostChecksSelection; diff --git a/assets/js/components/HostDetails/HostChecksSelection.stories.jsx b/assets/js/components/HostDetails/HostChecksSelection.stories.jsx deleted file mode 100644 index ffcf993b65..0000000000 --- a/assets/js/components/HostDetails/HostChecksSelection.stories.jsx +++ /dev/null @@ -1,128 +0,0 @@ -import React from 'react'; -import { faker } from '@faker-js/faker'; -import { MemoryRouter } from 'react-router-dom'; - -import { catalogCheckFactory, hostFactory } from '@lib/test-utils/factories'; -import HostChecksSelection from './HostChecksSelection'; - -const catalog = [ - ...catalogCheckFactory.buildList(3, { group: faker.animal.cat() }), - ...catalogCheckFactory.buildList(6, { group: faker.animal.dog() }), - ...catalogCheckFactory.buildList(2, { group: faker.lorem.word() }), -]; - -const selectedChecks = [ - catalog[0].id, - catalog[1].id, - catalog[2].id, - catalog[5].id, - catalog[6].id, -]; - -const host = hostFactory.build({ - provider: 'azure', - selected_checks: selectedChecks, -}); - -export default { - title: 'HostChecksSelection', - component: HostChecksSelection, - decorators: [ - (Story) => ( - - - - ), - ], - argTypes: { - hostID: { - control: 'string', - description: 'The host identifier', - table: { - type: { summary: 'string' }, - }, - }, - hostName: { - control: 'string', - description: 'The host name', - table: { - type: { summary: 'string' }, - }, - }, - provider: { - control: 'string', - description: 'The discovered CSP where the host is running', - table: { - type: { summary: 'string' }, - }, - }, - agentVersion: { - control: 'string', - description: 'The version of the installed agent', - table: { - type: { summary: 'string' }, - }, - }, - selectedChecks: { - control: 'array', - description: 'The check selection', - }, - catalog: { - control: 'object', - description: 'Catalog data', - table: { - type: { summary: 'object' }, - }, - }, - catalogError: { - control: 'text', - description: 'Error occurred while loading the relevant checks catalog', - table: { - type: { summary: 'string' }, - defaultValue: { summary: null }, - }, - }, - catalogLoading: { - control: { type: 'boolean' }, - description: 'Whether the catalog is loading', - table: { - type: { summary: 'string' }, - defaultValue: { summary: false }, - }, - }, - onUpdateCatalog: { - description: 'Updates the catalog', - }, - isSavingSelection: { - description: - 'Whether Save Checks Selection button is enabled or disabled', - }, - onSaveSelection: { - description: 'Updates the selected checks on save', - }, - onSelectedChecksChange: { - description: 'Updates the selected checks', - }, - hostChecksExecutionEnabled: { - description: 'Whether start execution button is enabled or disabled', - }, - onStartExecution: { - description: 'Starts the host checks execution', - }, - }, -}; - -export const Default = { - args: { - hostID: host.id, - hostName: host.hostname, - provider: host.provider, - agentVersion: host.agent_version, - selectedChecks: host.selected_checks, - catalog, - catalogError: null, - catalogLoading: false, - isSavingSelection: false, - hostChecksExecutionEnabled: false, - }, -}; diff --git a/assets/js/components/HostDetails/HostChecksSelection.test.jsx b/assets/js/components/HostDetails/HostChecksSelection.test.jsx deleted file mode 100644 index 3abd6a271b..0000000000 --- a/assets/js/components/HostDetails/HostChecksSelection.test.jsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; - -import { screen } from '@testing-library/react'; -import '@testing-library/jest-dom'; - -import { faker } from '@faker-js/faker'; -import { catalogCheckFactory, hostFactory } from '@lib/test-utils/factories'; - -import HostChecksSelection from './HostChecksSelection'; -import { renderWithRouter } from '../../lib/test-utils'; - -describe('HostChecksSelection component', () => { - it('should render host check selection', async () => { - const group0 = faker.animal.cat(); - const group1 = faker.animal.dog(); - const group2 = faker.lorem.word(); - const catalog = [ - ...catalogCheckFactory.buildList(2, { group: group0 }), - ...catalogCheckFactory.buildList(2, { group: group1 }), - ...catalogCheckFactory.buildList(2, { group: group2 }), - ]; - - const onUpdateCatalog = jest.fn(); - - const { - id: hostID, - hostname: hostName, - provider, - agent_version: agentVersion, - selected_checks: selectedChecks, - } = hostFactory.build({ provider: 'azure' }); - - renderWithRouter( - - ); - - expect(screen.getByText('Provider')).toBeVisible(); - expect(screen.getByText('Azure')).toBeVisible(); - expect(screen.getByText('Agent version')).toBeVisible(); - expect(screen.getByText(agentVersion)).toBeVisible(); - expect(screen.getByText(group0)).toBeVisible(); - expect(screen.getByText(group1)).toBeVisible(); - expect(screen.getByText(group2)).toBeVisible(); - expect( - screen.getByRole('button', { name: 'Back to Host Details' }) - ).toBeVisible(); - expect( - screen.getByRole('button', { name: 'Save Checks Selection' }) - ).toBeVisible(); - expect( - screen.queryByText( - 'Click Start Execution or wait for Trento to periodically run checks.' - ) - ).not.toBeInTheDocument(); - expect(onUpdateCatalog).toHaveBeenCalled(); - }); -}); diff --git a/assets/js/components/HostDetails/HostSettingsPage.jsx b/assets/js/components/HostDetails/HostSettingsPage.jsx index de42eff554..bca3c5764f 100644 --- a/assets/js/components/HostDetails/HostSettingsPage.jsx +++ b/assets/js/components/HostDetails/HostSettingsPage.jsx @@ -1,9 +1,8 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate, useParams } from 'react-router-dom'; import LoadingBox from '@components/LoadingBox'; -import { canStartExecution } from '@components/ChecksSelection'; import { TARGET_HOST } from '@lib/model'; @@ -13,14 +12,23 @@ import { hostExecutionRequested } from '@state/actions/lastExecutions'; import { getCatalog } from '@state/selectors/catalog'; import { getHost, getHostSelectedChecks } from '@state/selectors/host'; import { isSaving } from '@state/selectors/checksSelection'; -import HostChecksSelection from './HostChecksSelection'; + +import PageHeader from '@components/PageHeader'; +import BackButton from '@components/BackButton'; + +import ChecksSelection from '@components/ChecksSelection'; + +import ChecksSelectionHeader from '@components/ChecksSelection/ChecksSelectionHeader'; +import HostInfoBox from './HostInfoBox'; function HostSettingsPage() { const dispatch = useDispatch(); const navigate = useNavigate(); const { hostID } = useParams(); + const [selection, setSelection] = useState([]); + const host = useSelector(getHost(hostID)); - const hostSelectedChecks = useSelector((state) => + const selectedChecks = useSelector((state) => getHostSelectedChecks(state, hostID) ); @@ -31,10 +39,10 @@ function HostSettingsPage() { } = useSelector(getCatalog()); const saving = useSelector(isSaving(TARGET_HOST, hostID)); - const hostChecksExecutionEnabled = !canStartExecution( - hostSelectedChecks, - saving - ); + + useEffect(() => { + setSelection(selectedChecks); + }, [selectedChecks]); if (!host) { return ; @@ -59,26 +67,39 @@ function HostSettingsPage() { ); }; - const requestHostChecksExecution = () => { - dispatch(hostExecutionRequested(host, hostSelectedChecks, navigate)); + const requestChecksExecution = () => { + dispatch(hostExecutionRequested(host, selectedChecks, navigate)); }; return ( - + <> + Back to Host Details + } + pageHeader={ + + Check Settings for {hostName} + + } + isSavingSelection={saving} + savedSelection={selectedChecks} + selection={selection} + onSaveSelection={saveSelection} + onStartExecution={requestChecksExecution} + /> + + + ); }