From 25e3529f5312b70e382357e56dced8c82f415013 Mon Sep 17 00:00:00 2001 From: vichansson Date: Tue, 28 Nov 2023 17:26:33 +0200 Subject: [PATCH] F OpenNebula/one#6362: Add missing label selectors (#2844) --- .../client/components/Cards/BackupJobCard.js | 21 ++++++- .../src/client/components/Cards/HostCard.js | 42 ++++++++++++-- .../client/components/Cards/NetworkCard.js | 5 +- .../components/Cards/SecurityGroupCard.js | 33 ++++++++++- .../helpers/subComponents/ChartRenderer.js | 2 - .../Steps/ExtraConfiguration/backup/schema.js | 2 - .../components/Tables/BackupJobs/columns.js | 8 ++- .../components/Tables/Backups/columns.js | 17 +++++- .../client/components/Tables/Backups/row.js | 49 +++++++++++++++- .../components/Tables/Datastores/index.js | 2 +- .../components/Tables/Datastores/row.js | 23 +++++--- .../client/components/Tables/Files/columns.js | 8 ++- .../components/Tables/Groups/columns.js | 4 +- .../client/components/Tables/Hosts/columns.js | 17 +++++- .../src/client/components/Tables/Hosts/row.js | 50 +++++++++++++--- .../components/Tables/Images/columns.js | 16 ++++- .../client/components/Tables/Images/row.js | 48 ++++++++++++++- .../Tables/SecurityGroups/columns.js | 16 ++++- .../components/Tables/SecurityGroups/row.js | 53 ++++++++++++++--- .../components/Tables/VNetworks/columns.js | 6 ++ .../client/components/Tables/VNetworks/row.js | 37 +++++++++--- .../components/Tables/VmTemplates/columns.js | 6 ++ .../src/client/constants/translates.js | 2 +- .../src/client/containers/Backups/index.js | 6 +- .../src/client/containers/Images/index.js | 6 +- .../client/containers/SecurityGroups/index.js | 6 +- .../src/client/features/OneApi/datastore.js | 5 +- .../src/client/features/OneApi/image.js | 58 ++++++++++++++++++- .../src/client/features/OneApi/network.js | 5 +- .../client/features/OneApi/securityGroup.js | 29 +++++++++- src/fireedge/src/client/models/Helper.js | 31 ++++++---- 31 files changed, 527 insertions(+), 86 deletions(-) diff --git a/src/fireedge/src/client/components/Cards/BackupJobCard.js b/src/fireedge/src/client/components/Cards/BackupJobCard.js index eee3dea7ed..d9e8f74ef5 100644 --- a/src/fireedge/src/client/components/Cards/BackupJobCard.js +++ b/src/fireedge/src/client/components/Cards/BackupJobCard.js @@ -20,13 +20,18 @@ import { Typography } from '@mui/material' import { Tr } from 'client/components/HOC' import { StatusCircle } from 'client/components/Status' import { rowStyles } from 'client/components/Tables/styles' +import MultipleTags from 'client/components/MultipleTags' +import { + getColorFromString, + getUniqueLabels, + timeFromMilliseconds, +} from 'client/models/Helper' import Timer from 'client/components/Timer' import { T } from 'client/constants' import { Group, HighPriority, Lock, User } from 'iconoir-react' import COLOR from 'client/constants/color' -import { timeFromMilliseconds } from 'client/models/Helper' import PropTypes from 'prop-types' const haveValues = function (object) { @@ -56,6 +61,7 @@ const BackupJobCard = memo( PRIORITY, LAST_BACKUP_TIME, LOCK, + TEMPLATE: { LABELS } = {}, } = template const time = useMemo(() => { @@ -110,6 +116,18 @@ const BackupJobCard = memo( } }, [OUTDATED_VMS, BACKING_UP_VMS, ERROR_VMS, LAST_BACKUP_TIME]) + const labels = useMemo( + () => + getUniqueLabels(LABELS).map((label) => ({ + text: label, + dataCy: `label-${label}`, + stateColor: getColorFromString(label), + onClick: onClickLabel, + onDelete: onDeleteLabel, + })), + [LABELS, onClickLabel, onDeleteLabel] + ) + return (
@@ -118,6 +136,7 @@ const BackupJobCard = memo( {NAME} {LOCK && } +
diff --git a/src/fireedge/src/client/components/Cards/HostCard.js b/src/fireedge/src/client/components/Cards/HostCard.js index 0369b0308c..5da21fb12b 100644 --- a/src/fireedge/src/client/components/Cards/HostCard.js +++ b/src/fireedge/src/client/components/Cards/HostCard.js @@ -14,7 +14,8 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ import PropTypes from 'prop-types' -import { memo, ReactElement } from 'react' +import { memo, ReactElement, useMemo } from 'react' +import MultipleTags from 'client/components/MultipleTags' import { Typography } from '@mui/material' import { ModernTv, Server } from 'iconoir-react' @@ -27,6 +28,8 @@ import { } from 'client/components/Status' import { rowStyles } from 'client/components/Tables/styles' +import { getColorFromString, getUniqueLabels } from 'client/models/Helper' + import { Host, HOST_THRESHOLD, T } from 'client/constants' import { getAllocatedInfo, getState } from 'client/models/Host' @@ -36,11 +39,33 @@ const HostCard = memo( * @param {Host} props.host - Host resource * @param {object} props.rootProps - Props to root component * @param {ReactElement} props.actions - Actions + * @param {function(string):Promise} [props.onClickLabel] - Callback to click label + * @param {function(string):Promise} [props.onDeleteLabel] - Callback to delete label * @returns {ReactElement} - Card */ - ({ host, rootProps, actions }) => { + ({ host, rootProps, actions, onClickLabel, onDeleteLabel }) => { const classes = rowStyles() - const { ID, NAME, IM_MAD, VM_MAD, HOST_SHARE, CLUSTER } = host + const { + ID, + NAME, + IM_MAD, + VM_MAD, + HOST_SHARE, + CLUSTER, + TEMPLATE: { LABELS } = {}, + } = host + + const labels = useMemo( + () => + getUniqueLabels(LABELS).map((label) => ({ + text: label, + dataCy: `label-${label}`, + stateColor: getColorFromString(label), + onClick: onClickLabel, + onDelete: onDeleteLabel, + })), + [LABELS, onClickLabel, onDeleteLabel] + ) const { percentCpuUsed, @@ -55,7 +80,7 @@ const HostCard = memo( const totalVms = [host?.VMS?.ID ?? []].flat().length || 0 const { color: stateColor, name: stateName } = getState(host) - const labels = [...new Set([IM_MAD, VM_MAD])] + const statusLabels = [...new Set([IM_MAD, VM_MAD])] return (
@@ -65,11 +90,14 @@ const HostCard = memo( {NAME} - - {labels.map((label) => ( + + {statusLabels.map((label) => ( ))} + + +
{`#${ID}`} @@ -113,6 +141,8 @@ HostCard.propTypes = { className: PropTypes.string, }), actions: PropTypes.any, + onClickLabel: PropTypes.func, + onDeleteLabel: PropTypes.func, } HostCard.displayName = 'HostCard' diff --git a/src/fireedge/src/client/components/Cards/NetworkCard.js b/src/fireedge/src/client/components/Cards/NetworkCard.js index 03466e61d2..8c2978609c 100644 --- a/src/fireedge/src/client/components/Cards/NetworkCard.js +++ b/src/fireedge/src/client/components/Cards/NetworkCard.js @@ -60,9 +60,10 @@ const NetworkCard = memo( * @param {object} props.rootProps - Props to root component * @param {function(string):Promise} [props.onDeleteLabel] - Callback to delete label * @param {ReactElement} [props.actions] - Actions + * @param {function(string):Promise} [props.onClickLabel] - Callback to click label * @returns {ReactElement} - Card */ - ({ network, rootProps, actions, onDeleteLabel }) => { + ({ network, rootProps, actions, onClickLabel, onDeleteLabel }) => { const classes = rowStyles() const { [RESOURCE_NAMES.VM]: vmView } = useViews() @@ -93,6 +94,7 @@ const NetworkCard = memo( getUniqueLabels(LABELS).map((label) => ({ text: label, stateColor: getColorFromString(label), + onClick: onClickLabel, onDelete: enableEditLabels && onDeleteLabel, })), [LABELS, enableEditLabels, onDeleteLabel] @@ -169,6 +171,7 @@ NetworkCard.propTypes = { rootProps: PropTypes.shape({ className: PropTypes.string, }), + onClickLabel: PropTypes.func, onDeleteLabel: PropTypes.func, actions: PropTypes.any, } diff --git a/src/fireedge/src/client/components/Cards/SecurityGroupCard.js b/src/fireedge/src/client/components/Cards/SecurityGroupCard.js index 862c94547f..766d0244bd 100644 --- a/src/fireedge/src/client/components/Cards/SecurityGroupCard.js +++ b/src/fireedge/src/client/components/Cards/SecurityGroupCard.js @@ -19,8 +19,10 @@ import PropTypes from 'prop-types' import { User, Group, PcCheck, PcNoEntry, PcWarning } from 'iconoir-react' import { Typography } from '@mui/material' +import MultipleTags from 'client/components/MultipleTags' import { rowStyles } from 'client/components/Tables/styles' import { SecurityGroup } from 'client/constants' +import { getColorFromString, getUniqueLabels } from 'client/models/Helper' const getTotalOfResources = (resources) => [resources?.ID ?? []].flat().length || 0 @@ -31,13 +33,23 @@ const SecurityGroupCard = memo( * @param {SecurityGroup} props.securityGroup - Security Group resource * @param {object} props.rootProps - Props to root component * @param {ReactElement} [props.actions] - Actions + * @param {function(string):Promise} [props.onDeleteLabel] - Callback to delete label + * @param {function(string):Promise} [props.onClickLabel] - Callback to click label * @returns {ReactElement} - Card */ - ({ securityGroup, rootProps, actions }) => { + ({ securityGroup, rootProps, actions, onClickLabel, onDeleteLabel }) => { const classes = rowStyles() - const { ID, NAME, UNAME, GNAME, UPDATED_VMS, OUTDATED_VMS, ERROR_VMS } = - securityGroup + const { + ID, + NAME, + UNAME, + GNAME, + UPDATED_VMS, + OUTDATED_VMS, + ERROR_VMS, + TEMPLATE: { LABELS } = {}, + } = securityGroup const [totalUpdatedVms, totalOutdatedVms, totalErrorVms] = useMemo( () => [ @@ -48,6 +60,17 @@ const SecurityGroupCard = memo( [UPDATED_VMS?.ID, OUTDATED_VMS?.ID, ERROR_VMS?.ID] ) + const labels = useMemo( + () => + getUniqueLabels(LABELS).map((label) => ({ + text: label, + stateColor: getColorFromString(label), + onClick: onClickLabel, + onDelete: onDeleteLabel, + })), + [LABELS, onDeleteLabel] + ) + return (
@@ -55,6 +78,8 @@ const SecurityGroupCard = memo( {NAME} + +
{`#${ID}`} @@ -91,6 +116,8 @@ SecurityGroupCard.propTypes = { rootProps: PropTypes.shape({ className: PropTypes.string, }), + onClickLabel: PropTypes.func, + onDeleteLabel: PropTypes.func, actions: PropTypes.any, } diff --git a/src/fireedge/src/client/components/Charts/MultiChart/helpers/subComponents/ChartRenderer.js b/src/fireedge/src/client/components/Charts/MultiChart/helpers/subComponents/ChartRenderer.js index f1e2ee2e4a..1aaa3bacce 100644 --- a/src/fireedge/src/client/components/Charts/MultiChart/helpers/subComponents/ChartRenderer.js +++ b/src/fireedge/src/client/components/Charts/MultiChart/helpers/subComponents/ChartRenderer.js @@ -119,8 +119,6 @@ export const ChartRenderer = ({ () => (coordinateType === 'POLAR' ? FormatPolarDataset(datasets) : null), [coordinateType, datasets] ) - console.log('polarDataset: ', polarDataset) - const chartConfig = useMemo( () => GetChartConfig( diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/backup/schema.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/backup/schema.js index 9fe899944e..b90f016db1 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/backup/schema.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/backup/schema.js @@ -127,7 +127,5 @@ export const FIELDS = [ INCREMENT_MODE, ] -console.log({ FIELDS }) - /** @type {ObjectSchema} Graphics schema */ export const BACKUP_SCHEMA = getObjectSchemaFromFields(FIELDS) diff --git a/src/fireedge/src/client/components/Tables/BackupJobs/columns.js b/src/fireedge/src/client/components/Tables/BackupJobs/columns.js index c2222956eb..b2c90fcaf7 100644 --- a/src/fireedge/src/client/components/Tables/BackupJobs/columns.js +++ b/src/fireedge/src/client/components/Tables/BackupJobs/columns.js @@ -22,8 +22,14 @@ const COLUMNS = [ { Header: T.ID, id: 'id', accessor: 'ID', sortType: 'number' }, { Header: T.Name, id: 'name', accessor: 'NAME' }, { Header: T.Locked, id: 'locked', accessor: 'LOCK' }, + { + Header: T.Label, + id: 'label', + accessor: 'TEMPLATE.LABELS', + filter: 'includesSome', + }, ] -COLUMNS.noFilterIds = ['id', 'name'] +COLUMNS.noFilterIds = ['id', 'name', 'label'] export default COLUMNS diff --git a/src/fireedge/src/client/components/Tables/Backups/columns.js b/src/fireedge/src/client/components/Tables/Backups/columns.js index 1d3bdd1cb1..d410fae392 100644 --- a/src/fireedge/src/client/components/Tables/Backups/columns.js +++ b/src/fireedge/src/client/components/Tables/Backups/columns.js @@ -16,13 +16,14 @@ /* eslint-disable jsdoc/require-jsdoc */ import { CategoryFilter } from 'client/components/Tables/Enhanced/Utils' import * as ImageModel from 'client/models/Image' +import { T } from 'client/constants' const getTotalOfResources = (resources) => [resources?.ID ?? []].flat().length || 0 -export default [ - { Header: 'ID', accessor: 'ID', sortType: 'number' }, - { Header: 'Name', accessor: 'NAME' }, +const COLUMNS = [ + { Header: 'ID', accessor: 'ID', id: 'id', sortType: 'number' }, + { Header: 'Name', id: 'name', accessor: 'NAME' }, { Header: 'Owner', accessor: 'UNAME' }, { Header: 'Group', accessor: 'GNAME' }, { Header: 'Locked', id: 'locked', accessor: 'LOCK' }, @@ -49,6 +50,12 @@ export default [ id: 'DISK_TYPE', accessor: (row) => ImageModel.getDiskType(row), }, + { + Header: T.Label, + id: 'label', + accessor: 'TEMPLATE.LABELS', + filter: 'includesSome', + }, { Header: 'Registration Time', accessor: 'REGTIME' }, { Header: 'Datastore', accessor: 'DATASTORE' }, { Header: 'Persistent', accessor: 'PERSISTENT' }, @@ -64,3 +71,7 @@ export default [ sortType: 'number', }, ] + +COLUMNS.noFilterIds = ['id', 'name', 'label'] + +export default COLUMNS diff --git a/src/fireedge/src/client/components/Tables/Backups/row.js b/src/fireedge/src/client/components/Tables/Backups/row.js index eb7ef864e8..22df097e30 100644 --- a/src/fireedge/src/client/components/Tables/Backups/row.js +++ b/src/fireedge/src/client/components/Tables/Backups/row.js @@ -15,6 +15,8 @@ * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ import PropTypes from 'prop-types' +import { useMemo, useCallback } from 'react' +import imageApi, { useUpdateImageMutation } from 'client/features/OneApi/image' import { Lock, @@ -26,6 +28,12 @@ import { Archive as DiskTypeIcon, } from 'iconoir-react' import { Typography } from '@mui/material' +import MultipleTags from 'client/components/MultipleTags' +import { + jsonToXml, + getUniqueLabels, + getColorFromString, +} from 'client/models/Helper' import Timer from 'client/components/Timer' import { StatusCircle, StatusChip } from 'client/components/Status' @@ -35,10 +43,31 @@ import { T } from 'client/constants' import * as ImageModel from 'client/models/Image' import * as Helper from 'client/models/Helper' -const Row = ({ original, value, ...props }) => { +const Row = ({ original, value, onClickLabel, ...props }) => { + const [update] = useUpdateImageMutation() + + const state = imageApi.endpoints.getImages.useQueryState(undefined, { + selectFromResult: ({ data = [] }) => + data.find((image) => +image.ID === +original.ID), + }) + + const memoImage = useMemo(() => state ?? original, [state, original]) + + const handleDeleteLabel = useCallback( + (label) => { + const currentLabels = memoImage.TEMPLATE?.LABELS?.split(',') + const newLabels = currentLabels.filter((l) => l !== label).join(',') + const newImageTemplate = { ...memoImage.TEMPLATE, LABELS: newLabels } + const templateXml = jsonToXml(newImageTemplate) + + update({ id: original.ID, template: templateXml, replace: 0 }) + }, + [memoImage.TEMPLATE?.LABELS, update] + ) + const classes = rowStyles() const { - ID, + id: ID, NAME, UNAME, GNAME, @@ -49,6 +78,7 @@ const Row = ({ original, value, ...props }) => { DATASTORE, TOTAL_VMS, RUNNING_VMS, + label: LABELS = [], } = value const { @@ -62,6 +92,17 @@ const Row = ({ original, value, ...props }) => { const time = Helper.timeFromMilliseconds(+REGTIME) + const multiTagLabels = useMemo( + () => + getUniqueLabels(LABELS).map((label) => ({ + text: label, + stateColor: getColorFromString(label), + onClick: onClickLabel, + onDelete: handleDeleteLabel, + })), + [LABELS, handleDeleteLabel, onClickLabel] + ) + return (
@@ -76,6 +117,9 @@ const Row = ({ original, value, ...props }) => { ))} + + +
{`#${ID}`} @@ -130,6 +174,7 @@ Row.propTypes = { value: PropTypes.object, isSelected: PropTypes.bool, handleClick: PropTypes.func, + onClickLabel: PropTypes.func, } export default Row diff --git a/src/fireedge/src/client/components/Tables/Datastores/index.js b/src/fireedge/src/client/components/Tables/Datastores/index.js index 7456d5b6a0..5c88d540c9 100644 --- a/src/fireedge/src/client/components/Tables/Datastores/index.js +++ b/src/fireedge/src/client/components/Tables/Datastores/index.js @@ -138,7 +138,7 @@ const DatastoresTable = (props) => { return ( data, [data])} + data={useMemo(() => data, [data, data?.TEMPLATE?.LABELS])} rootProps={rootProps} searchProps={searchProps} refetch={refetch} diff --git a/src/fireedge/src/client/components/Tables/Datastores/row.js b/src/fireedge/src/client/components/Tables/Datastores/row.js index 62d41f5966..6f37bb3ea5 100644 --- a/src/fireedge/src/client/components/Tables/Datastores/row.js +++ b/src/fireedge/src/client/components/Tables/Datastores/row.js @@ -16,7 +16,7 @@ import { memo, useCallback, useMemo } from 'react' import PropTypes from 'prop-types' -import api, { +import datastoreApi, { useUpdateDatastoreMutation, } from 'client/features/OneApi/datastore' import { DatastoreCard } from 'client/components/Cards' @@ -26,12 +26,21 @@ const Row = memo( ({ original, onClickLabel, ...props }) => { const [update] = useUpdateDatastoreMutation() - const state = api.endpoints.getDatastores.useQueryState(undefined, { - selectFromResult: ({ data = [] }) => - data.find((datastore) => +datastore.ID === +original.ID), - }) + const { + data: datastores, + error, + isLoading, + } = datastoreApi.endpoints.getDatastores.useQuery(undefined) - const memoDs = useMemo(() => state ?? original, [state, original]) + const datastore = useMemo( + () => datastores?.find((ds) => +ds.ID === +original.ID) ?? original, + [datastores, original] + ) + + const memoDs = useMemo( + () => datastore ?? original, + [datastore, original, update, isLoading, error, datastores] + ) const handleDeleteLabel = useCallback( (label) => { @@ -47,7 +56,7 @@ const Row = memo( return ( [resources?.ID ?? []].flat().length || 0 -export default [ - { Header: 'ID', accessor: 'ID', sortType: 'number' }, - { Header: 'Name', accessor: 'NAME' }, +const COLUMNS = [ + { Header: 'ID', id: 'id', accessor: 'ID', sortType: 'number' }, + { Header: 'Name', id: 'name', accessor: 'NAME' }, { Header: 'Owner', accessor: 'UNAME' }, { Header: 'Group', accessor: 'GNAME' }, { Header: 'Locked', id: 'locked', accessor: 'LOCK' }, @@ -64,3 +64,5 @@ export default [ sortType: 'number', }, ] + +export default COLUMNS diff --git a/src/fireedge/src/client/components/Tables/Groups/columns.js b/src/fireedge/src/client/components/Tables/Groups/columns.js index 4dc500465b..8340ab0d5f 100644 --- a/src/fireedge/src/client/components/Tables/Groups/columns.js +++ b/src/fireedge/src/client/components/Tables/Groups/columns.js @@ -17,7 +17,7 @@ const getTotalOfResources = (resources) => [resources?.ID ?? []].flat().length || 0 -export default [ +const COLUMNS = [ { Header: 'ID', accessor: 'ID', sortType: 'number' }, { Header: 'Name', accessor: 'NAME' }, { @@ -31,3 +31,5 @@ export default [ { Header: 'Network quota', accessor: 'NETWORK_QUOTA' }, { Header: 'Image quota', accessor: 'IMAGE_QUOTA' }, ] + +export default COLUMNS diff --git a/src/fireedge/src/client/components/Tables/Hosts/columns.js b/src/fireedge/src/client/components/Tables/Hosts/columns.js index 73abb8d108..dcee1a5c9f 100644 --- a/src/fireedge/src/client/components/Tables/Hosts/columns.js +++ b/src/fireedge/src/client/components/Tables/Hosts/columns.js @@ -16,12 +16,13 @@ /* eslint-disable jsdoc/require-jsdoc */ import { CategoryFilter } from 'client/components/Tables/Enhanced/Utils' import * as HostModel from 'client/models/Host' +import { T } from 'client/constants' const getTotalOfResources = (resources) => [resources?.ID ?? []].flat().length || 0 -export default [ - { Header: 'ID', accessor: 'ID', sortType: 'number' }, +const COLUMNS = [ + { Header: 'ID', accessor: 'ID', id: 'ID', sortType: 'number' }, { Header: 'Name', id: 'NAME', @@ -41,9 +42,15 @@ export default [ filter: 'includesValue', }, { Header: 'Cluster', accessor: 'CLUSTER' }, + { + Header: T.Label, + id: 'label', + accessor: 'TEMPLATE.LABELS', + filter: 'includesSome', + }, { Header: 'IM MAD', - id: 'IM_MAD', + id: 'im_mad', accessor: 'IM_MAD', disableFilters: false, Filter: ({ column }) => @@ -85,3 +92,7 @@ export default [ disableSortBy: true, }, ] + +COLUMNS.noFilterIds = ['id', 'name', 'label'] + +export default COLUMNS diff --git a/src/fireedge/src/client/components/Tables/Hosts/row.js b/src/fireedge/src/client/components/Tables/Hosts/row.js index 2cccee5d3d..da098b7291 100644 --- a/src/fireedge/src/client/components/Tables/Hosts/row.js +++ b/src/fireedge/src/client/components/Tables/Hosts/row.js @@ -13,21 +13,52 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { memo, useMemo } from 'react' +import { memo, useMemo, useCallback } from 'react' import PropTypes from 'prop-types' -import hostApi from 'client/features/OneApi/host' +import hostApi, { useUpdateHostMutation } from 'client/features/OneApi/host' import { HostCard } from 'client/components/Cards' +import { jsonToXml } from 'client/models/Helper' const Row = memo( - ({ original, value, ...props }) => { - const state = hostApi.endpoints.getHosts.useQueryState(undefined, { - selectFromResult: ({ data = [] }) => - data.find((host) => +host?.ID === +original.ID), - }) + ({ original, value, onClickLabel, ...props }) => { + const [update] = useUpdateHostMutation() - const memoHost = useMemo(() => state ?? original, [state, original]) + const { + data: hosts, + error, + isLoading, + } = hostApi.endpoints.getHosts.useQuery(undefined) - return + const host = useMemo( + () => hosts?.find((h) => +h.ID === +original.ID) ?? original, + [hosts, original] + ) + + const memoHost = useMemo( + () => host ?? original, + [host, original, update, isLoading, error, hosts] + ) + + const handleDeleteLabel = useCallback( + (label) => { + const currentLabels = memoHost?.TEMPLATE?.LABELS.split(',') + const newLabels = currentLabels.filter((l) => l !== label).join(',') + const newHostTemplate = { ...memoHost.TEMPLATE, LABELS: newLabels } + const templateXml = jsonToXml(newHostTemplate) + + update({ id: original.ID, template: templateXml, replace: 0 }) + }, + [memoHost.TEMPLATE?.LABELS, update] + ) + + return ( + + ) }, (prev, next) => prev.className === next.className ) @@ -38,6 +69,7 @@ Row.propTypes = { isSelected: PropTypes.bool, className: PropTypes.string, handleClick: PropTypes.func, + onClickLabel: PropTypes.func, } Row.displayName = 'HostRow' diff --git a/src/fireedge/src/client/components/Tables/Images/columns.js b/src/fireedge/src/client/components/Tables/Images/columns.js index 1d3bdd1cb1..c8483556aa 100644 --- a/src/fireedge/src/client/components/Tables/Images/columns.js +++ b/src/fireedge/src/client/components/Tables/Images/columns.js @@ -16,13 +16,14 @@ /* eslint-disable jsdoc/require-jsdoc */ import { CategoryFilter } from 'client/components/Tables/Enhanced/Utils' import * as ImageModel from 'client/models/Image' +import { T } from 'client/constants' const getTotalOfResources = (resources) => [resources?.ID ?? []].flat().length || 0 -export default [ - { Header: 'ID', accessor: 'ID', sortType: 'number' }, - { Header: 'Name', accessor: 'NAME' }, +const COLUMNS = [ + { Header: 'ID', id: 'id', accessor: 'ID', sortType: 'number' }, + { Header: 'Name', id: 'name', accessor: 'NAME' }, { Header: 'Owner', accessor: 'UNAME' }, { Header: 'Group', accessor: 'GNAME' }, { Header: 'Locked', id: 'locked', accessor: 'LOCK' }, @@ -39,6 +40,12 @@ export default [ }), filter: 'includesValue', }, + { + Header: T.Label, + id: 'label', + accessor: 'TEMPLATE.LABELS', + filter: 'includesSome', + }, { Header: 'Type', id: 'TYPE', @@ -64,3 +71,6 @@ export default [ sortType: 'number', }, ] + +COLUMNS.noFilterIds = ['id', 'name', 'type', 'label'] +export default COLUMNS diff --git a/src/fireedge/src/client/components/Tables/Images/row.js b/src/fireedge/src/client/components/Tables/Images/row.js index 8ee81b7223..dba419de8e 100644 --- a/src/fireedge/src/client/components/Tables/Images/row.js +++ b/src/fireedge/src/client/components/Tables/Images/row.js @@ -15,6 +15,7 @@ * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ import PropTypes from 'prop-types' +import { useMemo, useCallback } from 'react' import { Lock, @@ -26,19 +27,27 @@ import { Archive as DiskTypeIcon, } from 'iconoir-react' import { Typography } from '@mui/material' +import MultipleTags from 'client/components/MultipleTags' +import imageApi, { useUpdateImageMutation } from 'client/features/OneApi/image' import Timer from 'client/components/Timer' import { StatusCircle, StatusChip } from 'client/components/Status' import { rowStyles } from 'client/components/Tables/styles' import { T } from 'client/constants' +import { + jsonToXml, + getUniqueLabels, + getColorFromString, +} from 'client/models/Helper' import * as ImageModel from 'client/models/Image' import * as Helper from 'client/models/Helper' -const Row = ({ original, value, ...props }) => { +const Row = ({ original, value, onClickLabel, ...props }) => { + const [update] = useUpdateImageMutation() const classes = rowStyles() const { - ID, + id: ID, NAME, UNAME, GNAME, @@ -50,14 +59,45 @@ const Row = ({ original, value, ...props }) => { DATASTORE, TOTAL_VMS, RUNNING_VMS, + label: LABELS = [], } = value + const state = imageApi.endpoints.getImages.useQueryState(undefined, { + selectFromResult: ({ data = [] }) => + data.find((image) => +image.ID === +original.ID), + }) + + const memoImage = useMemo(() => state ?? original, [state, original]) + + const handleDeleteLabel = useCallback( + (label) => { + const currentLabels = memoImage.TEMPLATE?.LABELS?.split(',') + const newLabels = currentLabels.filter((l) => l !== label).join(',') + const newImageTemplate = { ...memoImage.TEMPLATE, LABELS: newLabels } + const templateXml = jsonToXml(newImageTemplate) + + update({ id: original.ID, template: templateXml, replace: 0 }) + }, + [memoImage.TEMPLATE?.LABELS, update] + ) + const labels = [...new Set([TYPE])].filter(Boolean) const { color: stateColor, name: stateName } = ImageModel.getState(original) const time = Helper.timeFromMilliseconds(+REGTIME) + const multiTagLabels = useMemo( + () => + getUniqueLabels(LABELS).map((label) => ({ + text: label, + stateColor: getColorFromString(label), + onClick: onClickLabel, + onDelete: handleDeleteLabel, + })), + [LABELS, handleDeleteLabel, onClickLabel] + ) + return (
@@ -72,6 +112,9 @@ const Row = ({ original, value, ...props }) => { ))} + + +
{`#${ID}`} @@ -126,6 +169,7 @@ Row.propTypes = { value: PropTypes.object, isSelected: PropTypes.bool, handleClick: PropTypes.func, + onClickLabel: PropTypes.func, } export default Row diff --git a/src/fireedge/src/client/components/Tables/SecurityGroups/columns.js b/src/fireedge/src/client/components/Tables/SecurityGroups/columns.js index 3aa2d4a940..9b4b47a6f6 100644 --- a/src/fireedge/src/client/components/Tables/SecurityGroups/columns.js +++ b/src/fireedge/src/client/components/Tables/SecurityGroups/columns.js @@ -14,12 +14,13 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ +import { T } from 'client/constants' const getTotalOfResources = (resources) => [resources?.ID ?? []].flat().length || 0 -export default [ - { Header: 'ID', accessor: 'ID', sortType: 'number' }, - { Header: 'Name', accessor: 'NAME' }, +const COLUMNS = [ + { Header: 'ID', accessor: 'ID', id: 'id', sortType: 'number' }, + { Header: 'Name', id: 'name', accessor: 'NAME' }, { Header: 'Owner', accessor: 'UNAME' }, { Header: 'Group', accessor: 'GNAME' }, { @@ -28,6 +29,12 @@ export default [ accessor: (row) => getTotalOfResources(row?.UPDATED_VMS), sortType: 'number', }, + { + Header: T.Label, + id: 'label', + accessor: 'TEMPLATE.LABELS', + filter: 'includesSome', + }, { Header: 'Outdated VMs', id: 'OUTDATED_VMS', @@ -47,3 +54,6 @@ export default [ sortType: 'number', }, ] + +COLUMNS.noFilterIds = ['id', 'name', 'label'] +export default COLUMNS diff --git a/src/fireedge/src/client/components/Tables/SecurityGroups/row.js b/src/fireedge/src/client/components/Tables/SecurityGroups/row.js index 73f56da947..a833f82633 100644 --- a/src/fireedge/src/client/components/Tables/SecurityGroups/row.js +++ b/src/fireedge/src/client/components/Tables/SecurityGroups/row.js @@ -13,20 +13,56 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { memo } from 'react' +import { memo, useMemo, useCallback } from 'react' import PropTypes from 'prop-types' -import secGroupApi from 'client/features/OneApi/securityGroup' +import secGroupApi, { + useUpdateSecGroupMutation, +} from 'client/features/OneApi/securityGroup' import { SecurityGroupCard } from 'client/components/Cards' +import { jsonToXml } from 'client/models/Helper' const Row = memo( - ({ original, value, ...props }) => { - const state = secGroupApi.endpoints.getSecGroups.useQueryState(undefined, { - selectFromResult: ({ data = [] }) => - data.find((secgroup) => +secgroup.ID === +original.ID), - }) + ({ original, value, onClickLabel, ...props }) => { + const [update] = useUpdateSecGroupMutation() + + const { + data: secgroups, + error, + isLoading, + } = secGroupApi.endpoints.getSecGroups.useQuery(undefined) + + const secGroup = useMemo( + () => secgroups?.find((sg) => +sg.ID === +original.ID) ?? original, + [secgroups, original] + ) + + const memoSecGroup = useMemo( + () => secGroup ?? original, + [secGroup, original, update, isLoading, error, secgroups] + ) + + const handleDeleteLabel = useCallback( + (label) => { + const currentLabels = memoSecGroup.TEMPLATE?.LABELS?.split(',') + const newLabels = currentLabels.filter((l) => l !== label).join(',') + const newSecGroupTemplate = { + ...memoSecGroup.TEMPLATE, + LABELS: newLabels, + } + const templateXml = jsonToXml(newSecGroupTemplate) + + update({ id: original.ID, template: templateXml, replace: 0 }) + }, + [memoSecGroup.TEMPLATE?.LABELS, update] + ) return ( - + ) }, (prev, next) => prev.className === next.className @@ -37,6 +73,7 @@ Row.propTypes = { value: PropTypes.object, isSelected: PropTypes.bool, handleClick: PropTypes.func, + onClickLabel: PropTypes.func, } Row.displayName = 'SecurityGroupRow' diff --git a/src/fireedge/src/client/components/Tables/VNetworks/columns.js b/src/fireedge/src/client/components/Tables/VNetworks/columns.js index cc9c58783b..045e50cc17 100644 --- a/src/fireedge/src/client/components/Tables/VNetworks/columns.js +++ b/src/fireedge/src/client/components/Tables/VNetworks/columns.js @@ -27,6 +27,12 @@ const COLUMNS = [ { Header: T.Group, id: 'group', accessor: 'GNAME' }, { Header: T.Locked, id: 'locked', accessor: 'LOCK' }, { Header: T.Driver, id: 'vn_mad', accessor: getVNManager }, + { + Header: T.Label, + id: 'label', + accessor: 'TEMPLATE.LABELS', + filter: 'includesSome', + }, { Header: T.UsedLeases, id: 'used_leases', diff --git a/src/fireedge/src/client/components/Tables/VNetworks/row.js b/src/fireedge/src/client/components/Tables/VNetworks/row.js index 79dde44980..cb988aa875 100644 --- a/src/fireedge/src/client/components/Tables/VNetworks/row.js +++ b/src/fireedge/src/client/components/Tables/VNetworks/row.js @@ -13,26 +13,49 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { memo, useMemo } from 'react' +import { memo, useMemo, useCallback } from 'react' import PropTypes from 'prop-types' +import { jsonToXml } from 'client/models/Helper' -import api from 'client/features/OneApi/network' +import api, { useUpdateVNetMutation } from 'client/features/OneApi/network' import { NetworkCard } from 'client/components/Cards' const Row = memo( ({ original, value, onClickLabel, ...props }) => { - const state = api.endpoints.getVNetworks.useQueryState(undefined, { - selectFromResult: ({ data = [] }) => - data.find((network) => +network.ID === +original.ID), - }) + const [update] = useUpdateVNetMutation() + const { + data: vnetworks, + error, + isLoading, + } = api.endpoints.getVNetworks.useQuery(undefined) - const memoNetwork = useMemo(() => state ?? original, [state, original]) + const vnetwork = useMemo( + () => vnetworks?.find((vnet) => +vnet.ID === +original.ID) ?? original, + [vnetworks, original] + ) + + const memoNetwork = useMemo( + () => vnetwork ?? original, + [vnetwork, original, update, isLoading, error, vnetworks] + ) + const handleDeleteLabel = useCallback( + (label) => { + const currentLabels = memoNetwork.TEMPLATE?.LABELS?.split(',') + const newLabels = currentLabels.filter((l) => l !== label).join(',') + const newVnetTemplate = { ...memoNetwork.TEMPLATE, LABELS: newLabels } + const templateXml = jsonToXml(newVnetTemplate) + + update({ id: original.ID, template: templateXml, replace: 0 }) + }, + [memoNetwork.TEMPLATE?.LABELS, update] + ) return ( ) }, diff --git a/src/fireedge/src/client/components/Tables/VmTemplates/columns.js b/src/fireedge/src/client/components/Tables/VmTemplates/columns.js index 4675e9e766..8d4975889d 100644 --- a/src/fireedge/src/client/components/Tables/VmTemplates/columns.js +++ b/src/fireedge/src/client/components/Tables/VmTemplates/columns.js @@ -24,6 +24,12 @@ const COLUMNS = [ { Header: T.Owner, id: 'owner', accessor: 'UNAME' }, { Header: T.Group, id: 'group', accessor: 'GNAME' }, { Header: T.RegistrationTime, id: 'time', accessor: 'REGTIME' }, + { + Header: T.Label, + id: 'label', + accessor: 'TEMPLATE.LABELS', + filter: 'includesSome', + }, { Header: T.Locked, id: 'locked', accessor: 'LOCK' }, { Header: T.Logo, id: 'logo', accessor: 'TEMPLATE.LOGO' }, { Header: T.VirtualRouter, id: 'vrouter', accessor: 'TEMPLATE.VROUTER' }, diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js index d0e8461d20..77329f6f45 100644 --- a/src/fireedge/src/client/constants/translates.js +++ b/src/fireedge/src/client/constants/translates.js @@ -30,7 +30,7 @@ module.exports = { ApplyLabels: 'Apply labels', Apply: 'Apply', Label: 'Label', - NoLabels: 'NoLabels', + NoLabels: 'No labels', All: 'All', On: 'On', ToggleAllSelectedCardsCurrentPage: diff --git a/src/fireedge/src/client/containers/Backups/index.js b/src/fireedge/src/client/containers/Backups/index.js index 64af077c27..7a78177285 100644 --- a/src/fireedge/src/client/containers/Backups/index.js +++ b/src/fireedge/src/client/containers/Backups/index.js @@ -29,7 +29,10 @@ import { BackupsTable } from 'client/components/Tables' import BackupActions from 'client/components/Tables/Backups/actions' import BackupTabs from 'client/components/Tabs/Backup' import { Image, T } from 'client/constants' -import { useLazyGetImageQuery } from 'client/features/OneApi/image' +import { + useLazyGetImageQuery, + useUpdateImageMutation, +} from 'client/features/OneApi/image' /** * Displays a list of Backups with a split pane between the list and selected row(s). @@ -50,6 +53,7 @@ function Backups() { {hasSelectedRows && ( diff --git a/src/fireedge/src/client/containers/Images/index.js b/src/fireedge/src/client/containers/Images/index.js index f523560d6a..4701f69d0f 100644 --- a/src/fireedge/src/client/containers/Images/index.js +++ b/src/fireedge/src/client/containers/Images/index.js @@ -29,7 +29,10 @@ import { ImagesTable } from 'client/components/Tables' import ImageActions from 'client/components/Tables/Images/actions' import ImageTabs from 'client/components/Tabs/Image' import { Image, T } from 'client/constants' -import { useLazyGetImageQuery } from 'client/features/OneApi/image' +import { + useUpdateImageMutation, + useLazyGetImageQuery, +} from 'client/features/OneApi/image' /** * Displays a list of Images with a split pane between the list and selected row(s). @@ -50,6 +53,7 @@ function Images() { {hasSelectedRows && ( diff --git a/src/fireedge/src/client/containers/SecurityGroups/index.js b/src/fireedge/src/client/containers/SecurityGroups/index.js index ef9a2435ec..cccbe17e74 100644 --- a/src/fireedge/src/client/containers/SecurityGroups/index.js +++ b/src/fireedge/src/client/containers/SecurityGroups/index.js @@ -21,7 +21,10 @@ import Cancel from 'iconoir-react/dist/Cancel' import { Typography, Box, Stack, Chip } from '@mui/material' import { Row } from 'react-table' -import { useLazyGetSecGroupQuery } from 'client/features/OneApi/securityGroup' +import { + useLazyGetSecGroupQuery, + useUpdateSecGroupMutation, +} from 'client/features/OneApi/securityGroup' import { SecurityGroupsTable } from 'client/components/Tables' import SecurityGroupsActions from 'client/components/Tables/SecurityGroups/actions' import SecurityGroupsTabs from 'client/components/Tabs/SecurityGroup' @@ -50,6 +53,7 @@ function SecurityGroups() { {hasSelectedRows && ( diff --git a/src/fireedge/src/client/features/OneApi/datastore.js b/src/fireedge/src/client/features/OneApi/datastore.js index c0252001cb..40ff703b03 100644 --- a/src/fireedge/src/client/features/OneApi/datastore.js +++ b/src/fireedge/src/client/features/OneApi/datastore.js @@ -158,7 +158,10 @@ const datastoreApi = oneApi.injectEndpoints({ return { params, command } }, - invalidatesTags: (_, __, { id }) => [{ type: DATASTORE, id }], + invalidatesTags: (_, __, { id }) => [ + { type: DATASTORE, id }, + { type: DATASTORE_POOL, id }, + ], async onQueryStarted(params, { dispatch, queryFulfilled }) { try { const patchDatastore = dispatch( diff --git a/src/fireedge/src/client/features/OneApi/image.js b/src/fireedge/src/client/features/OneApi/image.js index 17b5bf6afa..0ecede26ee 100644 --- a/src/fireedge/src/client/features/OneApi/image.js +++ b/src/fireedge/src/client/features/OneApi/image.js @@ -31,6 +31,11 @@ import { IMAGE_TYPES_FOR_BACKUPS, } from 'client/constants' import { getType } from 'client/models/Image' +import { + updateResourceOnPool, + removeResourceOnPool, + updateTemplateOnResource, +} from 'client/features/OneApi/common' const { IMAGE } = ONE_RESOURCES const { IMAGE_POOL } = ONE_RESOURCES_POOL @@ -214,6 +219,28 @@ const imageApi = oneApi.injectEndpoints({ imageApi.util.updateQueryData('getImages', undefined, updateFn), resource: 'IMAGE', }), + async onQueryStarted({ id }, { dispatch, queryFulfilled }) { + try { + const { data: resourceFromQuery } = await queryFulfilled + + dispatch( + imageApi.util.updateQueryData( + 'getImages', + undefined, + updateResourceOnPool({ id, resourceFromQuery }) + ) + ) + } catch { + // if the query fails, we want to remove the resource from the pool + dispatch( + imageApi.util.updateQueryData( + 'getImages', + undefined, + removeResourceOnPool({ id }) + ) + ) + } + }, }), allocateImage: builder.mutation({ /** @@ -383,7 +410,34 @@ const imageApi = oneApi.injectEndpoints({ return { params, command } }, - invalidatesTags: (_, __, { id }) => [{ type: IMAGE, id }], + invalidatesTags: (_, __, { id }) => [ + { type: IMAGE, id }, + { type: IMAGE_POOL, id }, + ], + async onQueryStarted(params, { dispatch, queryFulfilled }) { + try { + const patchImage = dispatch( + imageApi.util.updateQueryData( + 'getImage', + { id: params.id }, + updateTemplateOnResource(params) + ) + ) + + const patchImages = dispatch( + imageApi.util.updateQueryData( + 'getImages', + undefined, + updateTemplateOnResource(params) + ) + ) + + queryFulfilled.catch(() => { + patchImage.undo() + patchImages.undo() + }) + } catch {} + }, }), changeImagePermissions: builder.mutation({ /** @@ -598,3 +652,5 @@ export const { useUploadImageMutation, useRestoreBackupMutation, } = imageApi + +export default imageApi diff --git a/src/fireedge/src/client/features/OneApi/network.js b/src/fireedge/src/client/features/OneApi/network.js index 293a583782..17376ad55e 100644 --- a/src/fireedge/src/client/features/OneApi/network.js +++ b/src/fireedge/src/client/features/OneApi/network.js @@ -304,7 +304,10 @@ const vNetworkApi = oneApi.injectEndpoints({ return { params, command } }, - invalidatesTags: (_, __, { id }) => [{ type: VNET, id }], + invalidatesTags: (_, __, { id }) => [ + { type: VNET, id }, + { type: VNET_POOL, id }, + ], async onQueryStarted(params, { dispatch, queryFulfilled }) { try { const patchVNet = dispatch( diff --git a/src/fireedge/src/client/features/OneApi/securityGroup.js b/src/fireedge/src/client/features/OneApi/securityGroup.js index 30ff2bde52..f34a97d58b 100644 --- a/src/fireedge/src/client/features/OneApi/securityGroup.js +++ b/src/fireedge/src/client/features/OneApi/securityGroup.js @@ -19,6 +19,10 @@ import { ONE_RESOURCES, ONE_RESOURCES_POOL, } from 'client/features/OneApi' +import { + removeResourceOnPool, + updateResourceOnPool, +} from 'client/features/OneApi/common' import { FilterFlag, Permission } from 'client/constants' const { SECURITYGROUP } = ONE_RESOURCES @@ -74,6 +78,26 @@ const securityGroupApi = oneApi.injectEndpoints({ }, transformResponse: (data) => data?.SECURITY_GROUP ?? {}, providesTags: (_, __, { id }) => [{ type: SECURITYGROUP, id }], + async onQueryStarted({ id }, { dispatch, queryFulfilled }) { + try { + const { data: resourceFromQuery } = await queryFulfilled + dispatch( + securityGroupApi.util.updateQueryData( + 'getSecGroups', + undefined, + updateResourceOnPool({ id, resourceFromQuery }) + ) + ) + } catch { + dispatch( + securityGroupApi.util.updateQueryData( + 'getSecGroups', + undefined, + removeResourceOnPool({ id }) + ) + ) + } + }, }), renameSecGroup: builder.mutation({ /** @@ -218,7 +242,10 @@ const securityGroupApi = oneApi.injectEndpoints({ return { params, command } }, - invalidatesTags: (_, __, { id }) => [{ type: SECURITYGROUP, id }], + invalidatesTags: (_, __, { id }) => [ + { type: SECURITYGROUP, id }, + { type: SECURITYGROUP_POOL, id }, + ], }), commitSegGroup: builder.mutation({ /** diff --git a/src/fireedge/src/client/models/Helper.js b/src/fireedge/src/client/models/Helper.js index 17b3d77027..4732505483 100644 --- a/src/fireedge/src/client/models/Helper.js +++ b/src/fireedge/src/client/models/Helper.js @@ -532,16 +532,27 @@ export const userInputsToObject = (userInputs) => * @param {string} labels - List of labels separated by comma * @returns {string[]} List of unique labels */ -export const getUniqueLabels = (labels) => - labels - ?.split(',') - ?.filter( - (label, _, list) => - label !== '' && !list.some((element) => element.startsWith(`${label}/`)) - ) - ?.sort((a, b) => - a.localeCompare(b, undefined, { numeric: true, ignorePunctuation: true }) - ) ?? [] +export const getUniqueLabels = (labels) => { + if (labels?.length < 1) { + return [] + } + + return ( + labels + ?.split(',') + ?.filter( + (label, _, list) => + label !== '' && + !list.some((element) => element.startsWith(`${label}/`)) + ) + ?.sort((a, b) => + a.localeCompare(b, undefined, { + numeric: true, + ignorePunctuation: true, + }) + ) ?? [] + ) +} // The number 16,777,215 is the total possible combinations // of RGB(255,255,255) which is 32 bit color