1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-21 14:50:08 +03:00

F OpenNebula/one#6362: Add missing label selectors (#2844)

This commit is contained in:
vichansson 2023-11-28 17:26:33 +02:00 committed by GitHub
parent ec64ac4457
commit 25e3529f53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 527 additions and 86 deletions

View File

@ -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 (
<div {...rootProps} data-cy={`backupjob-${ID}`}>
<div className={classes.main}>
@ -118,6 +136,7 @@ const BackupJobCard = memo(
<Typography component="span">{NAME}</Typography>
<span className={classes.labels}>
{LOCK && <Lock data-cy="lock" />}
<MultipleTags tags={labels} />
</span>
</div>
<div className={classes.caption}>

View File

@ -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 (
<div {...rootProps} data-cy={`host-${ID}`}>
@ -65,11 +90,14 @@ const HostCard = memo(
<Typography noWrap component="span">
{NAME}
</Typography>
<span className={classes.labels}>
{labels.map((label) => (
<span className={classes.statusLabels}>
{statusLabels.map((label) => (
<StatusChip key={label} text={label} />
))}
</span>
<span className={classes.labels}>
<MultipleTags tags={labels} />
</span>
</div>
<div className={classes.caption}>
<span>{`#${ID}`}</span>
@ -113,6 +141,8 @@ HostCard.propTypes = {
className: PropTypes.string,
}),
actions: PropTypes.any,
onClickLabel: PropTypes.func,
onDeleteLabel: PropTypes.func,
}
HostCard.displayName = 'HostCard'

View File

@ -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,
}

View File

@ -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 (
<div {...rootProps} data-cy={`secgroup-${ID}`}>
<div className={classes.main}>
@ -55,6 +78,8 @@ const SecurityGroupCard = memo(
<Typography noWrap component="span">
{NAME}
</Typography>
<MultipleTags tags={labels} />
</div>
<div className={classes.caption}>
<span>{`#${ID}`}</span>
@ -91,6 +116,8 @@ SecurityGroupCard.propTypes = {
rootProps: PropTypes.shape({
className: PropTypes.string,
}),
onClickLabel: PropTypes.func,
onDeleteLabel: PropTypes.func,
actions: PropTypes.any,
}

View File

@ -119,8 +119,6 @@ export const ChartRenderer = ({
() => (coordinateType === 'POLAR' ? FormatPolarDataset(datasets) : null),
[coordinateType, datasets]
)
console.log('polarDataset: ', polarDataset)
const chartConfig = useMemo(
() =>
GetChartConfig(

View File

@ -127,7 +127,5 @@ export const FIELDS = [
INCREMENT_MODE,
]
console.log({ FIELDS })
/** @type {ObjectSchema} Graphics schema */
export const BACKUP_SCHEMA = getObjectSchemaFromFields(FIELDS)

View File

@ -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

View File

@ -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

View File

@ -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 (
<div {...props} data-cy={`image-${ID}`}>
<div className={classes.main}>
@ -76,6 +117,9 @@ const Row = ({ original, value, ...props }) => {
<StatusChip key={label} text={label} />
))}
</span>
<span className={classes.labels}>
<MultipleTags tags={multiTagLabels} />
</span>
</div>
<div className={classes.caption}>
<span>{`#${ID}`}</span>
@ -130,6 +174,7 @@ Row.propTypes = {
value: PropTypes.object,
isSelected: PropTypes.bool,
handleClick: PropTypes.func,
onClickLabel: PropTypes.func,
}
export default Row

View File

@ -138,7 +138,7 @@ const DatastoresTable = (props) => {
return (
<EnhancedTable
columns={columns}
data={useMemo(() => data, [data])}
data={useMemo(() => data, [data, data?.TEMPLATE?.LABELS])}
rootProps={rootProps}
searchProps={searchProps}
refetch={refetch}

View File

@ -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 (
<DatastoreCard
datastore={state ?? original}
datastore={memoDs}
onClickLabel={onClickLabel}
onDeleteLabel={handleDeleteLabel}
rootProps={props}

View File

@ -20,9 +20,9 @@ import * as ImageModel from 'client/models/Image'
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' },
@ -64,3 +64,5 @@ export default [
sortType: 'number',
},
]
export default COLUMNS

View File

@ -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

View File

@ -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

View File

@ -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 <HostCard host={memoHost} rootProps={props} />
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 (
<HostCard
host={memoHost}
rootProps={props}
onClickLabel={onClickLabel}
onDeleteLabel={handleDeleteLabel}
/>
)
},
(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'

View File

@ -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

View File

@ -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 (
<div {...props} data-cy={`image-${ID}`}>
<div className={classes.main}>
@ -72,6 +112,9 @@ const Row = ({ original, value, ...props }) => {
<StatusChip key={label} text={label} />
))}
</span>
<span className={classes.labels}>
<MultipleTags tags={multiTagLabels} />
</span>
</div>
<div className={classes.caption}>
<span>{`#${ID}`}</span>
@ -126,6 +169,7 @@ Row.propTypes = {
value: PropTypes.object,
isSelected: PropTypes.bool,
handleClick: PropTypes.func,
onClickLabel: PropTypes.func,
}
export default Row

View File

@ -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

View File

@ -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 (
<SecurityGroupCard securityGroup={state ?? original} rootProps={props} />
<SecurityGroupCard
securityGroup={memoSecGroup}
rootProps={props}
onClickLabel={onClickLabel}
onDeleteLabel={handleDeleteLabel}
/>
)
},
(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'

View File

@ -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',

View File

@ -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 (
<NetworkCard
network={memoNetwork}
rootProps={props}
onClickLabel={onClickLabel}
onDeleteLabel={handleDeleteLabel}
/>
)
},

View File

@ -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' },

View File

@ -30,7 +30,7 @@ module.exports = {
ApplyLabels: 'Apply labels',
Apply: 'Apply',
Label: 'Label',
NoLabels: 'NoLabels',
NoLabels: 'No labels',
All: 'All',
On: 'On',
ToggleAllSelectedCardsCurrentPage:

View File

@ -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() {
<BackupsTable
onSelectedRowsChange={onSelectedRowsChange}
globalActions={actions}
useUpdateMutation={useUpdateImageMutation}
/>
{hasSelectedRows && (

View File

@ -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() {
<ImagesTable
onSelectedRowsChange={onSelectedRowsChange}
globalActions={actions}
useUpdateMutation={useUpdateImageMutation}
/>
{hasSelectedRows && (

View File

@ -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() {
<SecurityGroupsTable
onSelectedRowsChange={onSelectedRowsChange}
globalActions={actions}
useUpdateMutation={useUpdateSecGroupMutation}
/>
{hasSelectedRows && (

View File

@ -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(

View File

@ -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

View File

@ -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(

View File

@ -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({
/**

View File

@ -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