Skip to content

Commit

Permalink
Refs #36576 - Fix errors and bugs in edit modal, add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Trevor Allison committed Jul 24, 2023
1 parent da24d9d commit 9c33b9a
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 26 deletions.
8 changes: 4 additions & 4 deletions webpack/scenes/ActivationKeys/Details/ActivationKeyActions.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { translate as __ } from 'foremanReact/common/I18n';
import { APIActions, API_OPERATIONS, put, get } from 'foremanReact/redux/API';
import { errorToast } from '../../Tasks/helpers';
import api from '../../../services/api';
import katelloApi from '../../../services/api/index';
import { ACTIVATION_KEY } from './ActivationKeyConstants';

export const getActivationKey = akId => get({
type: API_OPERATIONS.GET,
key: `${ACTIVATION_KEY}_${akId}`,
url: api.getApiUrl(`/activation_keys/${akId}`),
url: katelloApi.getApiUrl(`/activation_keys/${akId}`),
});

export const putActivationKey = (akId, params) => put({
type: API_OPERATIONS.PUT,
key: `${ACTIVATION_KEY}_${akId}`,
url: api.getApiUrl(`/activation_keys/${akId}`),
url: katelloApi.getApiUrl(`/activation_keys/${akId}`),
successToast: () => __('Activation key details updated'),
errorToast,
params,
Expand All @@ -22,7 +22,7 @@ export const putActivationKey = (akId, params) => put({
export const deleteActivationKey = akId => APIActions.delete({
type: API_OPERATIONS.DELETE,
key: `${ACTIVATION_KEY}_${akId}`,
url: api.getApiUrl(`/activation_keys/${akId}`),
url: katelloApi.getApiUrl(`/activation_keys/${akId}`),
successToast: () => __('Activation key deleted'),
errorToast,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import DeleteMenu from './components/DeleteMenu';
import { getActivationKey } from './ActivationKeyActions';
import DeleteModal from './components/DeleteModal';


const ActivationKeyDetails = ({ match }) => {
const dispatch = useDispatch();
const akId = match?.params?.id;
Expand Down Expand Up @@ -74,7 +73,7 @@ const ActivationKeyDetails = ({ match }) => {
<Split hasGutter style={{ display: 'inline-flex' }}>
<SplitItem>
<Label>
{akDetails.usageCount}/{akDetails.unlimitedHosts ? __('Unlimited') : akDetails.maxHosts}
{akDetails.usageCount ? akDetails.usageCount : 0}/{akDetails.unlimitedHosts ? __('Unlimited') : akDetails.maxHosts}
</Label>
</SplitItem>
</Split>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { propsToCamelCase } from 'foremanReact/common/helpers';

Check failure on line 1 in webpack/scenes/ActivationKeys/Details/ActivationKeyDetailsHelpers.js

View workflow job for this annotation

GitHub Actions / build

'propsToCamelCase' is defined but never used

export const REMOTE_EXECUTION = 'remoteExecution';
export const KATELLO_AGENT = 'katelloAgent';


export const akIsNotRegistered = ({ akDetails }) => {
const {
purpose_usage: purposeUsage,
purpose_role: purposeRole,

Check failure on line 10 in webpack/scenes/ActivationKeys/Details/ActivationKeyDetailsHelpers.js

View workflow job for this annotation

GitHub Actions / build

'purposeRole' is assigned a value but never used
release_version: releaseVersion,

Check failure on line 11 in webpack/scenes/ActivationKeys/Details/ActivationKeyDetailsHelpers.js

View workflow job for this annotation

GitHub Actions / build

'releaseVersion' is assigned a value but never used
service_level: serviceLevel,

Check failure on line 12 in webpack/scenes/ActivationKeys/Details/ActivationKeyDetailsHelpers.js

View workflow job for this annotation

GitHub Actions / build

'serviceLevel' is assigned a value but never used
} = akDetails;
return !purposeUsage?.uuid;
};

export const akIsRegistered = ({ akDetails }) => !akIsNotRegistered({ akDetails });

export const akHasRequiredPermissions = (requiredPermissions = [], userPermissions = {}) => {
const permittedActions = Object.keys(userPermissions).filter(key => userPermissions[key]);
return requiredPermissions.every(permission => permittedActions.includes(permission));
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {
selectAPIStatus,
selectAPIError,
selectAPIResponse,
} from 'foremanReact/redux/API/APISelectors';
import { STATUS } from 'foremanReact/constants';
import { ACTIVATION_KEY } from './ActivationKeyConstants';

export const selectAKDetails = state =>
selectAPIResponse(state, ACTIVATION_KEY) ?? {};

export const selectAKDetailsStatus = state =>
selectAPIStatus(state, ACTIVATION_KEY) ?? STATUS.PENDING;

export const selectAKDetailsError = state =>
selectAPIError(state, ACTIVATION_KEY);
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React from 'react';
import { renderWithRedux, patientlyWaitFor, fireEvent } from 'react-testing-lib-wrapper';
import { assertNockRequest, nockInstance } from '../../../../test-utils/nockWrapper';
import ActivationKeyDetails from '../ActivationKeyDetails';
import katelloApi from '../../../../services/api/index';

const akDetails = katelloApi.getApiUrl('/activation_keys/1');

const baseAKDetails = {
id: 1,
name: 'test',
description: 'test description',
unlimited_hosts: false,
usage_count: 1,
max_hosts: 4,
};

const renderOptions = {
initialState: {
// This is the API state that your tests depend on for their data
// You can cross reference the needed useSelectors from your tested components
// with the data found within the redux chrome add-on to help determine this fixture data.
katello: {
hostDetails: {},
},
},
};

test('Makes API call and displays AK details on screen', async (done) => {
const akScope = nockInstance
.get(akDetails)
.reply(200, baseAKDetails);
// eslint-disable-next-line max-len
const { getByText, getByRole } = renderWithRedux(<ActivationKeyDetails match={{ params: { id: '1' } }} />, renderOptions);
await patientlyWaitFor(() => expect(getByRole('heading', { name: 'test' })).toBeInTheDocument());
expect(getByText('test description')).toBeInTheDocument();
expect(getByText('1/4')).toBeInTheDocument();

assertNockRequest(akScope, done);
});

test('Displays placeholder when description is missing', async (done) => {
const akScope = nockInstance
.get(akDetails)
.reply(
200,
{
...baseAKDetails,
description: '',
},
);
// eslint-disable-next-line max-len
const { getByText, getByRole } = renderWithRedux(<ActivationKeyDetails match={{ params: { id: '1' } }} />, renderOptions);
await patientlyWaitFor(() => expect(getByRole('heading', { name: 'test' })).toBeInTheDocument());
expect(getByText('No description provided')).toBeInTheDocument();

assertNockRequest(akScope, done);
});

test('Delete menu appears when toggle is clicked', async (done) => {
const akScope = nockInstance
.get(akDetails)
.reply(200, baseAKDetails);
// eslint-disable-next-line max-len
const { getByText, getByLabelText } = renderWithRedux(<ActivationKeyDetails match={{ params: { id: '1' } }} />, renderOptions);
const deleteToggle = getByLabelText('delete-toggle');
fireEvent.click(deleteToggle);
await patientlyWaitFor(() => expect(getByText('Delete')).toBeInTheDocument());

assertNockRequest(akScope, done);
});

test('Edit modal appears when button is clicked', async (done) => {
const akScope = nockInstance
.get(akDetails)
.reply(200, baseAKDetails);
const { getByLabelText, getByText } = renderWithRedux(<ActivationKeyDetails match={{ params: { id: '1' } }} />, renderOptions);
const editButton = getByLabelText('edit-button');
fireEvent.click(editButton);
await patientlyWaitFor(() => expect(getByText('Edit activation key')).toBeInTheDocument());

assertNockRequest(akScope, done);
});

test('Page displays 0 when usage count is null', async (done) => {
const akScope = nockInstance
.get(akDetails)
.reply(
200,
{
...baseAKDetails,
usage_count: null,
},
);

const { getByText, getByRole } = renderWithRedux(<ActivationKeyDetails match={{ params: { id: '1' } }} />, renderOptions);
await patientlyWaitFor(() => expect(getByRole('heading', { name: 'test' })).toBeInTheDocument());
expect(getByText('0/4')).toBeInTheDocument();

assertNockRequest(akScope, done);
});

test('Delete modal appears when link is clicked', async (done) => {
const akScope = nockInstance
.get(akDetails)
.reply(200, baseAKDetails);
// eslint-disable-next-line max-len
const { getByText, getByLabelText } = renderWithRedux(<ActivationKeyDetails match={{ params: { id: '1' } }} />, renderOptions);
const deleteToggle = getByLabelText('delete-toggle');
fireEvent.click(deleteToggle);
await patientlyWaitFor(() => expect(getByText('Delete')).toBeInTheDocument());
const deleteLink = getByLabelText('delete-link');
fireEvent.click(deleteLink);
await patientlyWaitFor(() => expect(getByText('Activation Key will no longer be available for use. This operation cannot be undone.')).toBeInTheDocument());

assertNockRequest(akScope, done);
});
30 changes: 23 additions & 7 deletions webpack/scenes/ActivationKeys/Details/components/DeleteMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import React, {
useState,
} from 'react';
import PropTypes from 'prop-types';
import { Dropdown, DropdownItem, KebabToggle, DropdownPosition } from '@patternfly/react-core';
import { Dropdown, DropdownItem, KebabToggle, DropdownPosition, Split, Icon, Text } from '@patternfly/react-core';
import { UndoIcon, TrashIcon } from '@patternfly/react-icons';
import { noop } from 'foremanReact/common/helpers';
import { translate as __ } from 'foremanReact/common/I18n';

Expand All @@ -22,25 +23,40 @@ const DeleteMenu = ({ handleModalToggle, akId }) => {
const dropdownItems = [
<DropdownItem
ouiaId="delete-menu-option"
key="delete"
aria-label="delete-link"
key="delete-link"
component="button"
onClick={handleModalToggle}
>
{__('Delete')}
<Split hasGutter>
<Icon>
<TrashIcon />
</Icon>
<Text ouiaId="delete-text">
{__('Delete')}
</Text>
</Split>
</DropdownItem>,
<DropdownItem
ouiaId="linkbacktooldpage"
key="link"
ouiaId="ak-legacy-ui"
key="ak-legacy-ui-link"
href={`../../../activation_keys/${akId}`}
>
{__('Old Activation key Details Page')}
<Split hasGutter>
<Icon>
<UndoIcon />
</Icon>
<Text ouiaId="delete-text">
{__('Legacy UI')}
</Text>
</Split>
</DropdownItem>];
return (
<Dropdown
ouiaId="dekete-action"
onSelect={onSelect}
position={DropdownPosition.right}
toggle={<KebabToggle id="toggle-kebab" onToggle={onToggle} />}
toggle={<KebabToggle id="toggle-kebab" aria-label="delete-toggle" onToggle={onToggle} />}
isOpen={isOpen}
isPlain
dropdownItems={dropdownItems}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ const DeleteModal = ({ isModalOpen, handleModalToggle, akId }) => {
ouiaId="ak-delete-modal"
variant={ModalVariant.small}
title={[
<Flex>
<Icon status="warning">
<Flex key="delete-modal-header">
<Icon status="warning" key="exclamation-triangle">
<ExclamationTriangleIcon />
</Icon>
<Title ouiaId="ak-delete-header" headingLevel="h5" size="2xl">
<Title ouiaId="ak-delete-header" key="delete-ak-title" headingLevel="h5" size="2xl">
{__('Delete activation key?')}
</Title>
</Flex>,
Expand Down
21 changes: 11 additions & 10 deletions webpack/scenes/ActivationKeys/Details/components/EditModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,19 @@ const EditModal = ({ akDetails, akId }) => {
name, description, maxHosts, unlimitedHosts, usageCount,
} = akDetails;

const initialMaxHosts = maxHosts || '';

const [nameValue, setNameValue] = useState(name);
const [descriptionValue, setDescriptionValue] = useState(description);
const [maxHostsValue, setMaxHostsValue] = useState(maxHosts);
const [maxHostsValue, setMaxHostsValue] = useState(initialMaxHosts);
const [isUnlimited, setUnlimited] = useState(unlimitedHosts);

useEffect(() => {
setNameValue(name);
setDescriptionValue(description);
setMaxHostsValue(maxHosts);
setMaxHostsValue(initialMaxHosts);
setUnlimited(unlimitedHosts);
}, [name, description, maxHosts, unlimitedHosts]);
}, [name, description, initialMaxHosts, unlimitedHosts]);


const [isModalOpen, setModalOpen] = useState(false);
Expand All @@ -53,7 +55,7 @@ const EditModal = ({ akDetails, akId }) => {
{
name: nameValue,
description: descriptionValue,
max_hosts: maxHostsValue,
max_hosts: maxHostsValue || (usageCount !== 0 ? usageCount : usageCount + 1),
unlimited_hosts: isUnlimited,
},
));
Expand All @@ -80,11 +82,11 @@ const EditModal = ({ akDetails, akId }) => {
};

const onMinus = () => {
maxHostsValue(oldValue => (oldValue || 0) - 1);
setMaxHostsValue(oldValue => (oldValue || 0) - 1);
};
const onChange = (event) => {
let newValue = (event.target.value === '' ? event.target.value : Math.round(+event.target.value));
if (newValue < 1) {
if (newValue < 1 && newValue !== '') {
newValue = 1;
}
setMaxHostsValue(newValue);
Expand All @@ -95,12 +97,12 @@ const EditModal = ({ akDetails, akId }) => {

const handleCheckBox = () => {
setUnlimited(prevUnlimited => !prevUnlimited);
setMaxHostsValue(usageCount);
setMaxHostsValue(usageCount > 0 ? usageCount : usageCount + 1);
};

return (
<>
<Button ouiaId="ak-edit-button" variant="secondary" onClick={handleModalToggle}>
<Button ouiaId="ak-edit-button" aria-label="edit-button" variant="secondary" onClick={handleModalToggle}>
{__('Edit')}
</Button>
<Modal
Expand Down Expand Up @@ -138,7 +140,7 @@ const EditModal = ({ akDetails, akId }) => {
<StackItem>
<NumberInput
value={maxHostsValue}
min={0}
min={1}
onMinus={onMinus}
onChange={onChange}
onPlus={onPlus}
Expand Down Expand Up @@ -168,7 +170,6 @@ const EditModal = ({ akDetails, akId }) => {
id="ak-description"
type="text"
placeholder={__('Description')}
defaultValue={descriptionValue}
value={descriptionValue}
onChange={handleDescriptionInputChange}
/>
Expand Down

0 comments on commit 9c33b9a

Please sign in to comment.