From 2f723ae2914caf82d357428eadda7c262a683a82 Mon Sep 17 00:00:00 2001 From: Trevor Allison Date: Thu, 29 Jun 2023 15:42:43 +0000 Subject: [PATCH 1/6] Fixes #36576 - Add Activation Key details top bar --- .../Details/ActivationKeyActions.js | 30 +++ .../Details/ActivationKeyConstants.js | 3 + .../Details/ActivationKeyDetails.js | 106 +++++++++- .../Details/ActivationKeyDetails.scss | 37 ++++ .../Details/components/DeleteMenu.js | 61 ++++++ .../Details/components/DeleteModal.js | 65 ++++++ .../Details/components/EditModal.js | 194 ++++++++++++++++++ 7 files changed, 494 insertions(+), 2 deletions(-) create mode 100644 webpack/scenes/ActivationKeys/Details/ActivationKeyActions.js create mode 100644 webpack/scenes/ActivationKeys/Details/ActivationKeyConstants.js create mode 100644 webpack/scenes/ActivationKeys/Details/ActivationKeyDetails.scss create mode 100644 webpack/scenes/ActivationKeys/Details/components/DeleteMenu.js create mode 100644 webpack/scenes/ActivationKeys/Details/components/DeleteModal.js create mode 100644 webpack/scenes/ActivationKeys/Details/components/EditModal.js diff --git a/webpack/scenes/ActivationKeys/Details/ActivationKeyActions.js b/webpack/scenes/ActivationKeys/Details/ActivationKeyActions.js new file mode 100644 index 00000000000..11e75b09e51 --- /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 api from '../../../services/api'; +import { ACTIVATION_KEY } from './ActivationKeyConstants'; + +export const getActivationKey = akId => get({ + type: API_OPERATIONS.GET, + key: `${ACTIVATION_KEY}_${akId}`, + url: api.getApiUrl(`/activation_keys/${akId}`), +}); + +export const putActivationKey = (akId, params) => put({ + type: API_OPERATIONS.PUT, + key: `${ACTIVATION_KEY}_${akId}`, + url: api.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: api.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..a679d543022 100644 --- a/webpack/scenes/ActivationKeys/Details/ActivationKeyDetails.js +++ b/webpack/scenes/ActivationKeys/Details/ActivationKeyDetails.js @@ -1,10 +1,112 @@ -import React from 'react'; +import React, { + useEffect, + useState, +} from 'react'; +import { + useDispatch, + useSelector, +} from 'react-redux'; 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 }) =>
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 : 'Description empty'} + + +
+
+ +
+ ); +}; 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..fb6d280ec49 --- /dev/null +++ b/webpack/scenes/ActivationKeys/Details/ActivationKeyDetails.scss @@ -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; +} \ No newline at end of file diff --git a/webpack/scenes/ActivationKeys/Details/components/DeleteMenu.js b/webpack/scenes/ActivationKeys/Details/components/DeleteMenu.js new file mode 100644 index 00000000000..032e3a91fa9 --- /dev/null +++ b/webpack/scenes/ActivationKeys/Details/components/DeleteMenu.js @@ -0,0 +1,61 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Dropdown, DropdownItem, KebabToggle, DropdownPosition } from '@patternfly/react-core'; +import { noop } from 'foremanReact/common/helpers'; + +const DeleteMenu = ({ handleModalToggle, akId }) => { + const [isOpen, setIsOpen] = React.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 + , + + Old Activation key Details Page + ]; + return ( + + } + isOpen={isOpen} + isPlain + dropdownItems={dropdownItems} + /> + + ); +}; + +DeleteMenu.propTypes = { + handleModalToggle: PropTypes.func, + akId: PropTypes.string, +}; + +DeleteMenu.defaultProps = { + handleModalToggle: noop, + akId: '', +}; + +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..5d309ba47c5 --- /dev/null +++ b/webpack/scenes/ActivationKeys/Details/components/DeleteModal.js @@ -0,0 +1,65 @@ +import React from 'react'; +import { + useDispatch, +} from 'react-redux'; +import PropTypes from 'prop-types'; +import { noop } from 'foremanReact/common/helpers'; +import { Modal, ModalVariant, Button, Icon, Title, Flex } from '@patternfly/react-core'; +import ExclamationTriangleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon'; +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, +}; + +DeleteModal.defaultProps = { + isModalOpen: false, + handleModalToggle: noop, + akId: '', +}; + +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..e6b7d8700c4 --- /dev/null +++ b/webpack/scenes/ActivationKeys/Details/components/EditModal.js @@ -0,0 +1,194 @@ +import React, { + useEffect, + useState, +} from 'react'; +import { + useDispatch, +} from 'react-redux'; +import PropTypes from 'prop-types'; +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, + } = akDetails; + + const [nameValue, setNameValue] = useState(name); + const [descriptionValue, setDescriptionValue] = useState(description); + const [maxHostsValue, setMaxHostsValue] = useState(maxHosts); + const [isUnlimited, setUnlimited] = useState(unlimitedHosts); + + useEffect(() => { + setNameValue(name); + setDescriptionValue(description); + setMaxHostsValue(maxHosts); + setUnlimited(unlimitedHosts); + }, [name, description, maxHosts, unlimitedHosts]); + + + const [isModalOpen, setModalOpen] = useState(false); + + const handleModalToggle = () => { + setModalOpen(!isModalOpen); + }; + const handleSave = () => { + dispatch(putActivationKey( + akId, + { + name: nameValue, + description: descriptionValue, + max_hosts: maxHostsValue, + 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 = () => { + maxHostsValue(oldValue => (oldValue || 0) - 1); + }; + const onChange = (event) => { + let newValue = (event.target.value === '' ? event.target.value : Math.round(+event.target.value)); + if (newValue < 0) { + newValue = 0; + } + setMaxHostsValue(newValue); + }; + const onPlus = () => { + setMaxHostsValue(oldValue => (oldValue || 0) + 1); + }; + + const handleCheckBox = () => { + setUnlimited(prevUnlimited => !prevUnlimited); + }; + + return ( + <> + + + Save + , + , + ]} + > +
+ + + + + + + + + + + + + + +