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}
+ />
+
+
+ >
);
}