Skip to content

Commit

Permalink
Uniform Target Checks Selection (#1842)
Browse files Browse the repository at this point in the history
* Add a generic ChecksSelectionHeader component

* Refactor Host and Cluster settings page to use the ChecksSelectionHeader component

* Remove unused FailAlert component

* Adjust ChecksSelectionHeader stories

* Fix start execution enablement detection

* Improve boolean naming in ChecksSelectionHeader
  • Loading branch information
nelsonkopliku authored Sep 20, 2023
1 parent 481bef8 commit 9993ec2
Show file tree
Hide file tree
Showing 9 changed files with 467 additions and 401 deletions.
69 changes: 69 additions & 0 deletions assets/js/components/ChecksSelection/ChecksSelectionHeader.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="w-full px-2 sm:px-0">
{backTo}
<div className="flex flex-wrap">
<div className="flex w-1/2 h-auto overflow-hidden overflow-ellipsis break-words">
{pageHeader}
</div>
<div className="flex w-1/2 justify-end">
<div className="flex w-fit whitespace-nowrap">
<Button
type="primary"
className="mx-1"
onClick={() => onSaveSelection(selection, targetID, targetName)}
disabled={isSavingSelection}
>
Save Checks Selection
</Button>
<Tooltip
className="w-56"
content="Click Start Execution or wait for Trento to periodically run checks."
visible={isAbleToStartExecution}
>
<Button
type="primary"
className="mx-1"
onClick={onStartExecution}
disabled={!isAbleToStartExecution}
>
<EOS_PLAY_CIRCLE
className={classNames('inline-block align-sub', {
'fill-white': isAbleToStartExecution,
'fill-gray-200': !isAbleToStartExecution,
})}
/>{' '}
Start Execution
</Button>
</Tooltip>
</div>
</div>
</div>
</div>
);
}

export default ChecksSelectionHeader;
170 changes: 170 additions & 0 deletions assets/js/components/ChecksSelection/ChecksSelectionHeader.stories.jsx
Original file line number Diff line number Diff line change
@@ -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) => (
<MemoryRouter>
<Story />
</MemoryRouter>
),
],
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: (
<BackButton url={`/target/${targetID}`}>
Back to Target Details
</BackButton>
),
pageHeader: (
<PageHeader>
Target Settings for <span className="font-bold">{targetName}</span>
</PageHeader>
),
selection,
savedSelection,
isSavingSelection: false,
},
};

export const ClusterChecksSelection = {
args: {
targetID,
targetName,
backTo: (
<BackButton url={`/clusters/${targetID}`}>
Back to Cluster Details
</BackButton>
),
pageHeader: (
<PageHeader>
Cluster Settings for <span className="font-bold">{targetName}</span>
</PageHeader>
),
selection,
savedSelection,
isSavingSelection: false,
},
};

export const HostChecksSelection = {
args: {
targetID,
targetName,
backTo: (
<BackButton url={`/hosts/${targetID}`}>Back to Host Details</BackButton>
),
pageHeader: (
<PageHeader>
Check Settings for <span className="font-bold">{targetName}</span>
</PageHeader>
),
selection,
savedSelection,
isSavingSelection: false,
},
};

export const SavedSelectionDisabled = {
args: {
targetID,
targetName,
backTo: (
<BackButton url={`/hosts/${targetID}`}>Back to Host Details</BackButton>
),
pageHeader: (
<PageHeader>
Check Settings for <span className="font-bold">{targetName}</span>
</PageHeader>
),
selection,
savedSelection,
isSavingSelection: true,
},
};

export const CannotStartExecution = {
args: {
targetID,
targetName,
backTo: (
<BackButton url={`/hosts/${targetID}`}>Back to Host Details</BackButton>
),
pageHeader: (
<PageHeader>
Check Settings for <span className="font-bold">{targetName}</span>
</PageHeader>
),
selection,
savedSelection: [],
isSavingSelection: false,
},
};
142 changes: 142 additions & 0 deletions assets/js/components/ChecksSelection/ChecksSelectionHeader.test.jsx
Original file line number Diff line number Diff line change
@@ -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(
<ChecksSelectionHeader
targetID={targetID}
targetName={targetName}
backTo={<button type="button">Back to Target Details</button>}
pageHeader={<div>Target Check Settings</div>}
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(
<ChecksSelectionHeader
targetID={targetID}
targetName={targetName}
backTo={<button type="button">Back to Target Details</button>}
pageHeader={<div>Target Check Settings</div>}
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(
<ChecksSelectionHeader
targetID={targetID}
targetName={targetName}
backTo={<button type="button">Back to Target Details</button>}
pageHeader={<div>Target Check Settings</div>}
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();
}
);
});
Loading

0 comments on commit 9993ec2

Please sign in to comment.