From 2dc04d99cf5e4967f8f6ab6bdad5dacc3f9f509e Mon Sep 17 00:00:00 2001 From: Trevor Allison Date: Thu, 29 Jun 2023 15:42:43 +0000 Subject: [PATCH] Fixes #36610 - AK Details: Add system purpose card --- .../SystemPurposeCard/SystemPurposeActions.js | 36 +++- .../SystemPurposeCard/SystemPurposeCard.js | 172 +++++++++++------- .../SystemPurposeCard/SystemPurposeCard.scss | 6 + .../SystemPurposeConstants.js | 2 + .../SystemPurposeEditModal.js | 71 +++++--- .../__tests__/SystemPurposeCard.test.js | 29 ++- .../Details/ActivationKeyDetails.js | 9 + 7 files changed, 221 insertions(+), 104 deletions(-) diff --git a/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeActions.js b/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeActions.js index b3765f5ba13..c2471082c72 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,20 +14,28 @@ export const getOrganization = ({ orgId }) => ({ }, }); -export const getAvailableReleaseVersions = ({ hostId }) => ({ +export const getHostAvailableReleaseVersions = ({ id }) => ({ type: 'API_GET', payload: { - key: `${AVAILABLE_RELEASE_VERSIONS}_${hostId}`, - url: foremanApi.getApiUrl(`/hosts/${hostId}/subscriptions/available_release_versions`), + key: `${AVAILABLE_RELEASE_VERSIONS}_${id}`, + url: foremanApi.getApiUrl(`/hosts/${id}/subscriptions/available_release_versions`), }, }); -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..6d1a322eddf 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, @@ -10,8 +11,8 @@ import { CardBody, DescriptionList, DescriptionListGroup, - DescriptionListDescription, - DescriptionListTerm, + DescriptionListDescription as Dd, + DescriptionListTerm as Dt, Flex, FlexItem, GridItem, @@ -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,18 @@ 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 sysPurposeCardType = hostDetails?.id ? 'host' : 'ak'; + const isAKType = sysPurposeCardType === 'ak'; + const isHostType = sysPurposeCardType === 'host'; + const details = isHostType ? hostDetails : akDetails; + const requiredPermission = isHostType ? '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 +50,26 @@ 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 = isHostType ? selectHostDetailsStatus : selectAKDetailsStatus; + const detailsStatus = useSelector(statusSelector); + const dataIsLoading = detailsStatus === STATUS.PENDING; const [editing, setEditing] = useState(false); - if (!hostDetails?.id) { + + const [isExpanded, setIsExpanded] = React.useState(true); + + const onExpand = () => { + setIsExpanded(!isExpanded); + }; + const cardHeaderProps = { + toggleButtonProps: { id: 'sys-purpose-toggle', 'aria-label': 'sys-purpose-toggle' }, + }; + if (isAKType) cardHeaderProps.onExpand = onExpand; + + if (!details?.id) { return ( @@ -57,12 +79,11 @@ const SystemPurposeCard = ({ hostDetails }) => { ); } - if (hostIsNotRegistered({ hostDetails })) return null; - + if (isHostType && 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 +193,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/SystemPurposeCard.scss b/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeCard.scss index 6dd4963a90f..56c23cdf6bf 100644 --- a/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeCard.scss +++ b/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeCard.scss @@ -3,4 +3,10 @@ .pf-c-label { font-size: smaller; } +} + +#system-purpose-card { + .pf-c-card__header-toggle { + margin-top: -2px; + } } \ No newline at end of file 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/components/extensions/HostDetails/Cards/SystemPurposeCard/__tests__/SystemPurposeCard.test.js b/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/__tests__/SystemPurposeCard.test.js index 6acbd3bfdb8..fc30b09d513 100644 --- a/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/__tests__/SystemPurposeCard.test.js +++ b/webpack/components/extensions/HostDetails/Cards/SystemPurposeCard/__tests__/SystemPurposeCard.test.js @@ -4,6 +4,7 @@ import HOST_DETAILS from '../../../HostDetailsConstants'; import SystemPurposeCard from '../SystemPurposeCard'; import katelloApi, { foremanApi } from '../../../../../../services/api'; import { assertNockRequest, nockInstance } from '../../../../../../test-utils/nockWrapper'; +import { ACTIVATION_KEY } from '../../../../../../scenes/ActivationKeys/Details/ActivationKeyConstants'; const organizationDetails = katelloApi.getApiUrl('/organizations/1'); const availableReleaseVersions = foremanApi.getApiUrl('/hosts/1/subscriptions/available_release_versions'); @@ -24,11 +25,17 @@ const baseHostDetails = { }, }; -const renderOptions = () => ({ - apiNamespace: HOST_DETAILS, +const akHostDetails = { + ...baseHostDetails, + subscription_facet_attributes: undefined, + ...baseHostDetails.subscription_facet_attributes, +}; + +const renderOptions = (apiNamespace = HOST_DETAILS) => ({ + apiNamespace, initialState: { API: { - HOST_DETAILS: { + [apiNamespace]: { response: { id: 1, name: 'test-host', @@ -40,7 +47,7 @@ const renderOptions = () => ({ }, }); -test('shows system purpose details', async (done) => { +test('shows system purpose details for a host', async (done) => { const orgScope = nockInstance .get(organizationDetails) .reply(200, { @@ -49,7 +56,6 @@ test('shows system purpose details', async (done) => { const availableReleaseVersionsScope = nockInstance .get(availableReleaseVersions) .reply(200, []); - const { getByText } = renderWithRedux(, renderOptions()); expect(getByText('Red Hat Enterprise Linux Server')).toBeInTheDocument(); @@ -63,6 +69,19 @@ test('shows system purpose details', async (done) => { assertNockRequest(availableReleaseVersionsScope, done); }); +test('shows system purpose details for an activation key', () => { + const { getByText } = renderWithRedux( + , + renderOptions(`${ACTIVATION_KEY}_1`), + ); + expect(getByText('Red Hat Enterprise Linux Server')).toBeInTheDocument(); + expect(getByText('Production')).toBeInTheDocument(); + expect(getByText('Premium')).toBeInTheDocument(); + expect(getByText('Addon1')).toBeInTheDocument(); + expect(getByText('Addon2')).toBeInTheDocument(); + expect(getByText('8')).toBeInTheDocument(); +}); + test('shows edit button for a user with edit_hosts permission', async (done) => { const orgScope = nockInstance diff --git a/webpack/scenes/ActivationKeys/Details/ActivationKeyDetails.js b/webpack/scenes/ActivationKeys/Details/ActivationKeyDetails.js index 2428033276d..87911bff4d7 100644 --- a/webpack/scenes/ActivationKeys/Details/ActivationKeyDetails.js +++ b/webpack/scenes/ActivationKeys/Details/ActivationKeyDetails.js @@ -25,6 +25,7 @@ import { Flex, FlexItem, Panel, + PageSection, } from '@patternfly/react-core'; import './ActivationKeyDetails.scss'; import EditModal from './components/EditModal'; @@ -32,6 +33,7 @@ import DeleteMenu from './components/DeleteMenu'; import { getActivationKey } from './ActivationKeyActions'; import DeleteModal from './components/DeleteModal'; import InactiveText from '../../ContentViews/components/InactiveText'; +import SystemPurposeCard from '../../../components/extensions/HostDetails/Cards/SystemPurposeCard/SystemPurposeCard'; const ActivationKeyDetails = ({ match }) => { const dispatch = useDispatch(); @@ -106,6 +108,13 @@ const ActivationKeyDetails = ({ match }) => { } + + + + + + + );