diff --git a/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeActions.js b/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeActions.js index b3765f5ba13..197a554dcd8 100644 --- a/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeActions.js +++ b/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeActions.js @@ -2,7 +2,8 @@ import { translate as __ } from 'foremanReact/common/I18n'; import { API_OPERATIONS, put } from 'foremanReact/redux/API'; import api, { foremanApi } from '../../../../../services/api'; import HOST_DETAILS_KEY from '../../HostDetailsConstants'; -import { ORGANIZATION, AVAILABLE_RELEASE_VERSIONS } from './SystemPurposeConstants'; +import { ACTIVATION_KEY } from '../../../../../scenes/ActivationKeys/Details/ActivationKeyConstants'; +import { ORGANIZATION, AVAILABLE_RELEASE_VERSIONS, RELEASES } from './SystemPurposeConstants'; import { errorToast } from '../../../../../scenes/Tasks/helpers'; export const getOrganization = ({ orgId }) => ({ @@ -13,7 +14,7 @@ export const getOrganization = ({ orgId }) => ({ }, }); -export const getAvailableReleaseVersions = ({ hostId }) => ({ +export const getHostAvailableReleaseVersions = ({ hostId }) => ({ type: 'API_GET', payload: { key: `${AVAILABLE_RELEASE_VERSIONS}_${hostId}`, @@ -21,12 +22,20 @@ export const getAvailableReleaseVersions = ({ hostId }) => ({ }, }); -export const updateSystemPurposeAttributes = ({ hostId, attributes, refreshHostDetails }) => put({ +export const getAKAvailableReleaseVersions = ({ id }) => ({ + type: API_OPERATIONS.GET, + payload: { + key: `${RELEASES}_${id}`, + url: api.getApiUrl(`/activation_keys/${id}/releases`), + }, +}); + +export const updateHostSysPurposeAttributes = ({ id, attributes, refreshHostDetails }) => put({ type: API_OPERATIONS.PUT, key: HOST_DETAILS_KEY, - url: foremanApi.getApiUrl(`/hosts/${hostId}`), + url: foremanApi.getApiUrl(`/hosts/${id}`), params: { - id: hostId, + id, host: { subscription_facet_attributes: attributes, }, @@ -35,3 +44,16 @@ export const updateSystemPurposeAttributes = ({ hostId, attributes, refreshHostD errorToast, handleSuccess: refreshHostDetails, }); + +export const updateAKSysPurposeAttributes = ({ id, attributes, refreshAKDetails }) => put({ + type: API_OPERATIONS.PUT, + key: ACTIVATION_KEY, + url: api.getApiUrl(`/activation_keys/${id}`), + params: { + id, + activation_key: attributes, + }, + successToast: () => __('System purpose attributes updated'), + errorToast, + handleSuccess: refreshAKDetails, +}); diff --git a/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeCard.js b/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeCard.js index 86fb9258a8b..676eb4a7576 100644 --- a/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeCard.js +++ b/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeCard.js @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { STATUS } from 'foremanReact/constants'; +import { selectAPIStatus } from 'foremanReact/redux/API/APISelectors'; import { Button, Card, @@ -20,6 +21,7 @@ import { ListItem, Tooltip, Skeleton, + CardExpandableContent, } from '@patternfly/react-core'; import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; import { translate as __ } from 'foremanReact/common/I18n'; @@ -29,13 +31,16 @@ import SystemPurposeEditModal from './SystemPurposeEditModal'; import { selectHostDetailsStatus } from '../../HostDetailsSelectors'; import { hasRequiredPermissions, hostIsNotRegistered } from '../../hostDetailsHelpers'; -const SystemPurposeCard = ({ hostDetails }) => { - const showEditButton = hasRequiredPermissions(['edit_hosts'], hostDetails?.permissions); - const { organization_id: orgId, name: hostName } = hostDetails; - const subscriptionFacetAttributes = hostDetails?.subscription_facet_attributes; +const SystemPurposeCard = ({ hostDetails, akDetails }) => { + const details = hostDetails?.id ? hostDetails : akDetails; + const sysPurposeCardType = details?.subscription_facet_attributes ? 'host' : 'ak'; + const requiredPermission = sysPurposeCardType === 'host' ? 'edit_hosts' : 'edit_activation_keys'; + const showEditButton = hasRequiredPermissions([requiredPermission], details?.permissions); + const { organization_id: orgId, name: hostName } = details; + const subscriptionFacetAttributes = details?.subscription_facet_attributes; const { purposeRole, purposeUsage, purposeAddons, releaseVersion, serviceLevel, - } = propsToCamelCase(subscriptionFacetAttributes ?? {}); + } = propsToCamelCase((subscriptionFacetAttributes || details) ?? {}); const sysPurposeProps = { purposeRole, purposeUsage, @@ -43,11 +48,30 @@ const SystemPurposeCard = ({ hostDetails }) => { releaseVersion, serviceLevel, }; - const hostDetailsStatus = useSelector(selectHostDetailsStatus); - const dataIsLoading = hostDetailsStatus === STATUS.PENDING; + + const selectAKDetailsStatus = state => + selectAPIStatus(state, `ACTIVATION_KEY_${details.id}`) ?? STATUS.PENDING; + + const statusSelector = sysPurposeCardType === 'host' ? selectHostDetailsStatus : selectAKDetailsStatus; + const detailsStatus = useSelector(statusSelector); + const dataIsLoading = detailsStatus === STATUS.PENDING; const [editing, setEditing] = useState(false); - if (!hostDetails?.id) { + + const [isExpanded, setIsExpanded] = React.useState(false); + + const onExpand = () => { + setIsExpanded(!isExpanded); + }; + + const cardHeaderProps = { + toggleButtonProps: { id: 'sys-purpose-toggle', 'aria-label': 'sys-purpose-toggle' }, + }; + if (sysPurposeCardType === 'ak') { + cardHeaderProps.onExpand = onExpand; + } + + if (!details?.id) { return ( @@ -57,12 +81,12 @@ const SystemPurposeCard = ({ hostDetails }) => { ); } - if (hostIsNotRegistered({ hostDetails })) return null; + if (sysPurposeCardType === 'host' && hostIsNotRegistered({ hostDetails: details })) return null; return ( - - + + { } - - - - {__('Role')} - - {dataIsLoading ? : purposeRole} - - {__('SLA')} - - {serviceLevel && (dataIsLoading ? : ( - - ))} - - {__('Usage type')} - - {purposeUsage && (dataIsLoading ? : ( - - ))} - - {__('Release version')} - - {dataIsLoading ? : releaseVersion} - - {!!purposeAddons?.length && ( - <> - {__('Add-ons')} - {dataIsLoading ? : ( - - - {purposeAddons.map(addon => ( - {addon} - ))} - - - )} - - ) - } - - - {showEditButton && ( - setEditing(false)} - hostName={hostName} - hostId={hostDetails.id} - {...sysPurposeProps} - /> - )} - + + + + + {__('Role')} + + {dataIsLoading ? : purposeRole} + + {__('SLA')} + + {serviceLevel && (dataIsLoading ? : ( + + ))} + + {__('Usage type')} + + {purposeUsage && (dataIsLoading ? : ( + + ))} + + {__('Release version')} + + {dataIsLoading ? : releaseVersion} + + {!!purposeAddons?.length && ( + <> + {__('Add-ons')} + {dataIsLoading ? : ( + + + {purposeAddons.map(addon => ( + {addon} + ))} + + + )} + + ) + } + + + {showEditButton && ( + setEditing(false)} + name={hostName} + id={details.id} + {...sysPurposeProps} + type={sysPurposeCardType} + /> + )} + + ); @@ -167,10 +194,24 @@ SystemPurposeCard.propTypes = { edit_hosts: PropTypes.bool, }), }), + akDetails: PropTypes.shape({ + name: PropTypes.string, + organization_id: PropTypes.number, + id: PropTypes.number, + purpose_usage: PropTypes.string, + purpose_role: PropTypes.string, + release_version: PropTypes.string, + service_level: PropTypes.string, + purpose_addons: PropTypes.arrayOf(PropTypes.string), + permissions: PropTypes.shape({ + edit_activation_keys: PropTypes.bool, + }), + }), }; SystemPurposeCard.defaultProps = { hostDetails: {}, + akDetails: {}, }; export default SystemPurposeCard; diff --git a/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeConstants.js b/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeConstants.js index 7090b90baf9..a79167664ef 100644 --- a/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeConstants.js +++ b/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeConstants.js @@ -4,3 +4,5 @@ export const defaultServiceLevels = ['Self-Support', 'Standard', 'Premium']; export const ORGANIZATION = 'ORGANIZATION'; export const AVAILABLE_RELEASE_VERSIONS = 'AVAILABLE_RELEASE_VERSIONS'; + +export const RELEASES = 'RELEASES'; diff --git a/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeEditModal.js b/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeEditModal.js index 8d747aa0d03..d60aab0d577 100644 --- a/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeEditModal.js +++ b/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeEditModal.js @@ -18,13 +18,14 @@ import { import { FormattedMessage } from 'react-intl'; import { translate as __ } from 'foremanReact/common/I18n'; import { selectOrganizationStatus, selectOrganization, selectAvailableReleaseVersions, selectAvailableReleaseVersionsStatus } from './SystemPurposeSelectors'; -import { getAvailableReleaseVersions, getOrganization, updateSystemPurposeAttributes } from './SystemPurposeActions'; +import { getHostAvailableReleaseVersions, getAKAvailableReleaseVersions, getOrganization, updateHostSysPurposeAttributes, updateAKSysPurposeAttributes } from './SystemPurposeActions'; import HOST_DETAILS_KEY from '../../HostDetailsConstants'; import { defaultUsages, defaultRoles, defaultServiceLevels } from './SystemPurposeConstants'; +import { getActivationKey } from '../../../../../scenes/ActivationKeys/Details/ActivationKeyActions'; const SystemPurposeEditModal = ({ - closeModal, hostName, purposeRole, purposeUsage, purposeAddons, - serviceLevel, releaseVersion, isOpen, orgId, hostId, + closeModal, name, purposeRole, purposeUsage, purposeAddons, + serviceLevel, releaseVersion, isOpen, orgId, id, type, }) => { const initialPurposeRole = purposeRole ?? ''; const initialServiceLevel = serviceLevel ?? ''; @@ -58,7 +59,7 @@ const SystemPurposeEditModal = ({ const availableReleaseVersionsStatus = useSelector(state => selectAvailableReleaseVersionsStatus(state, orgId)); const availableReleaseVersions = useSelector(state => - selectAvailableReleaseVersions(state, hostId))?.results ?? []; + selectAvailableReleaseVersions(state, id))?.results ?? []; useEffect(() => { if (orgId && orgStatus !== STATUS.RESOLVED) { dispatch(getOrganization({ orgId })); @@ -66,10 +67,12 @@ const SystemPurposeEditModal = ({ }, [orgId, orgStatus, dispatch]); useEffect(() => { - if (hostId && availableReleaseVersionsStatus !== STATUS.RESOLVED) { - dispatch(getAvailableReleaseVersions({ hostId })); + if (type === 'host' && id && availableReleaseVersionsStatus !== STATUS.RESOLVED) { + dispatch(getHostAvailableReleaseVersions({ id })); + } else if (type === 'ak' && id) { + dispatch(getAKAvailableReleaseVersions({ id })); } - }, [hostId, availableReleaseVersionsStatus, dispatch]); + }, [type, id, availableReleaseVersionsStatus, dispatch]); const toggleAddonSelect = isOpenState => setAddonSelectOpen(isOpenState); @@ -88,7 +91,7 @@ const SystemPurposeEditModal = ({ type: 'API_GET', payload: { key: HOST_DETAILS_KEY, - url: `/api/hosts/${hostName}`, + url: `/api/hosts/${name}`, }, }); @@ -133,18 +136,33 @@ const SystemPurposeEditModal = ({ closeModal(); const optionsToValue = (options, stateValue) => options.find(option => option.value === stateValue)?.value; - dispatch(updateSystemPurposeAttributes({ - hostId, - attributes: { - autoheal: true, - purpose_role: optionsToValue(roleOptions, selectedRole), - purpose_usage: optionsToValue(usageOptions, selectedUsage), - purpose_addons: selectedAddons, - release_version: optionsToValue(releaseVersionOptions, selectedReleaseVersion), - service_level: optionsToValue(serviceLevelOptions, selectedServiceLevel), - }, - refreshHostDetails, - })); + if (type === 'host') { + dispatch(updateHostSysPurposeAttributes({ + id, + attributes: { + autoheal: true, + purpose_role: optionsToValue(roleOptions, selectedRole), + purpose_usage: optionsToValue(usageOptions, selectedUsage), + purpose_addons: selectedAddons, + release_version: optionsToValue(releaseVersionOptions, selectedReleaseVersion), + service_level: optionsToValue(serviceLevelOptions, selectedServiceLevel), + }, + refreshHostDetails, + })); + } else { + dispatch(updateAKSysPurposeAttributes({ + id, + attributes: { + autoheal: true, + purpose_role: optionsToValue(roleOptions, selectedRole), + purpose_usage: optionsToValue(usageOptions, selectedUsage), + purpose_addons: selectedAddons, + release_version: optionsToValue(releaseVersionOptions, selectedReleaseVersion), + service_level: optionsToValue(serviceLevelOptions, selectedServiceLevel), + }, + refreshAKDetails: () => dispatch(getActivationKey(id)), + })); + } }; const handleCancel = () => { @@ -180,9 +198,9 @@ const SystemPurposeEditModal = ({ {hostName}, + name: {name}, }} />
@@ -288,7 +306,7 @@ export default SystemPurposeEditModal; SystemPurposeEditModal.propTypes = { closeModal: PropTypes.func.isRequired, - hostName: PropTypes.string, + name: PropTypes.string, purposeRole: PropTypes.string.isRequired, purposeUsage: PropTypes.string.isRequired, purposeAddons: PropTypes.arrayOf(PropTypes.string).isRequired, @@ -296,12 +314,13 @@ SystemPurposeEditModal.propTypes = { releaseVersion: PropTypes.string, isOpen: PropTypes.bool.isRequired, orgId: PropTypes.number, - hostId: PropTypes.number, + id: PropTypes.number, + type: PropTypes.string.isRequired, }; SystemPurposeEditModal.defaultProps = { - hostName: '', + name: '', orgId: null, - hostId: null, + id: null, releaseVersion: '', }; diff --git a/webpack/scenes/ActivationKeys/Details/ActivationKeyActions.js b/webpack/scenes/ActivationKeys/Details/ActivationKeyActions.js new file mode 100644 index 00000000000..3ebe37284be --- /dev/null +++ b/webpack/scenes/ActivationKeys/Details/ActivationKeyActions.js @@ -0,0 +1,30 @@ +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) => put({ + type: API_OPERATIONS.PUT, + key: `${ACTIVATION_KEY}_${akId}`, + url: katelloApi.getApiUrl(`/activation_keys/${akId}`), + successToast: () => __('Activation key details updated'), + errorToast, + params, +}); + +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; diff --git a/webpack/scenes/ActivationKeys/Details/ActivationKeyConstants.js b/webpack/scenes/ActivationKeys/Details/ActivationKeyConstants.js new file mode 100644 index 00000000000..7a0ad233c9b --- /dev/null +++ b/webpack/scenes/ActivationKeys/Details/ActivationKeyConstants.js @@ -0,0 +1,3 @@ +export const ACTIVATION_KEY = 'ACTIVATION_KEY'; + +export default ACTIVATION_KEY; diff --git a/webpack/scenes/ActivationKeys/Details/ActivationKeyDetails.js b/webpack/scenes/ActivationKeys/Details/ActivationKeyDetails.js index 330f2ce70d2..5a36a30f9dc 100644 --- a/webpack/scenes/ActivationKeys/Details/ActivationKeyDetails.js +++ b/webpack/scenes/ActivationKeys/Details/ActivationKeyDetails.js @@ -1,10 +1,116 @@ -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'; +import AKBody from './components/AKBody'; -const ActivationKeyDetails = ({ match }) =>
ActivationKeyDetails { match?.params?.id }
; +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)); + } + }, [akId, dispatch]); + + const [isModalOpen, setModalOpen] = useState(false); + const handleModalToggle = () => { + setModalOpen(!isModalOpen); + }; + + return ( +
+ +
+ + + {__('Activation keys')} + + + {akDetails.name} + + +
+ + + + + + {akDetails.name} + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + {akDetails.description ? akDetails.description : {__('No description provided')}} + + +
+
+ + +
+ ); +}; export default ActivationKeyDetails; + ActivationKeyDetails.propTypes = { match: PropTypes.shape({ params: PropTypes.shape({ diff --git a/webpack/scenes/ActivationKeys/Details/ActivationKeyDetails.scss b/webpack/scenes/ActivationKeys/Details/ActivationKeyDetails.scss new file mode 100644 index 00000000000..fc5858895ca --- /dev/null +++ b/webpack/scenes/ActivationKeys/Details/ActivationKeyDetails.scss @@ -0,0 +1,51 @@ +.ak-details-tab { + padding: 0px 24px; +} + +.masonry-item { + --pf-l-grid__item--GridColumnEnd: span 12; +} + +.ak-details-tab-page { + padding: 20px 24px; + padding-bottom: 100%; + background: var(--pf-global--BackgroundColor--200); +} + +.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; +} \ No newline at end of file diff --git a/webpack/scenes/ActivationKeys/Details/ActivationKeyDetailsSelectors.js b/webpack/scenes/ActivationKeys/Details/ActivationKeyDetailsSelectors.js new file mode 100644 index 00000000000..ae858a5eb23 --- /dev/null +++ b/webpack/scenes/ActivationKeys/Details/ActivationKeyDetailsSelectors.js @@ -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); diff --git a/webpack/scenes/ActivationKeys/Details/__tests__/activationKeyDetails.test.js b/webpack/scenes/ActivationKeys/Details/__tests__/activationKeyDetails.test.js new file mode 100644 index 00000000000..a8ecc262172 --- /dev/null +++ b/webpack/scenes/ActivationKeys/Details/__tests__/activationKeyDetails.test.js @@ -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(, 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(, 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(, 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(, 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(, 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(, 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); +}); diff --git a/webpack/scenes/ActivationKeys/Details/components/AKBody.js b/webpack/scenes/ActivationKeys/Details/components/AKBody.js new file mode 100644 index 00000000000..39d447bcd88 --- /dev/null +++ b/webpack/scenes/ActivationKeys/Details/components/AKBody.js @@ -0,0 +1,33 @@ +import React from 'react'; +import { + Grid, + GridItem, +} from '@patternfly/react-core'; +import PropTypes from 'prop-types'; +import '../ActivationKeyDetails.scss'; +import SystemPurposeCard from '../../../../components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeCard'; + + +const AKBody = ({ akDetails }) => ( + + + + + +); + +AKBody.propTypes = { + akDetails: PropTypes.shape({ + name: PropTypes.string, + maxHosts: PropTypes.number, + description: PropTypes.string, + unlimitedHosts: PropTypes.bool, + usageCount: PropTypes.number, + }), +}; + +AKBody.defaultProps = { + akDetails: {}, +}; + +export default AKBody; diff --git a/webpack/scenes/ActivationKeys/Details/components/DeleteMenu.js b/webpack/scenes/ActivationKeys/Details/components/DeleteMenu.js new file mode 100644 index 00000000000..a778a84491a --- /dev/null +++ b/webpack/scenes/ActivationKeys/Details/components/DeleteMenu.js @@ -0,0 +1,77 @@ +import React, { + useState, +} from 'react'; +import PropTypes from 'prop-types'; +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'; + +const DeleteMenu = ({ handleModalToggle, akId }) => { + const [isOpen, setIsOpen] = useState(false); + const onToggle = (isOpenValue) => { + setIsOpen(isOpenValue); + }; + const onFocus = () => { + const element = document.getElementById('toggle-kebab'); + element.focus(); + }; + const onSelect = () => { + setIsOpen(false); + onFocus(); + }; + const dropdownItems = [ + + + + + + + {__('Delete')} + + + , + + + + + + + {__('Legacy UI')} + + + ]; + return ( + } + isOpen={isOpen} + isPlain + dropdownItems={dropdownItems} + /> + ); +}; + +DeleteMenu.propTypes = { + handleModalToggle: PropTypes.func, + akId: PropTypes.string.isRequired, +}; + +DeleteMenu.defaultProps = { + handleModalToggle: noop, +}; + +export default DeleteMenu; + diff --git a/webpack/scenes/ActivationKeys/Details/components/DeleteModal.js b/webpack/scenes/ActivationKeys/Details/components/DeleteModal.js new file mode 100644 index 00000000000..72b570d9838 --- /dev/null +++ b/webpack/scenes/ActivationKeys/Details/components/DeleteModal.js @@ -0,0 +1,63 @@ +import React from 'react'; +import { + useDispatch, +} from 'react-redux'; +import PropTypes from 'prop-types'; +import { noop } from 'foremanReact/common/helpers'; +import { translate as __ } from 'foremanReact/common/I18n'; +import { Modal, ModalVariant, Button, Icon, Title, Flex } from '@patternfly/react-core'; +import { ExclamationTriangleIcon } from '@patternfly/react-icons'; +import { deleteActivationKey } from '../ActivationKeyActions'; + +const DeleteModal = ({ isModalOpen, handleModalToggle, akId }) => { + const dispatch = useDispatch(); + + const handleDelete = () => { + dispatch(deleteActivationKey(akId)); + handleModalToggle(); + window.location.replace('/activation_keys'); + }; + + return ( + + + + + + {__('Delete activation key?')} + + , + ]} + isOpen={isModalOpen} + onClose={handleModalToggle} + actions={[ + , + , + ]} + > + {__('Activation Key will no longer be available for use. This operation cannot be undone.')} + + ); +}; + + +DeleteModal.propTypes = { + isModalOpen: PropTypes.bool, + handleModalToggle: PropTypes.func, + akId: PropTypes.string.isRequired, +}; + +DeleteModal.defaultProps = { + isModalOpen: false, + handleModalToggle: noop, +}; + +export default DeleteModal; diff --git a/webpack/scenes/ActivationKeys/Details/components/EditModal.js b/webpack/scenes/ActivationKeys/Details/components/EditModal.js new file mode 100644 index 00000000000..2337a092785 --- /dev/null +++ b/webpack/scenes/ActivationKeys/Details/components/EditModal.js @@ -0,0 +1,198 @@ +import React, { + useEffect, + useState, +} from 'react'; +import { + useDispatch, +} from 'react-redux'; +import PropTypes from 'prop-types'; +import { translate as __ } from 'foremanReact/common/I18n'; +import { + Modal, + ModalVariant, + Button, + Form, + FormGroup, + TextInput, + Checkbox, + NumberInput, + TextArea, + Stack, + StackItem, +} from '@patternfly/react-core'; +import { putActivationKey } from '../ActivationKeyActions'; + +const EditModal = ({ akDetails, akId }) => { + const dispatch = useDispatch(); + + const { + name, description, maxHosts, unlimitedHosts, usageCount, + } = akDetails; + + const initialMaxHosts = maxHosts || ''; + + const [nameValue, setNameValue] = useState(name); + const [descriptionValue, setDescriptionValue] = useState(description); + const [maxHostsValue, setMaxHostsValue] = useState(initialMaxHosts); + const [isUnlimited, setUnlimited] = useState(unlimitedHosts); + + useEffect(() => { + setNameValue(name); + setDescriptionValue(description); + setMaxHostsValue(initialMaxHosts); + setUnlimited(unlimitedHosts); + }, [name, description, initialMaxHosts, unlimitedHosts]); + + + const [isModalOpen, setModalOpen] = useState(false); + + const handleModalToggle = () => { + setModalOpen(!isModalOpen); + }; + const handleSave = () => { + dispatch(putActivationKey( + akId, + { + name: nameValue, + description: descriptionValue, + max_hosts: maxHostsValue || (usageCount !== 0 ? usageCount : usageCount + 1), + unlimited_hosts: isUnlimited, + }, + )); + handleModalToggle(); + }; + + const resetModalValues = () => { + setNameValue(name); + setDescriptionValue(description); + setMaxHostsValue(maxHosts); + setUnlimited(unlimitedHosts); + }; + + const handleClose = () => { + resetModalValues(); + handleModalToggle(); + }; + + const handleNameInputChange = (value) => { + setNameValue(value); + }; + const handleDescriptionInputChange = (value) => { + setDescriptionValue(value); + }; + + const onMinus = () => { + setMaxHostsValue(oldValue => (oldValue || 0) - 1); + }; + const onChange = (event) => { + let newValue = (event.target.value === '' ? event.target.value : Math.round(+event.target.value)); + if (newValue < 1 && newValue !== '') { + newValue = 1; + } + setMaxHostsValue(newValue); + }; + const onPlus = () => { + setMaxHostsValue(oldValue => (oldValue || 0) + 1); + }; + + const handleCheckBox = () => { + setUnlimited(prevUnlimited => !prevUnlimited); + setMaxHostsValue(usageCount > 0 ? usageCount : usageCount + 1); + }; + + return ( + <> + + + {__('Save')} + , + , + ]} + > + + + + + + + + + + + + + + + +