Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #36576 - Add Activation Key details top bar #10637

Merged
merged 8 commits into from
Aug 3, 2023
Merged
31 changes: 31 additions & 0 deletions webpack/scenes/ActivationKeys/Details/ActivationKeyActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { translate as __ } from 'foremanReact/common/I18n';
import { APIActions, API_OPERATIONS, put, get } from 'foremanReact/redux/API';
import { errorToast } from '../../Tasks/helpers';
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: katelloApi.getApiUrl(`/activation_keys/${akId}`),
});

export const putActivationKey = (akId, params, refreshActivationKeyDetails) => put({
type: API_OPERATIONS.PUT,
key: `ALTER_ACTIVATION_KEY_${akId}`,
url: katelloApi.getApiUrl(`/activation_keys/${akId}`),
successToast: () => __('Activation key details updated'),
errorToast,
params,
handleSuccess: refreshActivationKeyDetails,
});

export const deleteActivationKey = akId => APIActions.delete({
type: API_OPERATIONS.DELETE,
key: `${ACTIVATION_KEY}_${akId}`,
url: katelloApi.getApiUrl(`/activation_keys/${akId}`),
successToast: () => __('Activation key deleted'),
errorToast,
});

export default getActivationKey;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const ACTIVATION_KEY = 'ACTIVATION_KEY';

export default ACTIVATION_KEY;
108 changes: 106 additions & 2 deletions webpack/scenes/ActivationKeys/Details/ActivationKeyDetails.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,114 @@
import React from 'react';
import React, {
useEffect,
useState,
} from 'react';
import {
useDispatch,
useSelector,
} from 'react-redux';
import { translate as __ } from 'foremanReact/common/I18n';
import PropTypes from 'prop-types';
import { propsToCamelCase } from 'foremanReact/common/helpers';
import { selectAPIResponse } from 'foremanReact/redux/API/APISelectors';
import {
Title,
TextContent,
Text,
TextVariants,
Breadcrumb,
BreadcrumbItem,
Grid,
GridItem,
Label,
Split,
SplitItem,
Flex,
FlexItem,
Panel,
} from '@patternfly/react-core';
import './ActivationKeyDetails.scss';
import EditModal from './components/EditModal';
import DeleteMenu from './components/DeleteMenu';
import { getActivationKey } from './ActivationKeyActions';
import DeleteModal from './components/DeleteModal';

const ActivationKeyDetails = ({ match }) => <div>ActivationKeyDetails { match?.params?.id } </div>;
const ActivationKeyDetails = ({ match }) => {
const dispatch = useDispatch();
const akId = match?.params?.id;
const akDetailsResponse = useSelector(state => selectAPIResponse(state, `ACTIVATION_KEY_${akId}`));
const akDetails = propsToCamelCase(akDetailsResponse);
useEffect(() => {
if (akId) { // TODO add back akNotLoaded condition
dispatch(getActivationKey(akId));
trev-allison03 marked this conversation as resolved.
Show resolved Hide resolved
}
}, [akId, dispatch]);

const [isModalOpen, setModalOpen] = useState(false);
const handleModalToggle = () => {
trev-allison03 marked this conversation as resolved.
Show resolved Hide resolved
setModalOpen(!isModalOpen);
};

return (
<div >
<Panel className="ak-details-header">
<div className="breadcrumb-bar-pf4">
<Breadcrumb ouiaId="ak-breadcrumbs" className="breadcrumb-display">
<BreadcrumbItem className="breadcrumb-list" to="/activation_keys">
{__('Activation keys')}
</BreadcrumbItem>
<BreadcrumbItem to="#" isActive>
{akDetails.name}
</BreadcrumbItem>
</Breadcrumb>
</div>
<Grid>
<GridItem span={8} className="ak-name-wrapper">
<Flex justifyContent={{ default: 'jusifyContentSpaceBetween' }} alignItems={{ default: 'alignItemsCenter' }}>
<FlexItem>
<Title ouiaId="ak-title" headingLevel="h5" size="2xl" className="ak-name-truncate">
{akDetails.name}
</Title>
</FlexItem>
<FlexItem>
<Split hasGutter style={{ display: 'inline-flex' }}>
<SplitItem>
<Label>
{akDetails.usageCount ? akDetails.usageCount : 0}/{akDetails.unlimitedHosts ? __('Unlimited') : akDetails.maxHosts}
</Label>
</SplitItem>
</Split>
</FlexItem>
</Flex>
</GridItem>
<GridItem offset={8} span={4}>
<Flex>
<FlexItem align={{ default: 'align-right' }}>
<Split>
<SplitItem>
<EditModal akDetails={akDetails} akId={akId} />
</SplitItem>
<DeleteMenu handleModalToggle={handleModalToggle} akId={akId} />
</Split>
</FlexItem>
</Flex>
</GridItem>
</Grid>
<div className="ak-details-description">
<TextContent>
<Text ouiaId="ak-description" component={TextVariants.p}>
{akDetails.description ? akDetails.description : <span style={{ color: '#c1c1c1' }}>{__('No description provided')}</span>}
</Text>
trev-allison03 marked this conversation as resolved.
Show resolved Hide resolved
</TextContent>
</div>
</Panel>
<DeleteModal isModalOpen={isModalOpen} handleModalToggle={handleModalToggle} akId={akId} />
</div>
);
};

export default ActivationKeyDetails;


ActivationKeyDetails.propTypes = {
match: PropTypes.shape({
params: PropTypes.shape({
Expand Down
37 changes: 37 additions & 0 deletions webpack/scenes/ActivationKeys/Details/ActivationKeyDetails.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.ak-details-header {
margin: 0 24px 16px;
padding-top: 16px;
}

.ak-details-description {
padding-top: 16px;
}

.breadcrumb-bar-pf4 {
margin: 0 0 16px;
}

.breadcrumb-display {
display: block;
}

.breadcrumb-list {
display: flex;
flex-wrap: wrap;
align-items: center;
}

.ak-name-truncate {
text-overflow: ellipsis;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
display: inline-block;
margin-right: 16px
}

.ak-name-wrapper {
display: inline-flex;
max-width: 60%;
margin-right: 8px;
}
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);
});
Loading
Loading