From 22bcd546a4f48120783e033bc8ea9ce80c2308f0 Mon Sep 17 00:00:00 2001 From: Sergio Betanzos Date: Wed, 13 Oct 2021 12:01:29 +0200 Subject: [PATCH] F #5422: Add to detail actions the VM state filter (#1520) --- .../src/client/components/Footer/index.js | 4 +- .../components/FormStepper/MobileStepper.js | 14 +-- .../Forms/Vm/CreateSnapshotForm/schema.js | 2 +- .../Steps/ExtraConfiguration/booting.js | 94 +++++++------- .../src/client/components/Header/Group.js | 9 +- .../src/client/components/Header/Popover.js | 116 ++++++++++-------- .../Vms/multiple.js => MultipleTags/index.js} | 6 +- .../client/components/Sidebar/SidebarLink.js | 5 +- .../src/client/components/Status/Chip.js | 5 +- .../src/client/components/Status/Circle.js | 2 +- .../Enhanced/Utils/GlobalActions/index.js | 53 +++++--- .../Enhanced/Utils/GlobalSelectedRows.js | 59 ++++++--- .../Tables/Enhanced/Utils/GlobalSort.js | 29 +---- .../components/Tables/Enhanced/Utils/index.js | 5 +- .../components/Tables/Enhanced/filters.js | 11 +- .../components/Tables/Enhanced/index.js | 3 +- .../components/Tables/Enhanced/styles.js | 24 ++-- .../components/Tables/Enhanced/toolbar.js | 58 +++++---- .../client/components/Tables/Vms/actions.js | 13 +- .../src/client/components/Tables/Vms/row.js | 17 +-- .../src/client/components/Tables/styles.js | 2 +- .../src/client/components/Tabs/Common/List.js | 5 +- .../components/Tabs/Vm/Capacity/index.js | 18 ++- .../client/components/Tabs/Vm/Info/index.js | 22 ++-- .../components/Tabs/Vm/Info/information.js | 4 +- .../client/components/Tabs/Vm/Network/Item.js | 8 +- .../components/Tabs/Vm/Network/index.js | 22 ++-- .../components/Tabs/Vm/Placement/index.js | 16 ++- .../components/Tabs/Vm/SchedActions/index.js | 17 ++- .../components/Tabs/Vm/Snapshot/index.js | 22 ++-- .../components/Tabs/Vm/Storage/index.js | 19 +-- .../src/client/constants/translates.js | 4 + .../DialogInfo/dialog.js | 2 +- .../src/client/containers/Clusters/index.js | 36 +++--- .../src/client/containers/Hosts/index.js | 36 +++--- .../src/client/containers/Images/index.js | 47 +++---- .../src/client/containers/Login/Form.js | 9 +- .../client/containers/TestApi/ResponseForm.js | 93 ++++++++++---- .../src/client/containers/TestApi/index.js | 28 ++--- .../containers/VirtualMachines/index.js | 37 +++--- .../client/containers/VmTemplates/index.js | 37 +++--- .../src/client/features/Auth/actions.js | 4 +- src/fireedge/src/client/hooks/useFetch.js | 2 +- .../src/client/models/VirtualMachine.js | 16 +++ src/fireedge/src/client/theme/defaults.js | 42 ++++--- 45 files changed, 606 insertions(+), 471 deletions(-) rename src/fireedge/src/client/components/{Tables/Vms/multiple.js => MultipleTags/index.js} (95%) diff --git a/src/fireedge/src/client/components/Footer/index.js b/src/fireedge/src/client/components/Footer/index.js index 351faf0b9a..22077bf080 100644 --- a/src/fireedge/src/client/components/Footer/index.js +++ b/src/fireedge/src/client/components/Footer/index.js @@ -42,15 +42,13 @@ const HeartIcon = styled('span')(({ theme }) => ({ })) const Footer = memo(() => { - const { config, version } = useSystem() + const { version } = useSystem() const { getOneVersion } = useSystemApi() useEffect(() => { !version && getOneVersion() }, []) - console.log({ config, version }) - return ( diff --git a/src/fireedge/src/client/components/FormStepper/MobileStepper.js b/src/fireedge/src/client/components/FormStepper/MobileStepper.js index 2f63b30db8..5c15ab305b 100644 --- a/src/fireedge/src/client/components/FormStepper/MobileStepper.js +++ b/src/fireedge/src/client/components/FormStepper/MobileStepper.js @@ -35,9 +35,7 @@ const useStyles = makeStyles(theme => ({ padding: theme.spacing(1, 2), color: theme.palette.primary.contrastText }, - error: { - padding: theme.spacing(1, 2) - }, + error: { padding: theme.spacing(1, 2) }, button: { color: theme.palette.action.active }, stepper: { background: 'transparent' } })) @@ -62,22 +60,22 @@ const CustomMobileStepper = ({ {typeof label === 'string' ? Tr(label) : label} {Boolean(errors[id]) && ( - + {errors[id]?.message} )} @@ -85,7 +83,7 @@ const CustomMobileStepper = ({ } nextButton={ - diff --git a/src/fireedge/src/client/components/Forms/Vm/CreateSnapshotForm/schema.js b/src/fireedge/src/client/components/Forms/Vm/CreateSnapshotForm/schema.js index 2e93a3c9db..1c67eef17c 100644 --- a/src/fireedge/src/client/components/Forms/Vm/CreateSnapshotForm/schema.js +++ b/src/fireedge/src/client/components/Forms/Vm/CreateSnapshotForm/schema.js @@ -19,7 +19,7 @@ import { INPUT_TYPES } from 'client/constants' import { getValidationFromFields } from 'client/utils' const NAME = { - name: 'NAME', + name: 'name', label: 'Snapshot name', type: INPUT_TYPES.TEXT, tooltip: 'The new snapshot name. It can be empty.', diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting.js index 4e8c48aa85..8cffeb7074 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting.js @@ -18,40 +18,42 @@ import { SetStateAction } from 'react' import PropTypes from 'prop-types' import { useWatch } from 'react-hook-form' -import { - NetworkAlt as NetworkIcon, - BoxIso as ImageIcon, - Check as CheckIcon, - Square as BlankSquareIcon -} from 'iconoir-react' -import { Divider } from '@mui/material' -import makeStyles from '@mui/styles/makeStyles' +import { NetworkAlt as NetworkIcon, BoxIso as ImageIcon } from 'iconoir-react' +import { Stack, Checkbox, styled } from '@mui/material' import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd' import { Translate } from 'client/components/HOC' -import { Action } from 'client/components/Cards/SelectCard' - import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration' import { TAB_ID as STORAGE_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/storage' import { TAB_ID as NIC_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/networking' import { T } from 'client/constants' -const useStyles = makeStyles(theme => ({ - container: { - margin: '1em' - }, - list: { - padding: '1em' - }, - item: { - border: `1px solid ${theme.palette.divider}`, - borderRadius: '0.5em', - padding: '1em', - marginBottom: '1em', - display: 'flex', - alignItems: 'center', - gap: '0.5em', - backgroundColor: theme.palette.background.default +const BootItem = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: '0.5em', + border: `1px solid ${theme.palette.divider}`, + borderRadius: '0.5em', + padding: '1em', + marginBottom: '1em', + backgroundColor: theme.palette.background.default +})) + +const BootItemDraggable = styled(BootItem)(({ theme }) => ({ + '&:before': { + content: "'.'", + fontSize: 20, + color: theme.palette.action.active, + paddingBottom: 20, + textShadow: ` + 0 5px ${theme.palette.action.active}, + 0 10px ${theme.palette.action.active}, + 5px 0 ${theme.palette.action.active}, + 5px 5px ${theme.palette.action.active}, + 5px 10px ${theme.palette.action.active}, + 10px 0 ${theme.palette.action.active}, + 10px 5px ${theme.palette.action.active}, + 10px 10px ${theme.palette.action.active}` } })) @@ -102,7 +104,6 @@ const reorder = (newBootOrder, setFormData) => { } const Booting = ({ data, setFormData, control }) => { - const classes = useStyles() const booting = useWatch({ name: `${EXTRA_ID}.${TAB_ID}`, control }) const bootOrder = booting?.split(',').filter(Boolean) ?? [] @@ -171,49 +172,44 @@ const Booting = ({ data, setFormData, control }) => { return ( -
+ {({ droppableProps, innerRef, placeholder }) => ( -
+ {enabledItems.map(({ ID, NAME }, idx) => ( {({ draggableProps, dragHandleProps, innerRef }) => ( -
- } - handleClick={() => handleEnable(ID)} + handleEnable(ID)} /> {NAME} -
+ )}
))} {placeholder} -
+
)} - {restOfItems.length > 0 && } {restOfItems.map(({ ID, NAME }) => ( -
- } - handleClick={() => handleEnable(ID)} + + handleEnable(ID)} /> {NAME} -
+ ))} -
+
) } diff --git a/src/fireedge/src/client/components/Header/Group.js b/src/fireedge/src/client/components/Header/Group.js index 54d9ffc6ab..9f3f0c149c 100644 --- a/src/fireedge/src/client/components/Header/Group.js +++ b/src/fireedge/src/client/components/Header/Group.js @@ -22,7 +22,7 @@ import { Group as GroupIcon, VerifiedBadge as SelectIcon } from 'iconoir-react' import { useAuth, useAuthApi } from 'client/features/Auth' import Search from 'client/components/Search' import HeaderPopover from 'client/components/Header/Popover' -import { Tr, Translate } from 'client/components/HOC' +import { Translate } from 'client/components/HOC' import { T, FILTER_POOL } from 'client/constants' const { ALL_RESOURCES, PRIMARY_GROUP_RESOURCES } = FILTER_POOL @@ -74,7 +74,7 @@ const Group = () => { } const sortMainGroupFirst = useMemo( - () => [{ ID: ALL_RESOURCES, NAME: Tr(T.ShowAll) }] + () => [{ ID: ALL_RESOURCES, NAME: }] ?.concat(groups) ?.sort(sortGroupAsMainFirst), [user?.GUID] @@ -113,7 +113,10 @@ const Group = () => { ButtonGroup.propTypes = { group: PropTypes.shape({ ID: PropTypes.string, - NAME: PropTypes.string + NAME: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.node + ]) }).isRequired, handleClick: PropTypes.func } diff --git a/src/fireedge/src/client/components/Header/Popover.js b/src/fireedge/src/client/components/Header/Popover.js index d88c36de78..8d9bd00807 100644 --- a/src/fireedge/src/client/components/Header/Popover.js +++ b/src/fireedge/src/client/components/Header/Popover.js @@ -14,7 +14,7 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import { memo, useState, useRef, useMemo, useEffect } from 'react' +import { memo, useState, useMemo, useEffect } from 'react' import PropTypes from 'prop-types' import { Cancel as CloseIcon, NavArrowDown as CaretIcon } from 'iconoir-react' @@ -27,7 +27,9 @@ import { IconButton, Button, Fade, - Box + Box, + buttonClasses, + ClickAwayListener } from '@mui/material' const HeaderPopover = memo(({ @@ -36,17 +38,21 @@ const HeaderPopover = memo(({ buttonLabel, buttonProps, headerTitle, - popoverProps, + popperProps, children }) => { const { zIndex } = useTheme() const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs')) const [open, setOpen] = useState(false) - const [fix, setFix] = useState(false) - const anchorRef = useRef(null) + const [anchorEl, setAnchorEl] = useState(null) - const handleToggle = () => isMobile && setFix(prevFix => !prevFix) + const handleClick = event => { + setAnchorEl(isMobile ? window.document : event.currentTarget) + setOpen((previousOpen) => !previousOpen) + } + + const handleClose = () => setOpen(false) const mobileStyles = useMemo(() => ({ ...(isMobile && { @@ -55,34 +61,36 @@ const HeaderPopover = memo(({ }) }), [isMobile]) + const canBeOpen = open && Boolean(anchorEl) + const hasId = canBeOpen ? id : undefined + useEffect(() => { - !isMobile && fix && setFix(false) + !isMobile && open && setOpen(false) }, [isMobile]) return ( -
setOpen(true), - onFocus: () => setOpen(true), - onMouseOut: () => setOpen(false) - }}> + <> {({ TransitionProps }) => ( - - - {(headerTitle || isMobile) && ( - - {headerTitle && ( - - {headerTitle} - - )} - {isMobile && ( - - - - )} - - )} - {children({ handleClose: handleToggle })} - - + + + + {(headerTitle || isMobile) && ( + + {headerTitle && ( + + {headerTitle} + + )} + {isMobile && ( + + + + )} + + )} + {children({ handleClose: handleClose })} + + + )} -
+ ) }) @@ -132,11 +142,11 @@ HeaderPopover.propTypes = { id: PropTypes.string, icon: PropTypes.node, buttonLabel: PropTypes.string, - buttonProps: PropTypes.objectOf(PropTypes.any), + buttonProps: PropTypes.object, tooltip: PropTypes.any, headerTitle: PropTypes.any, disablePadding: PropTypes.bool, - popoverProps: PropTypes.objectOf(PropTypes.any), + popperProps: PropTypes.object, children: PropTypes.func } @@ -148,7 +158,7 @@ HeaderPopover.defaultProps = { buttonProps: {}, headerTitle: undefined, disablePadding: false, - popoverProps: {}, + popperProps: {}, children: () => undefined } diff --git a/src/fireedge/src/client/components/Tables/Vms/multiple.js b/src/fireedge/src/client/components/MultipleTags/index.js similarity index 95% rename from src/fireedge/src/client/components/Tables/Vms/multiple.js rename to src/fireedge/src/client/components/MultipleTags/index.js index bfd1f12ae5..df6fc8c891 100644 --- a/src/fireedge/src/client/components/Tables/Vms/multiple.js +++ b/src/fireedge/src/client/components/MultipleTags/index.js @@ -20,7 +20,7 @@ import { Tooltip, Typography } from '@mui/material' import { StatusChip } from 'client/components/Status' -const Multiple = ({ tags, limitTags = 1 }) => { +const MultipleTags = ({ tags, limitTags = 1 }) => { if (tags?.length === 0) { return null } @@ -60,9 +60,9 @@ const Multiple = ({ tags, limitTags = 1 }) => { ) } -Multiple.propTypes = { +MultipleTags.propTypes = { tags: PropTypes.array, limitTags: PropTypes.number } -export default Multiple +export default MultipleTags diff --git a/src/fireedge/src/client/components/Sidebar/SidebarLink.js b/src/fireedge/src/client/components/Sidebar/SidebarLink.js index df27437c4e..ee6070d69e 100644 --- a/src/fireedge/src/client/components/Sidebar/SidebarLink.js +++ b/src/fireedge/src/client/components/Sidebar/SidebarLink.js @@ -28,7 +28,10 @@ import { useGeneralApi } from 'client/features/General' import sidebarStyles from 'client/components/Sidebar/styles' import { DevTypography } from 'client/components/Typography' -const STATIC_LABEL_PROPS = { 'data-cy': 'main-menu-item-text' } +const STATIC_LABEL_PROPS = { + 'data-cy': 'main-menu-item-text', + variant: 'body1' +} const SidebarLink = ({ label, path, icon: Icon, devMode, isSubItem }) => { const classes = sidebarStyles() diff --git a/src/fireedge/src/client/components/Status/Chip.js b/src/fireedge/src/client/components/Status/Chip.js index d7ed020e8f..657519a7a5 100644 --- a/src/fireedge/src/client/components/Status/Chip.js +++ b/src/fireedge/src/client/components/Status/Chip.js @@ -57,7 +57,10 @@ const StatusChip = memo(({ stateColor, text = '', ...props }) => { StatusChip.propTypes = { stateColor: PropTypes.string, - text: PropTypes.string + text: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.node + ]) } StatusChip.displayName = 'StatusChip' diff --git a/src/fireedge/src/client/components/Status/Circle.js b/src/fireedge/src/client/components/Status/Circle.js index caf577982c..ce89ac3a45 100644 --- a/src/fireedge/src/client/components/Status/Circle.js +++ b/src/fireedge/src/client/components/Status/Circle.js @@ -24,7 +24,7 @@ const useStyles = makeStyles({ circle: ({ color }) => ({ color, fill: 'currentColor', - verticalAlign: 'text-bottom', + verticalAlign: 'middle', pointerEvents: 'auto' }) }) diff --git a/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalActions/index.js b/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalActions/index.js index 902ecaadb1..35968d62aa 100644 --- a/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalActions/index.js +++ b/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalActions/index.js @@ -16,32 +16,41 @@ import { JSXElementConstructor, useMemo } from 'react' import PropTypes from 'prop-types' -import { Row } from 'react-table' -import makeStyles from '@mui/styles/makeStyles' +import { Stack, Checkbox } from '@mui/material' +import { + UseTableInstanceProps, + UseRowSelectState, + UseFiltersInstanceProps, + UseRowSelectInstanceProps +} from 'react-table' import Action, { ActionPropTypes, GlobalAction } from 'client/components/Tables/Enhanced/Utils/GlobalActions/Action' - -const useStyles = makeStyles({ - root: { - display: 'flex', - gap: '1em', - alignItems: 'center', - flexWrap: 'wrap' - } -}) +import { Tr } from 'client/components/HOC' +import { T } from 'client/constants' /** * Render bulk actions. * * @param {object} props - Props * @param {GlobalAction[]} props.globalActions - Possible bulk actions - * @param {Row[]} props.selectedRows - Selected rows + * @param {UseTableInstanceProps} props.useTableProps - Table props * @returns {JSXElementConstructor} Component JSX with all actions */ -const GlobalActions = ({ globalActions, selectedRows }) => { - const classes = useStyles() +const GlobalActions = ({ globalActions = [], useTableProps }) => { + /** @type {UseRowSelectInstanceProps} */ + const { + getToggleAllPageRowsSelectedProps, + getToggleAllRowsSelectedProps + } = useTableProps - const numberOfRowSelected = Object.keys(selectedRows)?.length + /** @type {UseRowSelectState} */ + const { selectedRowIds } = useTableProps?.state ?? {} + + /** @type {UseFiltersInstanceProps} */ + const { preFilteredRows } = useTableProps ?? {} + + const selectedRows = preFilteredRows.filter(row => !!selectedRowIds[row.id]) + const numberOfRowSelected = selectedRows.length const [actionsSelected, actionsNoSelected] = useMemo( () => globalActions.reduce((memoResult, item) => { @@ -55,7 +64,13 @@ const GlobalActions = ({ globalActions, selectedRows }) => { ) return ( -
+ + + {actionsNoSelected?.map(item => ( ))} @@ -73,13 +88,15 @@ const GlobalActions = ({ globalActions, selectedRows }) => { ) }) )} -
+ ) } GlobalActions.propTypes = { globalActions: PropTypes.arrayOf(ActionPropTypes), - selectedRows: PropTypes.array + useTableProps: PropTypes.object } +export { Action, ActionPropTypes, GlobalAction } + export default GlobalActions diff --git a/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalSelectedRows.js b/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalSelectedRows.js index ee18f01604..64a6a447c5 100644 --- a/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalSelectedRows.js +++ b/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalSelectedRows.js @@ -13,18 +13,23 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { useMemo, JSXElementConstructor } from 'react' +import { JSXElementConstructor } from 'react' import PropTypes from 'prop-types' import { TableProps } from 'react-table' -import { Chip } from '@mui/material' -import makeStyles from '@mui/styles/makeStyles' +import { styled, Chip, Alert, Button, alertClasses } from '@mui/material' -const useStyles = makeStyles({ - root: { +import { Translate } from 'client/components/HOC' +import { T } from 'client/constants' + +const MessageStyled = styled(Alert)({ + width: '100%', + [` .${alertClasses.message}`]: { + padding: 0, + width: '100%', display: 'flex', flexWrap: 'wrap', - gap: 6, + justifyContent: 'center', alignItems: 'center' } }) @@ -33,31 +38,45 @@ const useStyles = makeStyles({ * Render all selected rows. * * @param {object} props - Props + * @param {boolean} props.withAlert - If `true`, the list of selected rows will be an alert * @param {TableProps} props.useTableProps - Table props * @returns {JSXElementConstructor} Component JSX */ -const GlobalSelectedRows = ({ useTableProps }) => { - const classes = useStyles() +const GlobalSelectedRows = ({ withAlert = false, useTableProps }) => { + const { preFilteredRows, toggleAllRowsSelected, state: { selectedRowIds } } = useTableProps - const { preFilteredRows, state: { selectedRowIds } } = useTableProps const selectedRows = preFilteredRows.filter(row => !!selectedRowIds[row.id]) + const numberOfRowSelected = selectedRows.length + const allSelected = numberOfRowSelected === preFilteredRows.length - return ( -
- {useMemo(() => - selectedRows?.map(({ original, id, toggleRowSelected }) => ( - toggleRowSelected(false)} - /> - )), - [selectedRows[0]?.id] - )} + return withAlert ? ( + + + {'.'} + + + + ) : ( +
+ {selectedRows?.map(({ original, id, toggleRowSelected }) => ( + toggleRowSelected(false)} + /> + ))}
) } GlobalSelectedRows.propTypes = { + withAlert: PropTypes.bool, useTableProps: PropTypes.object.isRequired } diff --git a/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalSort.js b/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalSort.js index 7cffb6d8ae..94fe5ec1db 100644 --- a/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalSort.js +++ b/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalSort.js @@ -18,22 +18,12 @@ import { useEffect, useMemo, JSXElementConstructor } from 'react' import PropTypes from 'prop-types' import { SortDown, ArrowDown, ArrowUp } from 'iconoir-react' -import { MenuItem, MenuList, Chip } from '@mui/material' -import makeStyles from '@mui/styles/makeStyles' +import { MenuItem, MenuList, Chip, Stack } from '@mui/material' import { TableInstance, UseSortByInstanceProps, UseSortByState } from 'react-table' import HeaderPopover from 'client/components/Header/Popover' import { T } from 'client/constants' -const useStyles = makeStyles({ - root: { - display: 'flex', - flexWrap: 'wrap', - gap: 6, - alignItems: 'center' - } -}) - /** * Render all selected sorters. * @@ -42,8 +32,6 @@ const useStyles = makeStyles({ * @returns {JSXElementConstructor} Component JSX */ const GlobalSort = ({ useTableProps }) => { - const classes = useStyles() - const { headers, state } = useTableProps /** @type {UseSortByInstanceProps} */ @@ -72,7 +60,7 @@ const GlobalSort = ({ useTableProps }) => { } return ( -
+ {useMemo(() => ( { variant: 'outlined', color: 'secondary' }} - popoverProps= {{ - anchorOrigin: { - vertical: 'bottom', - horizontal: 'left' - }, - transformOrigin: { - vertical: 'top', - horizontal: 'left' - } - }} + popperProps={{ placement: 'bottom-start' }} > {() => ( @@ -119,7 +98,7 @@ const GlobalSort = ({ useTableProps }) => { onDelete={() => handleDelete(id)} /> )), [sortBy.length, handleToggle])} -
+ ) } diff --git a/src/fireedge/src/client/components/Tables/Enhanced/Utils/index.js b/src/fireedge/src/client/components/Tables/Enhanced/Utils/index.js index 5a7fddde3a..bbccce1e8e 100644 --- a/src/fireedge/src/client/components/Tables/Enhanced/Utils/index.js +++ b/src/fireedge/src/client/components/Tables/Enhanced/Utils/index.js @@ -14,7 +14,7 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ import CategoryFilter from 'client/components/Tables/Enhanced/Utils/CategoryFilter' -import GlobalActions from 'client/components/Tables/Enhanced/Utils/GlobalActions' +import GlobalActions, { Action, ActionPropTypes, GlobalAction } from 'client/components/Tables/Enhanced/Utils/GlobalActions' import GlobalFilter from 'client/components/Tables/Enhanced/Utils/GlobalFilter' import GlobalSelectedRows from 'client/components/Tables/Enhanced/Utils/GlobalSelectedRows' import GlobalSort from 'client/components/Tables/Enhanced/Utils/GlobalSort' @@ -23,7 +23,10 @@ import LabelFilter from 'client/components/Tables/Enhanced/Utils/LabelFilter' export * from 'client/components/Tables/Enhanced/Utils/utils' export { + Action, + ActionPropTypes, CategoryFilter, + GlobalAction, GlobalActions, GlobalFilter, GlobalSelectedRows, diff --git a/src/fireedge/src/client/components/Tables/Enhanced/filters.js b/src/fireedge/src/client/components/Tables/Enhanced/filters.js index 59995b682c..143c8dae63 100644 --- a/src/fireedge/src/client/components/Tables/Enhanced/filters.js +++ b/src/fireedge/src/client/components/Tables/Enhanced/filters.js @@ -13,14 +13,13 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -/* eslint-disable jsdoc/require-jsdoc */ -import { useMemo, Fragment } from 'react' +import { useMemo, Fragment, JSXElementConstructor } from 'react' import PropTypes from 'prop-types' import clsx from 'clsx' +import { UseTableInstanceProps } from 'react-table' import { useMediaQuery, Card, CardContent } from '@mui/material' import makeStyles from '@mui/styles/makeStyles' -import { UseTableInstanceProps } from 'react-table' import { GlobalFilter } from 'client/components/Tables/Enhanced/Utils' @@ -48,6 +47,12 @@ const useToolbarStyles = makeStyles({ } }) +/** + * @param {object} props - Props + * @param {boolean} props.onlyGlobalSearch - Show only the global search + * @param {UseTableInstanceProps} props.useTableProps - Table props + * @returns {JSXElementConstructor} Returns table toolbar + */ const Filters = ({ onlyGlobalSearch, useTableProps }) => { const classes = useToolbarStyles() const isMobile = useMediaQuery(theme => theme.breakpoints.down('md')) diff --git a/src/fireedge/src/client/components/Tables/Enhanced/index.js b/src/fireedge/src/client/components/Tables/Enhanced/index.js index e4a990df15..644d4736cf 100644 --- a/src/fireedge/src/client/components/Tables/Enhanced/index.js +++ b/src/fireedge/src/client/components/Tables/Enhanced/index.js @@ -35,6 +35,7 @@ import { import Toolbar from 'client/components/Tables/Enhanced/toolbar' import Pagination from 'client/components/Tables/Enhanced/pagination' import Filters from 'client/components/Tables/Enhanced/filters' +import { ActionPropTypes } from 'client/components/Tables/Enhanced/Utils' import EnhancedTableStyles from 'client/components/Tables/Enhanced/styles' import { Translate } from 'client/components/HOC' @@ -205,7 +206,7 @@ const EnhancedTable = ({ export const EnhancedTableProps = { canFetchMore: PropTypes.bool, - globalActions: PropTypes.array, + globalActions: PropTypes.arrayOf(ActionPropTypes), columns: PropTypes.array, data: PropTypes.array, fetchMore: PropTypes.func, diff --git a/src/fireedge/src/client/components/Tables/Enhanced/styles.js b/src/fireedge/src/client/components/Tables/Enhanced/styles.js index 0beec4f3b9..008870328e 100644 --- a/src/fireedge/src/client/components/Tables/Enhanced/styles.js +++ b/src/fireedge/src/client/components/Tables/Enhanced/styles.js @@ -26,16 +26,22 @@ export default makeStyles( toolbar: { ...typography.body1, marginBottom: 16, - display: 'flex', - gap: '1em', + display: 'grid', + gridTemplateColumns: '1fr auto', alignItems: 'start', - justifyContent: 'space-between', - '& > div:first-child': { - flexGrow: 1 + gap: '1em', + '& > .summary': { // global sort and selected rows + gridRow: '2', + gridColumn: '1 / -1', + display: 'grid', + gridTemplateColumns: 'minmax(auto, 300px) 1fr', + gap: '0.8em', + [breakpoints.down('md')]: { + gridTemplateColumns: '1fr' + } } }, pagination: { - flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'end', @@ -47,9 +53,9 @@ export default makeStyles( table: { display: 'grid', gridTemplateColumns: 'minmax(auto, 300px) 1fr', - gap: 8, + gap: '0.8em', overflow: 'auto', - [breakpoints.down('sm')]: { + [breakpoints.down('md')]: { gridTemplateColumns: 'minmax(0, 1fr)' } }, @@ -83,7 +89,7 @@ export default makeStyles( color: palette.text.hint, display: 'inline-flex', alignItems: 'center', - gap: 6, + gap: '0.8em', padding: '1em' } })) diff --git a/src/fireedge/src/client/components/Tables/Enhanced/toolbar.js b/src/fireedge/src/client/components/Tables/Enhanced/toolbar.js index 1fdf8c6807..704dba4b31 100644 --- a/src/fireedge/src/client/components/Tables/Enhanced/toolbar.js +++ b/src/fireedge/src/client/components/Tables/Enhanced/toolbar.js @@ -16,57 +16,61 @@ import { JSXElementConstructor } from 'react' import PropTypes from 'prop-types' -import { useMediaQuery } from '@mui/material' -import makeStyles from '@mui/styles/makeStyles' -import { UseTableInstanceProps, UseRowSelectState, UseFiltersInstanceProps } from 'react-table' +import { Stack, useMediaQuery } from '@mui/material' +import { UseTableInstanceProps, UseRowSelectState } from 'react-table' -import { GlobalActions, GlobalSelectedRows, GlobalSort } from 'client/components/Tables/Enhanced/Utils' - -const useToolbarStyles = makeStyles({ - root: { - display: 'flex', - flexDirection: 'column', - alignItems: 'start', - gap: '1em' - } -}) +import { + GlobalActions, + GlobalAction, + ActionPropTypes, + GlobalSelectedRows, + GlobalSort +} from 'client/components/Tables/Enhanced/Utils' /** * @param {object} props - Props - * @param {object} props.globalActions - Global actions + * @param {GlobalAction[]} props.globalActions - Global actions * @param {object} props.onlyGlobalSelectedRows - Show only the selected rows * @param {UseTableInstanceProps} props.useTableProps - Table props * @returns {JSXElementConstructor} Returns table toolbar */ const Toolbar = ({ globalActions, onlyGlobalSelectedRows, useTableProps }) => { - const classes = useToolbarStyles() const isMobile = useMediaQuery(theme => theme.breakpoints.down('sm')) const isSmallDevice = useMediaQuery(theme => theme.breakpoints.down('md')) /** @type {UseRowSelectState} */ const { selectedRowIds } = useTableProps?.state ?? {} - /** @type {UseFiltersInstanceProps} */ - const { preFilteredRows } = useTableProps ?? {} - - const selectedRows = preFilteredRows.filter(row => !!selectedRowIds[row.id]) - if (onlyGlobalSelectedRows) { return } return isMobile ? null : ( -
- {globalActions?.length > 0 && ( - - )} - {!isSmallDevice && } -
+ <> + + + + + {!isSmallDevice && ( +
+ +
+ )} + {!!Object.keys(selectedRowIds).length && ( + )} +
+ ) } Toolbar.propTypes = { - globalActions: PropTypes.array, + globalActions: PropTypes.arrayOf(ActionPropTypes), onlyGlobalSelectedRows: PropTypes.bool, useTableProps: PropTypes.object } diff --git a/src/fireedge/src/client/components/Tables/Vms/actions.js b/src/fireedge/src/client/components/Tables/Vms/actions.js index 2c4355ea6c..5e762a9301 100644 --- a/src/fireedge/src/client/components/Tables/Vms/actions.js +++ b/src/fireedge/src/client/components/Tables/Vms/actions.js @@ -37,16 +37,11 @@ import { Translate } from 'client/components/HOC' import { RecoverForm, ChangeUserForm, ChangeGroupForm, MigrateForm } from 'client/components/Forms/Vm' import { createActions } from 'client/components/Tables/Enhanced/Utils' import { PATH } from 'client/apps/sunstone/routesOne' -import { getLastHistory } from 'client/models/VirtualMachine' -import { T, VM_ACTIONS, MARKETPLACE_APP_ACTIONS, VM_ACTIONS_BY_STATE } from 'client/constants' +import { getLastHistory, isAvailableAction } from 'client/models/VirtualMachine' +import { T, VM_ACTIONS, MARKETPLACE_APP_ACTIONS } from 'client/constants' -const isDisabled = action => rows => { - if (VM_ACTIONS_BY_STATE[action]?.length === 0) return false - - const states = rows?.map?.(({ values }) => values?.STATE) - - return states.some(state => !VM_ACTIONS_BY_STATE[action]?.includes(state)) -} +const isDisabled = action => rows => + isAvailableAction(action)(rows, ({ values }) => values?.STATE) const ListVmNames = ({ rows = [] }) => { const datastores = useDatastore() diff --git a/src/fireedge/src/client/components/Tables/Vms/row.js b/src/fireedge/src/client/components/Tables/Vms/row.js index 4122d5fab3..a83c35e153 100644 --- a/src/fireedge/src/client/components/Tables/Vms/row.js +++ b/src/fireedge/src/client/components/Tables/Vms/row.js @@ -17,10 +17,10 @@ import PropTypes from 'prop-types' import { User, Group, Lock, HardDrive } from 'iconoir-react' -import { Typography } from '@mui/material' +import { Stack, Typography } from '@mui/material' import { StatusCircle } from 'client/components/Status' -import Multiple from 'client/components/Tables/Vms/multiple' +import MultipleTags from 'client/components/MultipleTags' import { rowStyles } from 'client/components/Tables/styles' import * as VirtualMachineModel from 'client/models/VirtualMachine' @@ -42,7 +42,7 @@ const Row = ({ original, value, ...props }) => {
- + {NAME} @@ -69,14 +69,9 @@ const Row = ({ original, value, ...props }) => {
{!!IPS?.length && (
-
- -
+ + +
)}
diff --git a/src/fireedge/src/client/components/Tables/styles.js b/src/fireedge/src/client/components/Tables/styles.js index a9120cc5e3..0a9d30ac4a 100644 --- a/src/fireedge/src/client/components/Tables/styles.js +++ b/src/fireedge/src/client/components/Tables/styles.js @@ -49,7 +49,7 @@ export const rowStyles = makeStyles( main: { flex: 'auto', overflow: 'hidden', - alignSelf: 'center' + alignSelf: 'start' }, title: { color: palette.text.primary, diff --git a/src/fireedge/src/client/components/Tabs/Common/List.js b/src/fireedge/src/client/components/Tabs/Common/List.js index 66144b1baa..c890ee49d7 100644 --- a/src/fireedge/src/client/components/Tabs/Common/List.js +++ b/src/fireedge/src/client/components/Tabs/Common/List.js @@ -18,7 +18,7 @@ import { Fragment, isValidElement } from 'react' import PropTypes from 'prop-types' import clsx from 'clsx' -import { List as MList, ListItem, Typography, Paper } from '@mui/material' +import { List as MList, ListItem, Typography, Paper, alpha } from '@mui/material' import makeStyles from '@mui/styles/makeStyles' import { Attribute, AttributePropTypes } from 'client/components/Tabs/Common/Attribute' @@ -36,6 +36,9 @@ const useStyles = makeStyles(theme => ({ '& > *': { flex: '1 1 50%', overflow: 'hidden' + }, + '&:hover': { + backgroundColor: alpha(theme.palette.text.primary, 0.05) } }, typo: theme.typography.body2 diff --git a/src/fireedge/src/client/components/Tabs/Vm/Capacity/index.js b/src/fireedge/src/client/components/Tabs/Vm/Capacity/index.js index 44aefedad0..1c8197f614 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/Capacity/index.js +++ b/src/fireedge/src/client/components/Tabs/Vm/Capacity/index.js @@ -14,15 +14,15 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import { useContext } from 'react' +import { useContext, useMemo } from 'react' import PropTypes from 'prop-types' import { useVmApi } from 'client/features/One' import { TabContext } from 'client/components/Tabs/TabProvider' import InformationPanel from 'client/components/Tabs/Vm/Capacity/information' -import * as VirtualMachine from 'client/models/VirtualMachine' -import * as Helper from 'client/models/Helper' +import { getHypervisor, isAvailableAction } from 'client/models/VirtualMachine' +import { getActionsAvailable, jsonToXml } from 'client/models/Helper' const VmCapacityTab = ({ tabProps: { actions } = {} }) => { const { resize } = useVmApi() @@ -30,12 +30,18 @@ const VmCapacityTab = ({ tabProps: { actions } = {} }) => { const { handleRefetch, data: vm = {} } = useContext(TabContext) const { ID } = vm - const hypervisor = VirtualMachine.getHypervisor(vm) - const actionsAvailable = Helper.getActionsAvailable(actions, hypervisor) + const actionsAvailable = useMemo(() => { + const hypervisor = getHypervisor(vm) + const actionsByHypervisor = getActionsAvailable(actions, hypervisor) + const actionsByState = actionsByHypervisor + .filter(action => !isAvailableAction(action)(vm)) + + return actionsByState + }, [vm]) const handleResizeCapacity = async formData => { const { enforce, ...restOfData } = formData - const template = Helper.jsonToXml(restOfData) + const template = jsonToXml(restOfData) const response = await resize(ID, { enforce, template }) String(response) === String(ID) && (await handleRefetch?.()) diff --git a/src/fireedge/src/client/components/Tabs/Vm/Info/index.js b/src/fireedge/src/client/components/Tabs/Vm/Info/index.js index 879b0bf994..6e654321bb 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/Info/index.js +++ b/src/fireedge/src/client/components/Tabs/Vm/Info/index.js @@ -14,7 +14,7 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import { useContext } from 'react' +import { useContext, useCallback } from 'react' import PropTypes from 'prop-types' import { useVmApi } from 'client/features/One' @@ -24,8 +24,8 @@ import Information from 'client/components/Tabs/Vm/Info/information' import { Tr } from 'client/components/HOC' import { T } from 'client/constants' -import * as VirtualMachine from 'client/models/VirtualMachine' -import * as Helper from 'client/models/Helper' +import { getHypervisor, isAvailableAction } from 'client/models/VirtualMachine' +import { getActionsAvailable, filterAttributes, jsonToXml } from 'client/models/Helper' import { cloneObject, set } from 'client/utils' const LXC_ATTRIBUTES_REG = /^LXC_/ @@ -68,7 +68,7 @@ const VmInfoTab = ({ tabProps = {} }) => { set(newTemplate, path, newValue) - const xml = Helper.jsonToXml(newTemplate) + const xml = jsonToXml(newTemplate) // 0: Replace the whole user template const response = await updateUserTemplate(ID, xml, 0) @@ -76,14 +76,20 @@ const VmInfoTab = ({ tabProps = {} }) => { String(response) === String(ID) && (await handleRefetch?.()) } - const hypervisor = VirtualMachine.getHypervisor(vm) - const getActions = actions => Helper.getActionsAvailable(actions, hypervisor) + const getActions = useCallback(actions => { + const hypervisor = getHypervisor(vm) + const actionsByHypervisor = getActionsAvailable(actions, hypervisor) + const actionsByState = actionsByHypervisor + .filter(action => !isAvailableAction(action)(vm)) + + return actionsByState + }, [vm]) const { attributes, lxc: lxcAttributes, vcenter: vcenterAttributes - } = Helper.filterAttributes(USER_TEMPLATE, { + } = filterAttributes(USER_TEMPLATE, { extra: { vcenter: VCENTER_ATTRIBUTES_REG, lxc: LXC_ATTRIBUTES_REG @@ -93,7 +99,7 @@ const VmInfoTab = ({ tabProps = {} }) => { const { attributes: monitoringAttributes - } = Helper.filterAttributes(MONITORING, { hidden: HIDDEN_MONITORING_REG }) + } = filterAttributes(MONITORING, { hidden: HIDDEN_MONITORING_REG }) const ATTRIBUTE_FUNCTION = { handleAdd: handleAttributeInXml, diff --git a/src/fireedge/src/client/components/Tabs/Vm/Info/information.js b/src/fireedge/src/client/components/Tabs/Vm/Info/information.js index f782bb7ab1..50e938e8fd 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/Info/information.js +++ b/src/fireedge/src/client/components/Tabs/Vm/Info/information.js @@ -21,7 +21,7 @@ import { generatePath } from 'react-router-dom' import { useCluster, useClusterApi } from 'client/features/One' import { StatusChip } from 'client/components/Status' import { List } from 'client/components/Tabs/Common' -import Multiple from 'client/components/Tables/Vms/multiple' +import MultipleTags from 'client/components/MultipleTags' import { getState, getLastHistory, getIps } from 'client/models/VirtualMachine' import * as Helper from 'client/models/Helper' @@ -72,7 +72,7 @@ const InformationPanel = ({ vm = {}, handleRename, actions }) => { }, { name: T.IP, - value: ips?.length ? : '--' + value: ips?.length ? : '--' }, { name: T.StartTime, diff --git a/src/fireedge/src/client/components/Tabs/Vm/Network/Item.js b/src/fireedge/src/client/components/Tabs/Vm/Network/Item.js index 96d3b91c5b..10d334b293 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/Network/Item.js +++ b/src/fireedge/src/client/components/Tabs/Vm/Network/Item.js @@ -33,7 +33,7 @@ import { useDialog } from 'client/hooks' import { TabContext } from 'client/components/Tabs/TabProvider' import { Action } from 'client/components/Cards/SelectCard' import { DialogConfirmation } from 'client/components/Dialogs' -import Multiple from 'client/components/Tables/Vms/multiple' +import MultipleTags from 'client/components/MultipleTags' import { Translate } from 'client/components/HOC' import { T, VM_ACTIONS } from 'client/constants' @@ -127,7 +127,7 @@ const NetworkItem = ({ nic = {}, actions }) => { {`${NIC_ID} | ${NETWORK}`}
- @@ -143,7 +143,7 @@ const NetworkItem = ({ nic = {}, actions }) => { {`${NIC_ID} | ${NETWORK}`} - @@ -164,7 +164,7 @@ const NetworkItem = ({ nic = {}, actions }) => { {`${ID} | ${NAME}`} - { @@ -33,19 +33,21 @@ const VmNetworkTab = ({ tabProps: { actions } = {} }) => { const { handleRefetch, data: vm } = useContext(TabContext) - const nics = VirtualMachine.getNics(vm, { - groupAlias: true, - securityGroupsFromTemplate: true - }) + const [nics, actionsAvailable] = useMemo(() => { + const groupedNics = getNics(vm, { groupAlias: true, securityGroupsFromTemplate: true }) + const hypervisor = getHypervisor(vm) + const actionsByHypervisor = getActionsAvailable(actions, hypervisor) + const actionsByState = actionsByHypervisor + .filter(action => !isAvailableAction(action)(vm)) - const hypervisor = VirtualMachine.getHypervisor(vm) - const actionsAvailable = Helper.getActionsAvailable(actions, hypervisor) + return [groupedNics, actionsByState] + }, [vm]) const handleAttachNic = async formData => { const isAlias = !!formData?.PARENT?.length const data = { [isAlias ? 'NIC_ALIAS' : 'NIC']: formData } - const template = Helper.jsonToXml(data) + const template = jsonToXml(data) const response = await attachNic(vm.ID, template) String(response) === String(vm.ID) && (await handleRefetch?.(vm.ID)) diff --git a/src/fireedge/src/client/components/Tabs/Vm/Placement/index.js b/src/fireedge/src/client/components/Tabs/Vm/Placement/index.js index 55bbf590a3..71ea533e56 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/Placement/index.js +++ b/src/fireedge/src/client/components/Tabs/Vm/Placement/index.js @@ -14,22 +14,26 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import { useContext } from 'react' +import { useContext, useMemo } from 'react' import PropTypes from 'prop-types' import { TabContext } from 'client/components/Tabs/TabProvider' import HistoryList from 'client/components/Tabs/Vm/Placement/List' -import * as VirtualMachine from 'client/models/VirtualMachine' -import * as Helper from 'client/models/Helper' +import { getHypervisor, getHistoryRecords, isAvailableAction } from 'client/models/VirtualMachine' +import { getActionsAvailable } from 'client/models/Helper' const VmPlacementTab = ({ tabProps: { actions } = {} }) => { const { data: vm } = useContext(TabContext) - const records = VirtualMachine.getHistoryRecords(vm) + const [records, actionsAvailable] = useMemo(() => { + const hypervisor = getHypervisor(vm) + const actionsByHypervisor = getActionsAvailable(actions, hypervisor) + const actionsByState = actionsByHypervisor + .filter(action => !isAvailableAction(action)(vm)) - const hypervisor = VirtualMachine.getHypervisor(vm) - const actionsAvailable = Helper.getActionsAvailable(actions, hypervisor) + return [getHistoryRecords(vm), actionsByState] + }, [vm]) return ( diff --git a/src/fireedge/src/client/components/Tabs/Vm/SchedActions/index.js b/src/fireedge/src/client/components/Tabs/Vm/SchedActions/index.js index ade9324ee5..e114f6ba4e 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/SchedActions/index.js +++ b/src/fireedge/src/client/components/Tabs/Vm/SchedActions/index.js @@ -14,7 +14,7 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import { useContext } from 'react' +import { useContext, useMemo } from 'react' import PropTypes from 'prop-types' import { useAuth } from 'client/features/Auth' @@ -22,17 +22,22 @@ import { TabContext } from 'client/components/Tabs/TabProvider' import { CreateSchedAction, CharterAction } from 'client/components/Tabs/Vm/SchedActions/Actions' import SchedulingList from 'client/components/Tabs/Vm/SchedActions/List' -import * as VirtualMachine from 'client/models/VirtualMachine' -import * as Helper from 'client/models/Helper' +import { getScheduleActions, getHypervisor, isAvailableAction } from 'client/models/VirtualMachine' +import { getActionsAvailable } from 'client/models/Helper' import { VM_ACTIONS } from 'client/constants' const VmSchedulingTab = ({ tabProps: { actions } = {} }) => { const { config } = useAuth() const { data: vm } = useContext(TabContext) - const scheduling = VirtualMachine.getScheduleActions(vm) - const hypervisor = VirtualMachine.getHypervisor(vm) - const actionsAvailable = Helper.getActionsAvailable(actions, hypervisor) + const [scheduling, actionsAvailable] = useMemo(() => { + const hypervisor = getHypervisor(vm) + const actionsByHypervisor = getActionsAvailable(actions, hypervisor) + const actionsByState = actionsByHypervisor + .filter(action => !isAvailableAction(action)(vm)) + + return [getScheduleActions(vm), actionsByState] + }, [vm]) return ( <> diff --git a/src/fireedge/src/client/components/Tabs/Vm/Snapshot/index.js b/src/fireedge/src/client/components/Tabs/Vm/Snapshot/index.js index 2201716710..41f165b9fb 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/Snapshot/index.js +++ b/src/fireedge/src/client/components/Tabs/Vm/Snapshot/index.js @@ -14,7 +14,7 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import { useContext } from 'react' +import { useContext, useMemo } from 'react' import PropTypes from 'prop-types' import { useVmApi } from 'client/features/One' @@ -24,8 +24,8 @@ import SnapshotList from 'client/components/Tabs/Vm/Snapshot/List' import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm' import { CreateSnapshotForm } from 'client/components/Forms/Vm' -import * as VirtualMachine from 'client/models/VirtualMachine' -import * as Helper from 'client/models/Helper' +import { getSnapshotList, getHypervisor, isAvailableAction } from 'client/models/VirtualMachine' +import { getActionsAvailable } from 'client/models/Helper' import { T, VM_ACTIONS } from 'client/constants' const VmSnapshotTab = ({ tabProps: { actions } = {} }) => { @@ -33,13 +33,17 @@ const VmSnapshotTab = ({ tabProps: { actions } = {} }) => { const { data: vm = {} } = useContext(TabContext) - const snapshots = VirtualMachine.getSnapshotList(vm) - const hypervisor = VirtualMachine.getHypervisor(vm) - const actionsAvailable = Helper.getActionsAvailable(actions, hypervisor) + const [snapshots, actionsAvailable] = useMemo(() => { + const hypervisor = getHypervisor(vm) + const actionsByHypervisor = getActionsAvailable(actions, hypervisor) + const actionsByState = actionsByHypervisor + .filter(action => !isAvailableAction(action)(vm)) - const handleSnapshotCreate = async ({ NAME } = {}) => { - const data = { name: NAME } - await createSnapshot(vm.ID, data) + return [getSnapshotList(vm), actionsByState] + }, [vm]) + + const handleSnapshotCreate = async (formData = {}) => { + await createSnapshot(vm.ID, formData) } return ( diff --git a/src/fireedge/src/client/components/Tabs/Vm/Storage/index.js b/src/fireedge/src/client/components/Tabs/Vm/Storage/index.js index c22b94d1f9..057034fcaa 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/Storage/index.js +++ b/src/fireedge/src/client/components/Tabs/Vm/Storage/index.js @@ -14,7 +14,7 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import { useContext } from 'react' +import { useContext, useMemo } from 'react' import PropTypes from 'prop-types' import { useVmApi } from 'client/features/One' @@ -24,8 +24,8 @@ import StorageList from 'client/components/Tabs/Vm/Storage/List' import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm' import { ImageSteps, VolatileSteps } from 'client/components/Forms/Vm' -import * as VirtualMachine from 'client/models/VirtualMachine' -import * as Helper from 'client/models/Helper' +import { getDisks, getHypervisor, isAvailableAction } from 'client/models/VirtualMachine' +import { getActionsAvailable, jsonToXml } from 'client/models/Helper' import { T, VM_ACTIONS } from 'client/constants' const VmStorageTab = ({ tabProps: { actions } = {} }) => { @@ -33,12 +33,17 @@ const VmStorageTab = ({ tabProps: { actions } = {} }) => { const { data: vm = {} } = useContext(TabContext) - const disks = VirtualMachine.getDisks(vm) - const hypervisor = VirtualMachine.getHypervisor(vm) - const actionsAvailable = Helper.getActionsAvailable(actions, hypervisor) + const [disks, hypervisor, actionsAvailable] = useMemo(() => { + const hyperV = getHypervisor(vm) + const actionsByHypervisor = getActionsAvailable(actions, hyperV) + const actionsByState = actionsByHypervisor + .filter(action => !isAvailableAction(action)(vm)) + + return [getDisks(vm), hyperV, actionsByState] + }, [vm]) const handleAttachDisk = async formData => { - const template = Helper.jsonToXml({ DISK: formData }) + const template = jsonToXml({ DISK: formData }) await attachDisk(vm.ID, template) } diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js index bcfb92fa1a..9de2e145f6 100644 --- a/src/fireedge/src/client/constants/translates.js +++ b/src/fireedge/src/client/constants/translates.js @@ -23,6 +23,10 @@ module.exports = { Filters: 'Filters', All: 'All', On: 'On', + ToggleAllCurrentPageRowsSelected: 'Toggle all current page rows selected', + NumberOfResourcesSelected: 'All %s resources are selected', + SelectAllResources: 'Select all %s resources', + ClearSelection: 'Clear selection', /* actions */ Accept: 'Accept', diff --git a/src/fireedge/src/client/containers/ApplicationsInstances/DialogInfo/dialog.js b/src/fireedge/src/client/containers/ApplicationsInstances/DialogInfo/dialog.js index 7c4806e6fd..6192bdf280 100644 --- a/src/fireedge/src/client/containers/ApplicationsInstances/DialogInfo/dialog.js +++ b/src/fireedge/src/client/containers/ApplicationsInstances/DialogInfo/dialog.js @@ -57,7 +57,7 @@ const CustomDialog = ({ title, handleClose, children }) => { {children} - diff --git a/src/fireedge/src/client/containers/Clusters/index.js b/src/fireedge/src/client/containers/Clusters/index.js index babbf588e4..e06c053505 100644 --- a/src/fireedge/src/client/containers/Clusters/index.js +++ b/src/fireedge/src/client/containers/Clusters/index.js @@ -15,40 +15,42 @@ * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ import { useState } from 'react' -import { Container, Box } from '@mui/material' +import { Container, Stack, Chip } from '@mui/material' import { ClustersTable } from 'client/components/Tables' import ClusterTabs from 'client/components/Tabs/Cluster' import SplitPane from 'client/components/SplitPane' +import MultipleTags from 'client/components/MultipleTags' function Clusters () { - const [selectedRows, onSelectedRowsChange] = useState([]) - - const getRowIds = () => - JSON.stringify(selectedRows?.map(row => row.id).join(', '), null, 2) + const [selectedRows, onSelectedRowsChange] = useState(() => []) return ( - + {selectedRows?.length > 0 && ( -
+ {selectedRows?.length === 1 ? - :
{getRowIds()}
+ : + ( + toggleRowSelected(false)} + /> + ))} + /> + } -
+
)} -
+ ) } diff --git a/src/fireedge/src/client/containers/Hosts/index.js b/src/fireedge/src/client/containers/Hosts/index.js index be2831c317..5c4a5508aa 100644 --- a/src/fireedge/src/client/containers/Hosts/index.js +++ b/src/fireedge/src/client/containers/Hosts/index.js @@ -15,40 +15,42 @@ * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ import { useState } from 'react' -import { Container, Box } from '@mui/material' +import { Container, Stack, Chip } from '@mui/material' import { HostsTable } from 'client/components/Tables' import HostTabs from 'client/components/Tabs/Host' import SplitPane from 'client/components/SplitPane' +import MultipleTags from 'client/components/MultipleTags' function Hosts () { - const [selectedRows, onSelectedRowsChange] = useState([]) - - const getRowIds = () => - JSON.stringify(selectedRows?.map(row => row.id).join(', '), null, 2) + const [selectedRows, onSelectedRowsChange] = useState(() => []) return ( - + {selectedRows?.length > 0 && ( -
+ {selectedRows?.length === 1 ? - :
{getRowIds()}
+ : + ( + toggleRowSelected(false)} + /> + ))} + /> + } -
+
)} -
+ ) } diff --git a/src/fireedge/src/client/containers/Images/index.js b/src/fireedge/src/client/containers/Images/index.js index 6746daec46..79c72a2982 100644 --- a/src/fireedge/src/client/containers/Images/index.js +++ b/src/fireedge/src/client/containers/Images/index.js @@ -14,50 +14,43 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import { useState, useMemo } from 'react' - -import { Container, Box } from '@mui/material' +import { useState } from 'react' +import { Container, Stack, Chip } from '@mui/material' import { ImagesTable } from 'client/components/Tables' import Detail from 'client/components/Tables/Images/detail' import SplitPane from 'client/components/SplitPane' +import MultipleTags from 'client/components/MultipleTags' function Images () { - const [selectedRows, onSelectedRowsChange] = useState() - - const selectedRowIds = useMemo( - () => selectedRows?.map(row => row.id), - [selectedRows] - ) + const [selectedRows, onSelectedRowsChange] = useState(() => []) return ( - + {selectedRows?.length > 0 && ( -
+ {selectedRows?.length === 1 ? - : ( -
-                  
-                    {JSON.stringify(Object.keys(selectedRowIds)?.join(', '), null, 2)}
-                  
-                
- ) + : + ( + toggleRowSelected(false)} + /> + ))} + /> + } -
+
)} -
+ ) } diff --git a/src/fireedge/src/client/containers/Login/Form.js b/src/fireedge/src/client/containers/Login/Form.js index f5a6ab2e67..e0f883785f 100644 --- a/src/fireedge/src/client/containers/Login/Form.js +++ b/src/fireedge/src/client/containers/Login/Form.js @@ -18,8 +18,7 @@ import { useEffect } from 'react' import PropTypes from 'prop-types' import clsx from 'clsx' -import { Box } from '@mui/system' -import { Button, Slide } from '@mui/material' +import { Button, Box, Slide, Stack } from '@mui/material' import { useForm, FormProvider } from 'react-hook-form' import { yupResolver } from '@hookform/resolvers/yup' @@ -59,9 +58,9 @@ const Form = ({ onBack, onSubmit, resolver, fields, error, isLoading, transition - + {onBack && ( - )} @@ -72,7 +71,7 @@ const Form = ({ onBack, onSubmit, resolver, fields, error, isLoading, transition sx={{ textTransform: 'uppercase', padding: '0.5em' }} label={onBack ? Tr(T.Next) : Tr(T.SignIn)} /> - + ) diff --git a/src/fireedge/src/client/containers/TestApi/ResponseForm.js b/src/fireedge/src/client/containers/TestApi/ResponseForm.js index ad3ab16c26..7f50487a0b 100644 --- a/src/fireedge/src/client/containers/TestApi/ResponseForm.js +++ b/src/fireedge/src/client/containers/TestApi/ResponseForm.js @@ -13,18 +13,18 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { JSXElementConstructor } from 'react' +import { useMemo, SetStateAction, JSXElementConstructor } from 'react' import { string, func, shape, object } from 'prop-types' import { useForm, Controller } from 'react-hook-form' -import { TextField, Grid, Typography, FormControlLabel, Checkbox } from '@mui/material' +import { TextField, Grid, Typography, FormControlLabel, Checkbox, Autocomplete, Chip } from '@mui/material' import { SubmitButton } from 'client/components/FormControl' import { RestClient, requestConfig } from 'client/utils' /** * @param {object} props - Component props - * @param {Function} props.handleChangeResponse - Change after + * @param {SetStateAction} props.handleChangeResponse - Change after * @param {object} props.command - Resource command action * @param {string} props.command.name - Name of command * @param {('GET'|'POST'|'DELETE'|'PUT')} props.command.httpMethod - Http method @@ -36,14 +36,16 @@ const ResponseForm = ({ command: { name, httpMethod, params } }) => { const { control, handleSubmit, errors, formState } = useForm() + const memoParams = useMemo(() => Object.entries(params), [name]) const onSubmit = async dataForm => { try { const config = requestConfig(dataForm, { name, httpMethod, params }) - const { id, ...res } = (await RestClient.request(config)) ?? {} + const { id, ...res } = await RestClient.request(config) ?? {} handleChangeResponse(JSON.stringify(res, null, '\t')) } catch (err) { + handleChangeResponse(JSON.stringify(err.data, null, '\t')) console.log('ERROR', err) } } @@ -60,40 +62,77 @@ const ResponseForm = ({ - {Object.entries(params)?.map(([nameCommand, { default: value }]) => ( - + {memoParams?.map(([nameParam, { default: defaultValue }]) => ( + } - label={nameCommand} - labelPlacement={nameCommand} - /> - ) : ( - - ) - } + render={({ value, onChange, ...controllerProps }) => ({ + boolean: onChange(e.target.checked)} + /> + )} + label={nameParam} + labelPlacement='end' + />, + object: onChange(newValue ?? '')} + renderTags={(tags, getTagProps) => + tags.map((tag, index) => ( + + )) + } + renderInput={(params) => ( + + )} + /> + }[typeof defaultValue] ?? ( + + ))} control={control} - name={`${nameCommand}`} - defaultValue={String(value)} + name={`${nameParam}`} + defaultValue={defaultValue} /> ))} - + diff --git a/src/fireedge/src/client/containers/TestApi/index.js b/src/fireedge/src/client/containers/TestApi/index.js index 470400de78..4400f40a19 100644 --- a/src/fireedge/src/client/containers/TestApi/index.js +++ b/src/fireedge/src/client/containers/TestApi/index.js @@ -14,7 +14,7 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ import { useState, useMemo, JSXElementConstructor } from 'react' -import { Container, TextField, Grid, MenuItem, Box } from '@mui/material' +import { Container, TextField, Grid, Box } from '@mui/material' import ResponseForm from 'client/containers/TestApi/ResponseForm' import { InputCode } from 'client/components/FormControl' @@ -25,13 +25,15 @@ import Commands from 'server/utils/constants/commands' import testApiStyles from 'client/containers/TestApi/styles' +const COMMANDS = Object.keys(Commands)?.sort() + /** * @returns {JSXElementConstructor} - Component that allows you * to fetch, resolve, and interact with OpenNebula API. */ function TestApi () { const classes = testApiStyles() - const [name, setName] = useState('acl.addrule') + const [name, setName] = useState(() => COMMANDS[0]) const [response, setResponse] = useState('') const handleChangeCommand = evt => setName(evt?.target?.value) @@ -40,7 +42,7 @@ function TestApi () { return ( @@ -52,19 +54,13 @@ function TestApi () { value={name} onChange={handleChangeCommand} > - {Tr(T.None)} + {useMemo(() => - Object.keys(Commands)?.sort().map( - commandName => ( - - {commandName} - - ), - [] - ) + COMMANDS.map(commandName => ( + + ), []) )} {name && name !== '' && ( @@ -75,7 +71,7 @@ function TestApi () { )} - + diff --git a/src/fireedge/src/client/containers/VirtualMachines/index.js b/src/fireedge/src/client/containers/VirtualMachines/index.js index d1d46e9d12..9f2ebb08ce 100644 --- a/src/fireedge/src/client/containers/VirtualMachines/index.js +++ b/src/fireedge/src/client/containers/VirtualMachines/index.js @@ -15,30 +15,20 @@ * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ import { useState } from 'react' - -import { Container, Box } from '@mui/material' +import { Container, Stack, Chip } from '@mui/material' import { VmsTable } from 'client/components/Tables' import VmActions from 'client/components/Tables/Vms/actions' import VmTabs from 'client/components/Tabs/Vm' import SplitPane from 'client/components/SplitPane' +import MultipleTags from 'client/components/MultipleTags' function VirtualMachines () { - const [selectedRows, onSelectedRowsChange] = useState([]) + const [selectedRows, onSelectedRowsChange] = useState(() => []) const actions = VmActions() - const getRowIds = () => - JSON.stringify(selectedRows?.map(row => row.id).join(', '), null, 2) - return ( - + {selectedRows?.length > 0 && ( -
+ {selectedRows?.length === 1 ? - :
{getRowIds()}
+ : + ( + toggleRowSelected(false)} + /> + ))} + /> + } -
+
)} -
+ ) } diff --git a/src/fireedge/src/client/containers/VmTemplates/index.js b/src/fireedge/src/client/containers/VmTemplates/index.js index f7ee318507..6d46216184 100644 --- a/src/fireedge/src/client/containers/VmTemplates/index.js +++ b/src/fireedge/src/client/containers/VmTemplates/index.js @@ -15,30 +15,20 @@ * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ import { useState } from 'react' - -import { Container, Box } from '@mui/material' +import { Container, Stack, Chip } from '@mui/material' import { VmTemplatesTable } from 'client/components/Tables' import VmTemplateActions from 'client/components/Tables/VmTemplates/actions' import VmTemplateTabs from 'client/components/Tabs/VmTemplate' import SplitPane from 'client/components/SplitPane' +import MultipleTags from 'client/components/MultipleTags' function VmTemplates () { - const [selectedRows, onSelectedRowsChange] = useState([]) + const [selectedRows, onSelectedRowsChange] = useState(() => []) const actions = VmTemplateActions() - const getRowIds = () => - JSON.stringify(selectedRows?.map(row => row.id).join(', '), null, 2) - return ( - + {selectedRows?.length > 0 && ( -
+ {selectedRows?.length === 1 ? - :
{getRowIds()}
+ : + ( + toggleRowSelected(false)} + /> + ))} + /> + } -
+
)} -
+ ) } diff --git a/src/fireedge/src/client/features/Auth/actions.js b/src/fireedge/src/client/features/Auth/actions.js index f780d5e27c..105d864a10 100644 --- a/src/fireedge/src/client/features/Auth/actions.js +++ b/src/fireedge/src/client/features/Auth/actions.js @@ -47,9 +47,7 @@ export const login = createAsyncThunk( isLoginInProgress: !!token && !isOneAdmin } } catch (error) { - const { message, data, status, statusText } = error - - status === httpCodes.unauthorized.id && dispatch(logout(T.SessionExpired)) + const { message, data, statusText } = error return rejectWithValue({ error: message ?? data?.message ?? statusText }) } diff --git a/src/fireedge/src/client/hooks/useFetch.js b/src/fireedge/src/client/hooks/useFetch.js index 81a1954949..5e49e7447c 100644 --- a/src/fireedge/src/client/hooks/useFetch.js +++ b/src/fireedge/src/client/hooks/useFetch.js @@ -150,7 +150,7 @@ const useFetch = (request, socket) => { } await fakeDelay(delay) - await doFetch(payload, reload) + return await doFetch(payload, reload) }, [request]) return { ...state, fetchRequest, STATUS } diff --git a/src/fireedge/src/client/models/VirtualMachine.js b/src/fireedge/src/client/models/VirtualMachine.js index b7a71486d6..dc1248d1ea 100644 --- a/src/fireedge/src/client/models/VirtualMachine.js +++ b/src/fireedge/src/client/models/VirtualMachine.js @@ -19,6 +19,7 @@ import { Tr } from 'client/components/HOC' import { STATES, + VM_ACTIONS_BY_STATE, VM_STATES, VM_LCM_STATES, NIC_ALIAS_IP_ATTRS, @@ -255,3 +256,18 @@ export const periodicityToString = scheduleAction => { return { repeat, end } } + +/** + * Returns `true` if action is available by VM state. + * + * @param {object} action - VM action + * @returns {function(Array, Function):boolean} + * - The list of vms that will be perform the action + */ +export const isAvailableAction = action => (vms = [], getVmState = vm => getState(vm)?.name) => { + if (VM_ACTIONS_BY_STATE[action]?.length === 0) return false + + const states = [vms].flat().map(getVmState) + + return states?.some(state => !VM_ACTIONS_BY_STATE[action]?.includes(state)) +} diff --git a/src/fireedge/src/client/theme/defaults.js b/src/fireedge/src/client/theme/defaults.js index 6906e12a22..64b729062b 100644 --- a/src/fireedge/src/client/theme/defaults.js +++ b/src/fireedge/src/client/theme/defaults.js @@ -19,6 +19,8 @@ import { SCHEMES } from 'client/constants' const defaultTheme = createTheme() const { grey } = colors +const black = '#1D1D1D' +const white = '#ffffff' const systemFont = [ '-apple-system', @@ -77,11 +79,11 @@ export default (appTheme, mode = SCHEMES.DARK) => { primary, secondary, common: { - black: '#1D1D1D', - white: '#ffffff' + black, + white }, background: { - paper: isDarkMode ? '#2a2d3d' : '#ffffff', + paper: isDarkMode ? '#2a2d3d' : white, default: isDarkMode ? '#222431' : '#f2f4f8' }, error: { @@ -96,7 +98,7 @@ export default (appTheme, mode = SCHEMES.DARK) => { light: '#f8c0b7', main: '#ec5840', dark: '#f2391b', - contrastText: '#ffffff' + contrastText: white }, warning: { 100: '#FFF4DB', @@ -116,7 +118,7 @@ export default (appTheme, mode = SCHEMES.DARK) => { light: '#64b5f6', main: '#2196f3', dark: '#01579b', - contrastText: '#ffffff' + contrastText: white }, success: { 100: '#bce1bd', @@ -130,13 +132,13 @@ export default (appTheme, mode = SCHEMES.DARK) => { light: '#3adb76', main: '#4caf50', dark: '#388e3c', - contrastText: '#ffffff' + contrastText: white }, debug: { light: '#e0e0e0', main: '#757575', dark: '#424242', - contrastText: '#ffffff' + contrastText: isDarkMode ? white : black } }, breakpoints: { @@ -279,13 +281,16 @@ export default (appTheme, mode = SCHEMES.DARK) => { height: '1rem' }, text: { - backgroundColor: isDarkMode ? primary[400] : primary[800] + color: isDarkMode ? white : grey[900], + '&:hover': { + backgroundColor: isDarkMode ? alpha(white, 0.1) : alpha(grey[900], 0.1) + } }, outlined: { border: '1px solid', - borderColor: isDarkMode ? alpha(grey[100], 0.1) : alpha(grey[700], 0.15), + borderColor: isDarkMode ? alpha(grey[100], 0.45) : alpha(grey[700], 0.45), borderRadius: defaultTheme.shape.borderRadius, - color: isDarkMode ? '#ffffff' : grey[900] + color: isDarkMode ? white : grey[900] } } }, @@ -314,13 +319,13 @@ export default (appTheme, mode = SCHEMES.DARK) => { borderBottomWidth: 'thin', backgroundColor: primary.main, '& .MuiIconButton-root, & .MuiButton-root': { - color: '#ffffff', + color: white, border: 'none', backgroundColor: 'transparent', '&:hover': { border: 'none', backgroundColor: 'transparent', - color: alpha('#ffffff', 0.7) + color: alpha(white, 0.7) } } } @@ -364,7 +369,7 @@ export default (appTheme, mode = SCHEMES.DARK) => { left: 30, right: 30, height: '100%', - backgroundColor: '#ffffff' + backgroundColor: white } } } @@ -376,7 +381,7 @@ export default (appTheme, mode = SCHEMES.DARK) => { textTransform: 'capitalize', fontSize: '1rem', '&.Mui-selected': { - color: '#ffffff' + color: white } } } @@ -386,6 +391,15 @@ export default (appTheme, mode = SCHEMES.DARK) => { dense: true, disablePadding: true } + }, + MuiChip: { + variants: [{ + props: { variant: 'text' }, + style: { + border: 0, + backgroundColor: 'transparent' + } + }] } } }