mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-29 18:50:08 +03:00
parent
723854107b
commit
41c956f143
@ -37,9 +37,12 @@ actions:
|
||||
|
||||
filters:
|
||||
label: true
|
||||
owner: true
|
||||
group: true
|
||||
state: true
|
||||
type: true
|
||||
marketplace: true
|
||||
zone: true
|
||||
|
||||
# Info Tabs - Which info tabs are used to show extended information
|
||||
|
||||
|
@ -29,8 +29,6 @@ actions:
|
||||
deploy: true
|
||||
migrate: true
|
||||
migrate_live: true
|
||||
migrate_poff: false
|
||||
migrate_poff_hard: false
|
||||
hold: true
|
||||
release: true
|
||||
suspend: true
|
||||
@ -56,13 +54,19 @@ actions:
|
||||
vnc: true
|
||||
ssh: true
|
||||
rdp: true
|
||||
edit_labels: true
|
||||
|
||||
# Filters - List of criteria to filter the resources
|
||||
|
||||
filters:
|
||||
label: true
|
||||
state: true
|
||||
|
||||
owner: true
|
||||
group: true
|
||||
type: true
|
||||
locked: true
|
||||
ips: true
|
||||
hostname: true
|
||||
|
||||
# Info Tabs - Which info tabs are used to show extended information
|
||||
|
||||
|
@ -37,11 +37,16 @@ actions:
|
||||
unlock: true
|
||||
share: true
|
||||
unshare: true
|
||||
edit_labels: true
|
||||
|
||||
# Filters - List of criteria to filter the resources
|
||||
|
||||
filters:
|
||||
label: true
|
||||
owner: true
|
||||
group: true
|
||||
locked: true
|
||||
vrouter: true
|
||||
|
||||
|
||||
# Info Tabs - Which info tabs are used to show extended information
|
||||
|
@ -12,7 +12,7 @@ vcenter_prepend_command: ''
|
||||
# Prepend for sunstone commands
|
||||
sunstone_prepend: ''
|
||||
|
||||
# Directory to store temp files when uploading images
|
||||
# Directory to store temporal files when uploading images
|
||||
tmpdir: /var/tmp
|
||||
|
||||
################################################################################
|
||||
|
@ -25,11 +25,10 @@ resource_name: "VM"
|
||||
|
||||
actions:
|
||||
create_dialog: true
|
||||
create_app_dialog: true # reference to create_dialog in marketplace-app-tab.yaml
|
||||
deploy: true
|
||||
migrate: true
|
||||
migrate_live: true
|
||||
migrate_poff: false
|
||||
migrate_poff_hard: false
|
||||
hold: true
|
||||
release: true
|
||||
suspend: true
|
||||
@ -55,12 +54,19 @@ actions:
|
||||
vnc: true
|
||||
ssh: true
|
||||
rdp: true
|
||||
edit_labels: false
|
||||
|
||||
# Filters - List of criteria to filter the resources
|
||||
|
||||
filters:
|
||||
label: true
|
||||
state: true
|
||||
owner: true
|
||||
group: true
|
||||
type: true
|
||||
locked: true
|
||||
ips: true
|
||||
hostname: true
|
||||
|
||||
|
||||
# Info Tabs - Which info tabs are used to show extended information
|
||||
@ -130,6 +136,7 @@ info-tabs:
|
||||
enabled: true
|
||||
not_on:
|
||||
- vcenter
|
||||
- firecracker
|
||||
|
||||
network:
|
||||
enabled: true
|
||||
|
@ -28,6 +28,7 @@ actions:
|
||||
import_dialog: false
|
||||
update_dialog: true
|
||||
instantiate_dialog: true
|
||||
create_app_dialog: false # reference to create_dialog in marketplace-app-tab.yaml
|
||||
clone: true
|
||||
delete: true
|
||||
chown: false
|
||||
@ -36,11 +37,16 @@ actions:
|
||||
unlock: true
|
||||
share: true
|
||||
unshare: true
|
||||
edit_labels: false
|
||||
|
||||
# Filters - List of criteria to filter the resources
|
||||
|
||||
filters:
|
||||
label: true
|
||||
owner: true
|
||||
group: true
|
||||
locked: true
|
||||
vrouter: true
|
||||
|
||||
|
||||
# Info Tabs - Which info tabs are used to show extended information
|
||||
@ -73,6 +79,7 @@ dialogs:
|
||||
information: true
|
||||
ownership: true
|
||||
capacity: true
|
||||
showback: true
|
||||
vm_group: true
|
||||
vcenter:
|
||||
enabled: true
|
||||
@ -86,7 +93,6 @@ dialogs:
|
||||
sched_action: true
|
||||
booting: true
|
||||
create_dialog:
|
||||
information: true
|
||||
capacity: true
|
||||
ownership: true
|
||||
vm_group: true
|
||||
|
@ -83,7 +83,7 @@ const DiskCard = memo(({ disk = {}, actions = [], snapshotActions = [] }) => {
|
||||
>
|
||||
<div className={classes.main}>
|
||||
<div className={classes.title}>
|
||||
<Typography component="span" data-cy="name">
|
||||
<Typography noWrap component="span" data-cy="name">
|
||||
{getDiskName(disk)}
|
||||
</Typography>
|
||||
<span className={classes.labels}>
|
||||
|
110
src/fireedge/src/client/components/Cards/MarketplaceAppCard.js
Normal file
110
src/fireedge/src/client/components/Cards/MarketplaceAppCard.js
Normal file
@ -0,0 +1,110 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Lock, User, Group, Cart } from 'iconoir-react'
|
||||
import { Typography } from '@mui/material'
|
||||
|
||||
import Timer from 'client/components/Timer'
|
||||
import { StatusCircle, StatusChip } from 'client/components/Status'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { rowStyles } from 'client/components/Tables/styles'
|
||||
|
||||
import { getState, getType } from 'client/models/MarketplaceApp'
|
||||
import { timeFromMilliseconds } from 'client/models/Helper'
|
||||
import { prettyBytes } from 'client/utils'
|
||||
import { T, MarketplaceApp } from 'client/constants'
|
||||
|
||||
const MarketplaceAppCard = memo(
|
||||
/**
|
||||
* @param {object} props - Props
|
||||
* @param {MarketplaceApp} props.app - Marketplace App resource
|
||||
* @param {object} props.rootProps - Props to root component
|
||||
* @returns {ReactElement} - Card
|
||||
*/
|
||||
({ app, rootProps }) => {
|
||||
const classes = rowStyles()
|
||||
const {
|
||||
ID,
|
||||
NAME,
|
||||
UNAME,
|
||||
GNAME,
|
||||
LOCK,
|
||||
REGTIME,
|
||||
MARKETPLACE,
|
||||
ZONE_ID,
|
||||
SIZE,
|
||||
} = app
|
||||
|
||||
const state = useMemo(() => getState(app), [app?.STATE])
|
||||
const { color: stateColor, name: stateName } = state
|
||||
|
||||
const time = useMemo(() => timeFromMilliseconds(+REGTIME), [REGTIME])
|
||||
const type = useMemo(() => getType(app), [app?.TYPE])
|
||||
|
||||
return (
|
||||
<div {...rootProps} data-cy={`app-${ID}`}>
|
||||
<div className={classes.main}>
|
||||
<div className={classes.title}>
|
||||
<StatusCircle color={stateColor} tooltip={stateName} />
|
||||
<Typography component="span">{NAME}</Typography>
|
||||
{LOCK && <Lock />}
|
||||
<span className={classes.labels}>
|
||||
<StatusChip text={type} />
|
||||
</span>
|
||||
</div>
|
||||
<div className={classes.caption}>
|
||||
<span data-cy="id">{`#${ID}`}</span>
|
||||
<span title={useMemo(() => time.toFormat('ff'), [REGTIME])}>
|
||||
<Timer translateWord={T.RegisteredAt} initial={time} />
|
||||
</span>
|
||||
<span title={`${Tr(T.Owner)}: ${UNAME}`}>
|
||||
<User />
|
||||
<span data-cy="owner">{UNAME}</span>
|
||||
</span>
|
||||
<span title={`${Tr(T.Group)}: ${GNAME}`}>
|
||||
<Group />
|
||||
<span data-cy="group">{GNAME}</span>
|
||||
</span>
|
||||
<span title={`${Tr(T.Marketplace)}: ${MARKETPLACE}`}>
|
||||
<Cart />
|
||||
<span data-cy="marketplace">{MARKETPLACE}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.secondary}>
|
||||
<span className={classes.labels}>
|
||||
<StatusChip text={`${Tr(T.Zone)} ${ZONE_ID}`} />
|
||||
<StatusChip text={prettyBytes(+SIZE, 'MB')} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
MarketplaceAppCard.propTypes = {
|
||||
app: PropTypes.object,
|
||||
rootProps: PropTypes.shape({
|
||||
className: PropTypes.string,
|
||||
}),
|
||||
actions: PropTypes.any,
|
||||
}
|
||||
|
||||
MarketplaceAppCard.displayName = 'MarketplaceAppCard'
|
||||
|
||||
export default MarketplaceAppCard
|
@ -99,6 +99,7 @@ const NicCard = memo(
|
||||
|
||||
const isAlias = !!PARENT?.length
|
||||
const isPciDevice = PCI_ID !== undefined
|
||||
const isAdditionalIp = NIC_ID !== undefined || NETWORK === 'Additional IP'
|
||||
|
||||
const dataCy = isAlias ? 'alias' : 'nic'
|
||||
|
||||
@ -149,7 +150,9 @@ const NicCard = memo(
|
||||
</span>
|
||||
</div>
|
||||
</Box>
|
||||
{!isPciDevice && <div className={classes.actions}>{actions}</div>}
|
||||
{!isPciDevice && !isAdditionalIp && (
|
||||
<div className={classes.actions}>{actions}</div>
|
||||
)}
|
||||
{!!ALIAS?.length && (
|
||||
<Box flexBasis="100%">
|
||||
{ALIAS?.map((alias) => (
|
||||
@ -182,7 +185,12 @@ const NicCard = memo(
|
||||
|
||||
return (
|
||||
<AccordionDetails key={key}>
|
||||
<SecurityGroupRules id={ID} rules={rules} actions={acts} />
|
||||
<SecurityGroupRules
|
||||
parentKey={key}
|
||||
id={ID}
|
||||
rules={rules}
|
||||
actions={acts}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
)
|
||||
})}
|
||||
@ -205,7 +213,7 @@ NicCard.propTypes = {
|
||||
|
||||
NicCard.displayName = 'NicCard'
|
||||
|
||||
const SecurityGroupRules = memo(({ id, actions, rules }) => {
|
||||
const SecurityGroupRules = memo(({ parentKey, id, actions, rules }) => {
|
||||
const classes = rowStyles()
|
||||
|
||||
const COLUMNS = useMemo(
|
||||
@ -222,7 +230,7 @@ const SecurityGroupRules = memo(({ id, actions, rules }) => {
|
||||
noWrap
|
||||
component="span"
|
||||
variant="subtitle1"
|
||||
data-cy={`${id}-rule-name`}
|
||||
data-cy={`${parentKey}-rule-name`}
|
||||
>
|
||||
{`#${id} ${name}`}
|
||||
</Typography>
|
||||
@ -230,14 +238,19 @@ const SecurityGroupRules = memo(({ id, actions, rules }) => {
|
||||
</Stack>
|
||||
<Box display="grid" gridTemplateColumns="repeat(5, 1fr)" gap="0.5em">
|
||||
{COLUMNS.map((col) => (
|
||||
<Typography key={col} noWrap component="span" variant="subtitle2">
|
||||
<Typography
|
||||
key={`${parentKey}-${col}`}
|
||||
noWrap
|
||||
component="span"
|
||||
variant="subtitle2"
|
||||
>
|
||||
<Translate word={col} />
|
||||
</Typography>
|
||||
))}
|
||||
{rules.map((rule, ruleIdx) => (
|
||||
{rules.map((rule) => (
|
||||
<SecurityGroupRule
|
||||
data-cy={`${id}-rule-${ruleIdx}`}
|
||||
key={`${id}-rule-${ruleIdx}`}
|
||||
key={`${parentKey}-rule-${rule.RULE_TYPE}`}
|
||||
data-cy={`${parentKey}-rule-${rule.RULE_TYPE}`}
|
||||
rule={rule}
|
||||
/>
|
||||
))}
|
||||
@ -247,6 +260,7 @@ const SecurityGroupRules = memo(({ id, actions, rules }) => {
|
||||
})
|
||||
|
||||
SecurityGroupRules.propTypes = {
|
||||
parentKey: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
rules: PropTypes.array,
|
||||
actions: PropTypes.node,
|
||||
@ -254,7 +268,7 @@ SecurityGroupRules.propTypes = {
|
||||
|
||||
SecurityGroupRules.displayName = 'SecurityGroupRule'
|
||||
|
||||
const SecurityGroupRule = memo(({ rule, 'data-cy': cy }) => {
|
||||
const SecurityGroupRule = memo(({ rule, 'data-cy': parentCy }) => {
|
||||
/** @type {PrettySecurityGroupRule} */
|
||||
const { PROTOCOL, RULE_TYPE, ICMP_TYPE, RANGE, NETWORK_ID } = rule
|
||||
|
||||
@ -266,11 +280,11 @@ const SecurityGroupRule = memo(({ rule, 'data-cy': cy }) => {
|
||||
{ text: RANGE, dataCy: 'range' },
|
||||
{ text: NETWORK_ID, dataCy: 'networkid' },
|
||||
{ text: ICMP_TYPE, dataCy: 'icmp-type' },
|
||||
].map(({ text, dataCy }, index) => (
|
||||
].map(({ text, dataCy }) => (
|
||||
<Typography
|
||||
noWrap
|
||||
key={`${index}-${cy}`}
|
||||
data-cy={`${cy}-${dataCy}`}
|
||||
key={`${parentCy}-${dataCy}`}
|
||||
data-cy={`${parentCy}-${dataCy}`.toLowerCase()}
|
||||
variant="subtitle2"
|
||||
>
|
||||
{text}
|
||||
|
@ -13,49 +13,90 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, memo } from 'react'
|
||||
import { ReactElement, memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {
|
||||
User,
|
||||
Group,
|
||||
Lock,
|
||||
HardDrive,
|
||||
Cpu,
|
||||
Network,
|
||||
WarningCircledOutline as WarningIcon,
|
||||
} from 'iconoir-react'
|
||||
import { Box, Stack, Typography, Tooltip } from '@mui/material'
|
||||
|
||||
import Timer from 'client/components/Timer'
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import MultipleTags from 'client/components/MultipleTags'
|
||||
import Timer from 'client/components/Timer'
|
||||
import { MemoryIcon } from 'client/components/Icons'
|
||||
import { StatusCircle, StatusChip } from 'client/components/Status'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { rowStyles } from 'client/components/Tables/styles'
|
||||
|
||||
import {
|
||||
getState,
|
||||
getLastHistory,
|
||||
getHypervisor,
|
||||
getIps,
|
||||
getErrorMessage,
|
||||
} from 'client/models/VirtualMachine'
|
||||
import { timeFromMilliseconds } from 'client/models/Helper'
|
||||
import { VM } from 'client/constants'
|
||||
import {
|
||||
timeFromMilliseconds,
|
||||
getUniqueLabels,
|
||||
getColorFromString,
|
||||
} from 'client/models/Helper'
|
||||
import { prettyBytes } from 'client/utils'
|
||||
import { T, VM, ACTIONS, RESOURCE_NAMES } from 'client/constants'
|
||||
|
||||
const VirtualMachineCard = memo(
|
||||
/**
|
||||
* @param {object} props - Props
|
||||
* @param {VM} props.vm - Virtual machine resource
|
||||
* @param {object} props.rootProps - Props to root component
|
||||
* @param {function(string):Promise} [props.onDeleteLabel] - Callback to delete label
|
||||
* @param {ReactElement} [props.actions] - Actions
|
||||
* @returns {ReactElement} - Card
|
||||
*/
|
||||
({ vm, rootProps, actions }) => {
|
||||
({ vm, rootProps, actions, onDeleteLabel }) => {
|
||||
const classes = rowStyles()
|
||||
const { ID, NAME, UNAME, GNAME, IPS, STIME, ETIME, LOCK } = vm
|
||||
const { [RESOURCE_NAMES.VM]: vmView } = useViews()
|
||||
|
||||
const HOSTNAME = getLastHistory(vm)?.HOSTNAME ?? '--'
|
||||
const hypervisor = getHypervisor(vm)
|
||||
const time = timeFromMilliseconds(+ETIME || +STIME)
|
||||
const error = getErrorMessage(vm)
|
||||
const enableEditLabels =
|
||||
vmView?.actions?.[ACTIONS.EDIT_LABELS] === true && !!onDeleteLabel
|
||||
|
||||
const {
|
||||
ID,
|
||||
NAME,
|
||||
STIME,
|
||||
ETIME,
|
||||
LOCK,
|
||||
USER_TEMPLATE: { LABELS } = {},
|
||||
TEMPLATE: { CPU, MEMORY } = {},
|
||||
} = vm
|
||||
|
||||
const { HOSTNAME = '--', VM_MAD: hypervisor } = useMemo(
|
||||
() => getLastHistory(vm) ?? '--',
|
||||
[vm.HISTORY_RECORDS]
|
||||
)
|
||||
|
||||
const time = useMemo(
|
||||
() => timeFromMilliseconds(+ETIME || +STIME),
|
||||
[ETIME, STIME]
|
||||
)
|
||||
|
||||
const { color: stateColor, name: stateName } = getState(vm)
|
||||
const error = useMemo(() => getErrorMessage(vm), [vm])
|
||||
const ips = useMemo(() => getIps(vm), [vm])
|
||||
const memValue = useMemo(() => prettyBytes(+MEMORY, 'MB'), [MEMORY])
|
||||
|
||||
const labels = useMemo(
|
||||
() =>
|
||||
getUniqueLabels(LABELS).map((label) => ({
|
||||
text: label,
|
||||
stateColor: getColorFromString(label),
|
||||
onDelete: enableEditLabels && onDeleteLabel,
|
||||
})),
|
||||
[LABELS, enableEditLabels, onDeleteLabel]
|
||||
)
|
||||
|
||||
return (
|
||||
<div {...rootProps} data-cy={`vm-${ID}`}>
|
||||
@ -79,34 +120,40 @@ const VirtualMachineCard = memo(
|
||||
<span className={classes.labels}>
|
||||
{hypervisor && <StatusChip text={hypervisor} />}
|
||||
{LOCK && <Lock data-cy="lock" />}
|
||||
<MultipleTags tags={labels} />
|
||||
</span>
|
||||
</div>
|
||||
<div className={classes.caption}>
|
||||
<span title={time.toFormat('ff')}>
|
||||
{`#${ID} ${+ETIME ? 'done' : 'started'} `}
|
||||
<span data-cy="id">{`#${ID}`}</span>
|
||||
<span title={useMemo(() => time.toFormat('ff'), [ETIME, STIME])}>
|
||||
{`${+ETIME ? T.Done : T.Started} `}
|
||||
<Timer initial={time} />
|
||||
</span>
|
||||
<span title={`Owner: ${UNAME}`}>
|
||||
<User />
|
||||
<span data-cy="uname">{` ${UNAME}`}</span>
|
||||
<span title={`${Tr(T.PhysicalCpu)}: ${CPU}`}>
|
||||
<Cpu />
|
||||
<span data-cy="cpu">{CPU}</span>
|
||||
</span>
|
||||
<span title={`Group: ${GNAME}`}>
|
||||
<Group />
|
||||
<span data-cy="gname">{` ${GNAME}`}</span>
|
||||
<span title={`${Tr(T.Memory)}: ${memValue}`}>
|
||||
<MemoryIcon width={20} height={20} />
|
||||
<span data-cy="memory">{memValue}</span>
|
||||
</span>
|
||||
<span title={`Hostname: ${HOSTNAME}`}>
|
||||
<span
|
||||
className={classes.captionItem}
|
||||
title={`${Tr(T.Hostname)}: ${HOSTNAME}`}
|
||||
>
|
||||
<HardDrive />
|
||||
<span data-cy="hostname">{` ${HOSTNAME}`}</span>
|
||||
<span data-cy="hostname">{HOSTNAME}</span>
|
||||
</span>
|
||||
{!!ips?.length && (
|
||||
<span className={classes.captionItem}>
|
||||
<Network />
|
||||
<Stack direction="row" justifyContent="end" alignItems="center">
|
||||
<MultipleTags tags={ips} clipboard />
|
||||
</Stack>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!!IPS?.length && (
|
||||
<div className={classes.secondary}>
|
||||
<Stack flexWrap="wrap" justifyContent="end" alignItems="center">
|
||||
<MultipleTags tags={IPS.split(',')} />
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
{actions && <div className={classes.actions}>{actions}</div>}
|
||||
</div>
|
||||
)
|
||||
@ -118,6 +165,7 @@ VirtualMachineCard.propTypes = {
|
||||
rootProps: PropTypes.shape({
|
||||
className: PropTypes.string,
|
||||
}),
|
||||
onDeleteLabel: PropTypes.func,
|
||||
actions: PropTypes.any,
|
||||
}
|
||||
|
||||
|
@ -19,24 +19,44 @@ import PropTypes from 'prop-types'
|
||||
import { User, Group, Lock } from 'iconoir-react'
|
||||
import { Typography } from '@mui/material'
|
||||
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import MultipleTags from 'client/components/MultipleTags'
|
||||
import Timer from 'client/components/Timer'
|
||||
import { StatusChip } from 'client/components/Status'
|
||||
import { rowStyles } from 'client/components/Tables/styles'
|
||||
import Image from 'client/components/Image'
|
||||
import { StatusChip } from 'client/components/Status'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { rowStyles } from 'client/components/Tables/styles'
|
||||
|
||||
import { timeFromMilliseconds } from 'client/models/Helper'
|
||||
import {
|
||||
timeFromMilliseconds,
|
||||
getUniqueLabels,
|
||||
getColorFromString,
|
||||
} from 'client/models/Helper'
|
||||
import { isExternalURL } from 'client/utils'
|
||||
import { VM, STATIC_FILES_URL, DEFAULT_TEMPLATE_LOGO } from 'client/constants'
|
||||
import {
|
||||
T,
|
||||
VM,
|
||||
ACTIONS,
|
||||
RESOURCE_NAMES,
|
||||
STATIC_FILES_URL,
|
||||
DEFAULT_TEMPLATE_LOGO,
|
||||
} from 'client/constants'
|
||||
|
||||
const VmTemplateCard = memo(
|
||||
/**
|
||||
* @param {object} props - Props
|
||||
* @param {VM} props.template - Virtual machine resource
|
||||
* @param {object} props.rootProps - Props to root component
|
||||
* @param {function(string):Promise} [props.onDeleteLabel] - Callback to delete label
|
||||
* @returns {ReactElement} - Card
|
||||
*/
|
||||
({ template, rootProps }) => {
|
||||
({ template, rootProps, onDeleteLabel }) => {
|
||||
const classes = rowStyles()
|
||||
const { [RESOURCE_NAMES.VM_TEMPLATE]: templateView } = useViews()
|
||||
|
||||
const enableEditLabels =
|
||||
templateView?.actions?.[ACTIONS.EDIT_LABELS] === true && !!onDeleteLabel
|
||||
|
||||
const {
|
||||
ID,
|
||||
NAME,
|
||||
@ -45,10 +65,11 @@ const VmTemplateCard = memo(
|
||||
REGTIME,
|
||||
LOCK,
|
||||
VROUTER,
|
||||
TEMPLATE: { LOGO = '' } = {},
|
||||
TEMPLATE: { HYPERVISOR, LABELS, LOGO = '' } = {},
|
||||
} = template
|
||||
|
||||
const isExternalImage = useMemo(() => isExternalURL(LOGO), [LOGO])
|
||||
const time = useMemo(() => timeFromMilliseconds(+REGTIME), [REGTIME])
|
||||
|
||||
const logoSource = useMemo(() => {
|
||||
if (!LOGO) return `${STATIC_FILES_URL}/${DEFAULT_TEMPLATE_LOGO}`
|
||||
@ -56,7 +77,15 @@ const VmTemplateCard = memo(
|
||||
return isExternalImage ? LOGO : `${STATIC_FILES_URL}/${LOGO}`
|
||||
}, [isExternalImage, LOGO])
|
||||
|
||||
const time = useMemo(() => timeFromMilliseconds(+REGTIME), [REGTIME])
|
||||
const labels = useMemo(
|
||||
() =>
|
||||
getUniqueLabels(LABELS).map((label) => ({
|
||||
text: label,
|
||||
stateColor: getColorFromString(label),
|
||||
onDelete: enableEditLabels && onDeleteLabel,
|
||||
})),
|
||||
[LABELS, enableEditLabels, onDeleteLabel]
|
||||
)
|
||||
|
||||
return (
|
||||
<div {...rootProps} data-cy={`template-${ID}`}>
|
||||
@ -71,20 +100,22 @@ const VmTemplateCard = memo(
|
||||
<div className={classes.title}>
|
||||
<Typography component="span">{NAME}</Typography>
|
||||
<span className={classes.labels}>
|
||||
{HYPERVISOR && <StatusChip text={HYPERVISOR} />}
|
||||
{LOCK && <Lock />}
|
||||
{VROUTER && <StatusChip text={VROUTER} />}
|
||||
<MultipleTags tags={labels} />
|
||||
</span>
|
||||
</div>
|
||||
<div className={classes.caption}>
|
||||
<span title={time.toFormat('ff')} className="full-width">
|
||||
{`#${ID} registered `}
|
||||
<Timer initial={time} />
|
||||
<span data-cy="id">{`#${ID}`}</span>
|
||||
<span title={time.toFormat('ff')}>
|
||||
<Timer translateWord={T.RegisteredAt} initial={time} />
|
||||
</span>
|
||||
<span title={`Owner: ${UNAME}`}>
|
||||
<span title={`${Tr(T.Owner)}: ${UNAME}`}>
|
||||
<User />
|
||||
<span>{` ${UNAME}`}</span>
|
||||
</span>
|
||||
<span title={`Group: ${GNAME}`}>
|
||||
<span title={`${Tr(T.Group)}: ${GNAME}`}>
|
||||
<Group />
|
||||
<span>{` ${GNAME}`}</span>
|
||||
</span>
|
||||
@ -100,6 +131,7 @@ VmTemplateCard.propTypes = {
|
||||
rootProps: PropTypes.shape({
|
||||
className: PropTypes.string,
|
||||
}),
|
||||
onDeleteLabel: PropTypes.func,
|
||||
}
|
||||
|
||||
VmTemplateCard.displayName = 'VmTemplateCard'
|
||||
|
@ -22,6 +22,7 @@ import DiskCard from 'client/components/Cards/DiskCard'
|
||||
import DiskSnapshotCard from 'client/components/Cards/DiskSnapshotCard'
|
||||
import EmptyCard from 'client/components/Cards/EmptyCard'
|
||||
import HostCard from 'client/components/Cards/HostCard'
|
||||
import MarketplaceAppCard from 'client/components/Cards/MarketplaceAppCard'
|
||||
import NetworkCard from 'client/components/Cards/NetworkCard'
|
||||
import NicCard from 'client/components/Cards/NicCard'
|
||||
import PolicyCard from 'client/components/Cards/PolicyCard'
|
||||
@ -29,8 +30,8 @@ import ProvisionCard from 'client/components/Cards/ProvisionCard'
|
||||
import ProvisionTemplateCard from 'client/components/Cards/ProvisionTemplateCard'
|
||||
import ScheduleActionCard from 'client/components/Cards/ScheduleActionCard'
|
||||
import SecurityGroupCard from 'client/components/Cards/SecurityGroupCard'
|
||||
import SnapshotCard from 'client/components/Cards/SnapshotCard'
|
||||
import SelectCard from 'client/components/Cards/SelectCard'
|
||||
import SnapshotCard from 'client/components/Cards/SnapshotCard'
|
||||
import TierCard from 'client/components/Cards/TierCard'
|
||||
import VirtualMachineCard from 'client/components/Cards/VirtualMachineCard'
|
||||
import VmTemplateCard from 'client/components/Cards/VmTemplateCard'
|
||||
@ -46,6 +47,7 @@ export {
|
||||
DiskSnapshotCard,
|
||||
EmptyCard,
|
||||
HostCard,
|
||||
MarketplaceAppCard,
|
||||
NetworkCard,
|
||||
NicCard,
|
||||
PolicyCard,
|
||||
@ -53,8 +55,8 @@ export {
|
||||
ProvisionTemplateCard,
|
||||
ScheduleActionCard,
|
||||
SecurityGroupCard,
|
||||
SnapshotCard,
|
||||
SelectCard,
|
||||
SnapshotCard,
|
||||
TierCard,
|
||||
VirtualMachineCard,
|
||||
VmTemplateCard,
|
||||
|
@ -17,8 +17,6 @@ import { useCallback, ReactElement } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { AnySchema } from 'yup'
|
||||
|
||||
import clsx from 'clsx'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { useForm, FormProvider } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
|
||||
@ -26,19 +24,6 @@ import DialogConfirmation, {
|
||||
DialogPropTypes,
|
||||
} from 'client/components/Dialogs/DialogConfirmation'
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
content: {
|
||||
width: '80vw',
|
||||
height: '60vh',
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
[theme.breakpoints.only('xs')]: {
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
/**
|
||||
* Creates dialog with a form inside.
|
||||
*
|
||||
@ -57,13 +42,8 @@ const DialogForm = ({
|
||||
dialogProps,
|
||||
children,
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
const { className, ...contentProps } = dialogProps.contentProps ?? {}
|
||||
|
||||
dialogProps.contentProps = {
|
||||
className: clsx(classes.content, className),
|
||||
...contentProps,
|
||||
}
|
||||
dialogProps.fixedWidth ??= true
|
||||
dialogProps.fixedHeight ??= true
|
||||
|
||||
const methods = useForm({
|
||||
mode: 'onBlur',
|
||||
|
@ -19,7 +19,8 @@ import { styled, Link, Typography } from '@mui/material'
|
||||
|
||||
import { useGetOneVersionQuery } from 'client/features/OneApi/system'
|
||||
import { StatusChip } from 'client/components/Status'
|
||||
import { BY } from 'client/constants'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { BY, T } from 'client/constants'
|
||||
|
||||
const FooterBox = styled('footer')(({ theme }) => ({
|
||||
color: theme.palette.primary.contrastText,
|
||||
@ -48,12 +49,19 @@ const Footer = memo(() => {
|
||||
return (
|
||||
<FooterBox>
|
||||
<Typography variant="body2">
|
||||
{'Made with'}
|
||||
<Translate word={T.MadeWith} />
|
||||
<HeartIcon role="img" aria-label="heart-emoji" />
|
||||
<Link href={BY.url} color="primary.contrastText">
|
||||
{BY.text}
|
||||
</Link>
|
||||
{version && <StatusChip stateColor="secondary" text={version} mx={1} />}
|
||||
{version && (
|
||||
<StatusChip
|
||||
forceWhiteColor
|
||||
stateColor="secondary"
|
||||
text={version}
|
||||
mx={1}
|
||||
/>
|
||||
)}
|
||||
</Typography>
|
||||
</FooterBox>
|
||||
)
|
||||
|
@ -69,13 +69,13 @@ const TableController = memo(
|
||||
<ErrorHelper data-cy={`${cy}-error`} label={error?.message} mb={2} />
|
||||
)}
|
||||
<Table
|
||||
pageSize={4}
|
||||
pageSize={5}
|
||||
disableGlobalSort
|
||||
displaySelectedRows
|
||||
disableRowSelect={readOnly}
|
||||
singleSelect={singleSelect}
|
||||
onlyGlobalSearch
|
||||
onlyGlobalSelectedRows
|
||||
getRowId={getRowId}
|
||||
initialState={{ ...initialState, selectedRowIds: initialRows }}
|
||||
disableRowSelect={readOnly}
|
||||
onSelectedRowsChange={(rows) => {
|
||||
if (readOnly) return
|
||||
|
||||
|
@ -36,7 +36,7 @@ import { isDevelopment } from 'client/utils'
|
||||
|
||||
const ButtonToTriggerForm = ({ buttonProps = {}, options = [] }) => {
|
||||
const buttonId = buttonProps['data-cy'] ?? 'main-button'
|
||||
const isGroupButton = options.length > 1
|
||||
const moreThanOneOption = options.length > 1
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState(() => null)
|
||||
const open = Boolean(anchorEl)
|
||||
@ -76,16 +76,16 @@ const ButtonToTriggerForm = ({ buttonProps = {}, options = [] }) => {
|
||||
aria-describedby={buttonId}
|
||||
aria-controls={open ? `${buttonId}-button` : undefined}
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
aria-haspopup={isGroupButton ? 'true' : false}
|
||||
aria-haspopup={moreThanOneOption ? 'true' : false}
|
||||
disabled={!options.length}
|
||||
endicon={isGroupButton ? <NavArrowDown /> : undefined}
|
||||
endicon={moreThanOneOption ? <NavArrowDown /> : undefined}
|
||||
onClick={(evt) =>
|
||||
!isGroupButton ? openDialogForm(options[0]) : handleToggle(evt)
|
||||
moreThanOneOption ? handleToggle(evt) : openDialogForm(options[0])
|
||||
}
|
||||
{...buttonProps}
|
||||
/>
|
||||
|
||||
{isGroupButton && (
|
||||
{moreThanOneOption && !buttonProps.disabled && (
|
||||
<Menu
|
||||
id={`${buttonId}-menu`}
|
||||
anchorEl={anchorEl}
|
||||
|
@ -41,8 +41,9 @@ const Content = ({ data, setFormData }) => {
|
||||
return (
|
||||
<ClustersTable
|
||||
singleSelect
|
||||
onlyGlobalSearch
|
||||
onlyGlobalSelectedRows
|
||||
disableGlobalSort
|
||||
displaySelectedRows
|
||||
pageSize={5}
|
||||
initialState={{ selectedRowIds: { [ID]: true } }}
|
||||
onSelectedRowsChange={handleSelectedRows}
|
||||
/>
|
||||
|
@ -40,8 +40,9 @@ const Content = ({ data }) => {
|
||||
return (
|
||||
<MarketplacesTable
|
||||
singleSelect
|
||||
onlyGlobalSearch
|
||||
onlyGlobalSelectedRows
|
||||
disableGlobalSort
|
||||
displaySelectedRows
|
||||
pageSize={5}
|
||||
getRowId={(market) => String(market.NAME)}
|
||||
useQuery={() =>
|
||||
useGetMarketplacesQuery(undefined, {
|
||||
|
@ -43,8 +43,9 @@ const Content = ({ data, app }) => {
|
||||
return (
|
||||
<DatastoresTable
|
||||
singleSelect
|
||||
onlyGlobalSearch
|
||||
onlyGlobalSelectedRows
|
||||
disableGlobalSort
|
||||
displaySelectedRows
|
||||
pageSize={5}
|
||||
getRowId={(row) => String(row.NAME)}
|
||||
initialState={{
|
||||
selectedRowIds: { [NAME]: true },
|
||||
|
@ -37,8 +37,9 @@ const Content = ({ data, app }) => {
|
||||
<DockerHubTagsTable
|
||||
app={app}
|
||||
singleSelect
|
||||
onlyGlobalSearch
|
||||
onlyGlobalSelectedRows
|
||||
disableGlobalSort
|
||||
displaySelectedRows
|
||||
pageSize={5}
|
||||
initialState={{ selectedRowIds: { [name]: true } }}
|
||||
onSelectedRowsChange={handleSelectedRows}
|
||||
/>
|
||||
|
@ -40,8 +40,8 @@ const Content = ({ data, setFormData }) => {
|
||||
return (
|
||||
<ImagesTable
|
||||
singleSelect
|
||||
onlyGlobalSearch
|
||||
onlyGlobalSelectedRows
|
||||
disableGlobalSort
|
||||
pageSize={5}
|
||||
initialState={{ selectedRowIds: { [ID]: true } }}
|
||||
onSelectedRowsChange={handleSelectedRows}
|
||||
/>
|
||||
|
@ -41,8 +41,9 @@ const Content = ({ data, setFormData }) => {
|
||||
return (
|
||||
<VNetworksTable
|
||||
singleSelect
|
||||
onlyGlobalSearch
|
||||
onlyGlobalSelectedRows
|
||||
disableGlobalSort
|
||||
displaySelectedRows
|
||||
pageSize={5}
|
||||
getRowId={(row) => String(row.NAME)}
|
||||
initialState={{ selectedRowIds: { [NAME]: true } }}
|
||||
onSelectedRowsChange={handleSelectedRows}
|
||||
|
@ -41,8 +41,9 @@ const Content = ({ data, setFormData }) => {
|
||||
return (
|
||||
<HostsTable
|
||||
singleSelect
|
||||
onlyGlobalSearch
|
||||
onlyGlobalSelectedRows
|
||||
disableGlobalSort
|
||||
displaySelectedRows
|
||||
pageSize={5}
|
||||
initialState={{ selectedRowIds: { [ID]: true } }}
|
||||
onSelectedRowsChange={handleSelectedRows}
|
||||
/>
|
||||
|
@ -23,7 +23,7 @@ import {
|
||||
} from 'client/utils'
|
||||
import { T, HYPERVISORS, USER_INPUT_TYPES, VmTemplate } from 'client/constants'
|
||||
|
||||
const { number, numberFloat } = USER_INPUT_TYPES
|
||||
const { number, numberFloat, range, rangeFloat } = USER_INPUT_TYPES
|
||||
|
||||
const TRANSLATES = {
|
||||
MEMORY: { name: 'MEMORY', label: T.Memory, tooltip: T.MemoryConcept },
|
||||
@ -60,6 +60,7 @@ export const FIELDS = (vmTemplate) => {
|
||||
const isMemory = name === 'MEMORY'
|
||||
const isVCenter = HYPERVISOR === HYPERVISORS.vcenter
|
||||
const divisibleBy4 = isVCenter && isMemory
|
||||
const isRange = [range, rangeFloat].includes(userInput.type)
|
||||
|
||||
// set default type to number
|
||||
userInput.type ??= name === 'CPU' ? numberFloat : number
|
||||
@ -71,18 +72,17 @@ export const FIELDS = (vmTemplate) => {
|
||||
const schemaUi = schemaUserInput({ options: ensuredOptions, ...userInput })
|
||||
const isNumber = schemaUi.validation instanceof NumberSchema
|
||||
|
||||
if (isNumber) {
|
||||
// add positive number validator
|
||||
isNumber && (schemaUi.validation &&= schemaUi.validation.positive())
|
||||
// add positive number validator
|
||||
isNumber && (schemaUi.validation &&= schemaUi.validation.positive())
|
||||
|
||||
if (isMemory && isRange) {
|
||||
// add label format on pretty bytes
|
||||
isMemory &&
|
||||
(schemaUi.fieldProps = { ...schemaUi.fieldProps, valueLabelFormat })
|
||||
schemaUi.fieldProps = { ...schemaUi.fieldProps, valueLabelFormat }
|
||||
}
|
||||
|
||||
if (divisibleBy4) {
|
||||
schemaUi.validation &&= schemaUi.validation.isDivisibleBy(4)
|
||||
schemaUi.fieldProps = { ...schemaUi.fieldProps, step: 4 }
|
||||
}
|
||||
if (isNumber && divisibleBy4) {
|
||||
schemaUi.validation &&= schemaUi.validation.isDivisibleBy(4)
|
||||
schemaUi.fieldProps = { ...schemaUi.fieldProps, step: 4 }
|
||||
}
|
||||
|
||||
return { ...TRANSLATES[name], ...schemaUi, grid: { md: 12 } }
|
||||
|
@ -43,6 +43,9 @@ export default makeStyles((theme) => ({
|
||||
scrollable: {
|
||||
paddingTop: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(2),
|
||||
[theme.breakpoints.up('xs')]: {
|
||||
padding: 0,
|
||||
},
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
'&::-webkit-scrollbar': {
|
||||
|
44
src/fireedge/src/client/components/Icons/MemoryIcon.js
Normal file
44
src/fireedge/src/client/components/Icons/MemoryIcon.js
Normal file
@ -0,0 +1,44 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo } from 'react'
|
||||
import { number, string, oneOfType } from 'prop-types'
|
||||
|
||||
const MemoryIcon = memo(({ viewBox, width, height, color, ...props }) => (
|
||||
<svg viewBox={viewBox} width={width} height={height} {...props}>
|
||||
<path
|
||||
fill={color}
|
||||
d="M13.78 13.25V3.84a3.841 3.841 0 0 1 7.68 0v9.41h9.73V3.84a3.841 3.841 0 0 1 7.68 0v9.41h9.73V3.84a3.841 3.841 0 0 1 7.68 0v9.41h9.73V3.84a3.841 3.841 0 0 1 7.68 0v9.41h9.73V3.84a3.841 3.841 0 0 1 7.68 0v9.41h9.73V3.84a3.841 3.841 0 0 1 7.68 0v9.41h8.6c1.59 0 3.03.65 4.07 1.69a5.744 5.744 0 0 1 1.69 4.07v60.66c0 1.57-.65 3.01-1.69 4.06l.01.01a5.744 5.744 0 0 1-4.07 1.69h-8.6v8.82a3.841 3.841 0 0 1-7.68 0v-8.82h-9.73v8.82a3.841 3.841 0 0 1-7.68 0v-8.82H73.7v8.82a3.841 3.841 0 0 1-7.68 0v-8.82h-9.73v8.82a3.841 3.841 0 0 1-7.68 0v-8.82h-9.73v8.82a3.841 3.841 0 0 1-7.68 0v-8.82h-9.73v8.82a3.841 3.841 0 0 1-7.68 0v-8.82H5.75c-1.59 0-3.03-.65-4.07-1.69A5.814 5.814 0 0 1 0 79.67V19.01c0-1.59.65-3.03 1.69-4.07.12-.12.25-.23.38-.33a5.748 5.748 0 0 1 3.69-1.35h8.02v-.01zm16.98 49.52-5.2-9.85v9.85h-8.61V35.31h12.8c2.22 0 4.12.39 5.7 1.18 1.58.79 2.76 1.86 3.55 3.22s1.18 2.89 1.18 4.6c0 1.84-.51 3.47-1.53 4.89-1.02 1.42-2.49 2.44-4.4 3.06l5.97 10.51h-9.46zm-5.2-15.59h3.41c.83 0 1.45-.19 1.86-.56.41-.38.62-.96.62-1.77 0-.72-.21-1.29-.64-1.71-.43-.41-1.04-.62-1.84-.62h-3.41v4.66zm34.99 11.44H51.4l-1.36 4.15H41l10.05-27.46h9.93l10.01 27.46h-9.08l-1.36-4.15zm-2.1-6.47-2.48-7.64-2.48 7.64h4.96zm47.48-16.84v27.46h-8.57V49.08l-4.23 13.69h-7.37l-4.23-13.69v13.69h-8.61V35.31h10.55l6.05 16.49 5.9-16.49h10.51zm9.27-14.38H7.68v56.81H115.2V20.93z"
|
||||
/>
|
||||
</svg>
|
||||
))
|
||||
|
||||
MemoryIcon.propTypes = {
|
||||
width: oneOfType([number, string]).isRequired,
|
||||
height: oneOfType([number, string]).isRequired,
|
||||
viewBox: string,
|
||||
color: string,
|
||||
}
|
||||
|
||||
MemoryIcon.defaultProps = {
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 122.88 98.08',
|
||||
color: 'currentColor',
|
||||
}
|
||||
|
||||
MemoryIcon.displayName = 'MemoryIcon'
|
||||
|
||||
export default MemoryIcon
|
@ -15,7 +15,8 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import DockerLogo from 'client/components/Icons/DockerIcon'
|
||||
import GuacamoleLogo from 'client/components/Icons/GuacamoleIcon'
|
||||
import MemoryIcon from 'client/components/Icons/MemoryIcon'
|
||||
import OpenNebulaLogo from 'client/components/Icons/OpenNebulaIcon'
|
||||
import WebMKSLogo from 'client/components/Icons/WebMKSIcon'
|
||||
|
||||
export { DockerLogo, GuacamoleLogo, OpenNebulaLogo, WebMKSLogo }
|
||||
export { DockerLogo, GuacamoleLogo, MemoryIcon, OpenNebulaLogo, WebMKSLogo }
|
||||
|
@ -13,55 +13,63 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { ReactElement, useMemo, isValidElement } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Tooltip, Typography } from '@mui/material'
|
||||
import { Stack, Tooltip, Typography } from '@mui/material'
|
||||
|
||||
import { StatusChip } from 'client/components/Status'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const MultipleTags = ({ tags, limitTags = 1, clipboard }) => {
|
||||
/**
|
||||
* @typedef TagType
|
||||
* @property {string} text - The text to display in the chip
|
||||
* @property {string} [dataCy] - Data-cy to be used by Cypress
|
||||
*/
|
||||
|
||||
/**
|
||||
* Render a number of tags with a tooltip to show the full list.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {string[]|TagType} props.tags - Tags to display
|
||||
* @param {number} [props.limitTags] - Limit the number of tags to display
|
||||
* @param {boolean} [props.clipboard] - If true, the chip will be clickable
|
||||
* @returns {ReactElement} - Tag list
|
||||
*/
|
||||
const MultipleTags = ({ tags, limitTags = 1, clipboard = false }) => {
|
||||
if (tags?.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const more = tags.length - limitTags
|
||||
const [tagsToDisplay, tagsToHide, more] = useMemo(() => {
|
||||
const ensureTags = (dirtyTags = [], isHidden) =>
|
||||
dirtyTags.map((tag) => {
|
||||
if (isValidElement(tag)) return tag
|
||||
|
||||
const Tags = tags.splice(0, limitTags).map((tag, idx) => {
|
||||
const text = tag.text ?? tag
|
||||
const text = tag.text ?? tag
|
||||
|
||||
return (
|
||||
<StatusChip
|
||||
key={`${idx}-${text}`}
|
||||
text={text}
|
||||
clipboard={clipboard}
|
||||
dataCy={tag.dataCy ?? ''}
|
||||
/>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<StatusChip
|
||||
key={text}
|
||||
clipboard={clipboard}
|
||||
forceWhiteColor={isHidden}
|
||||
{...(typeof tag === 'string' ? { text } : tag)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
return [
|
||||
ensureTags(tags.slice(0, limitTags)),
|
||||
ensureTags(tags.slice(limitTags), true),
|
||||
tags.length - limitTags,
|
||||
]
|
||||
}, [tags, limitTags])
|
||||
|
||||
return (
|
||||
<>
|
||||
{Tags}
|
||||
{tagsToDisplay}
|
||||
{more > 0 && (
|
||||
<Tooltip
|
||||
arrow
|
||||
title={tags.map((tag, idx) => {
|
||||
const text = tag.text ?? tag
|
||||
|
||||
return (
|
||||
<Typography
|
||||
key={`${idx}-${text}`}
|
||||
variant="subtitle2"
|
||||
sx={{ height: 'max-content' }}
|
||||
{...(tag.dataCy && { dataCy: tag.dataCy })}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
)
|
||||
})}
|
||||
>
|
||||
<Tooltip arrow title={<Stack>{tagsToHide}</Stack>}>
|
||||
<Typography component="span" variant="subtitle2" sx={{ ml: 1 }}>
|
||||
{`+${more} `}
|
||||
<Translate word={T.More} />
|
||||
|
@ -13,54 +13,45 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo } from 'react'
|
||||
import { memo, useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Typography, Tooltip, lighten, darken } from '@mui/material'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { Copy as CopyIcon } from 'iconoir-react'
|
||||
import { styled, Typography, alpha } from '@mui/material'
|
||||
import { Copy as CopyIcon, Check as CopiedIcon, Cancel } from 'iconoir-react'
|
||||
|
||||
import { useClipboard } from 'client/hooks'
|
||||
import { Tr, Translate } from 'client/components/HOC'
|
||||
import { addOpacityToColor } from 'client/utils'
|
||||
import { T, SCHEMES } from 'client/constants'
|
||||
|
||||
const useStyles = makeStyles(({ spacing, palette, typography }) => {
|
||||
const getBackgroundColor = palette.mode === SCHEMES.DARK ? lighten : darken
|
||||
const defaultStateColor =
|
||||
palette.grey[palette.mode === SCHEMES.DARK ? 300 : 700]
|
||||
const Chip = styled(Typography)(
|
||||
({ theme: { palette, typography }, state = 'debug', white, icon }) => {
|
||||
const { dark = state } = palette[state] ?? {}
|
||||
|
||||
return {
|
||||
text: ({ stateColor = defaultStateColor, clipboard }) => {
|
||||
const paletteColor = palette[stateColor]
|
||||
const bgColor = alpha(dark, 0.2)
|
||||
const color = white ? palette.common.white : palette.text.primary
|
||||
|
||||
const color =
|
||||
paletteColor?.contrastText ?? getBackgroundColor(stateColor, 0.75)
|
||||
const bgColor = paletteColor?.dark ?? stateColor
|
||||
|
||||
return {
|
||||
color,
|
||||
backgroundColor: addOpacityToColor(bgColor, 0.2),
|
||||
padding: spacing('0.25rem', '0.5rem'),
|
||||
borderRadius: 2,
|
||||
textTransform: 'uppercase',
|
||||
fontSize: typography.overline.fontSize,
|
||||
fontWeight: 500,
|
||||
lineHeight: 'normal',
|
||||
cursor: 'default',
|
||||
...(clipboard && {
|
||||
return {
|
||||
color,
|
||||
backgroundColor: bgColor,
|
||||
padding: icon ? '0.1rem 0.5rem' : '0.25rem 0.5rem',
|
||||
borderRadius: 6,
|
||||
textTransform: 'uppercase',
|
||||
fontSize: typography.overline.fontSize,
|
||||
fontWeight: 500,
|
||||
lineHeight: 'normal',
|
||||
cursor: 'default',
|
||||
...(icon && {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5em',
|
||||
'& > .icon': {
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5em',
|
||||
'&:hover > .copy-icon': {
|
||||
color: bgColor,
|
||||
color,
|
||||
'&:hover': {
|
||||
color: white ? palette.getContrastText(color) : dark,
|
||||
},
|
||||
}),
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const callAll =
|
||||
(...fns) =>
|
||||
@ -72,57 +63,66 @@ const StatusChip = memo(
|
||||
stateColor,
|
||||
text = '',
|
||||
dataCy = '',
|
||||
clipboard = false,
|
||||
clipboard,
|
||||
onClick,
|
||||
onDelete,
|
||||
forceWhiteColor,
|
||||
...props
|
||||
}) => {
|
||||
const { copy, isCopied } = useClipboard()
|
||||
const textToCopy = typeof clipboard === 'string' ? clipboard : text
|
||||
const classes = useStyles({ stateColor, clipboard })
|
||||
|
||||
const handleCopy = (evt) => {
|
||||
!isCopied && copy(textToCopy)
|
||||
evt.stopPropagation()
|
||||
}
|
||||
const handleCopy = useCallback(
|
||||
(evt) => {
|
||||
const textToCopy = typeof clipboard === 'string' ? clipboard : text
|
||||
|
||||
!isCopied && copy(textToCopy)
|
||||
evt.stopPropagation()
|
||||
},
|
||||
[clipboard, copy, text, isCopied]
|
||||
)
|
||||
|
||||
const handleDelete = useCallback(
|
||||
(evt) => {
|
||||
onDelete(text)
|
||||
evt.stopPropagation()
|
||||
},
|
||||
[text, onDelete]
|
||||
)
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
arrow
|
||||
open={isCopied}
|
||||
title={
|
||||
<>
|
||||
{'✔️'}
|
||||
<Translate word={T.CopiedToClipboard} />
|
||||
</>
|
||||
}
|
||||
<Chip
|
||||
component="span"
|
||||
state={stateColor}
|
||||
white={forceWhiteColor ? 'true' : undefined}
|
||||
icon={clipboard || onDelete ? 'true' : undefined}
|
||||
onClick={callAll(onClick, clipboard && handleCopy)}
|
||||
data-cy={dataCy}
|
||||
{...props}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
className={classes.text}
|
||||
onClick={callAll(onClick, clipboard && handleCopy)}
|
||||
{...(dataCy && { 'data-cy': dataCy })}
|
||||
{...props}
|
||||
>
|
||||
{text}
|
||||
{clipboard && (
|
||||
<CopyIcon className="copy-icon" title={Tr(T.ClickToCopy)} />
|
||||
)}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
{text}
|
||||
{clipboard &&
|
||||
(isCopied ? <CopiedIcon /> : <CopyIcon className="icon" />)}
|
||||
{typeof onDelete === 'function' && (
|
||||
<Cancel onClick={handleDelete} className="icon" />
|
||||
)}
|
||||
</Chip>
|
||||
)
|
||||
},
|
||||
(prev, next) =>
|
||||
prev.stateColor === next.stateColor &&
|
||||
prev.text === next.text &&
|
||||
prev.clipboard === next.clipboard
|
||||
prev.clipboard === next.clipboard &&
|
||||
prev.onDelete === next.onDelete
|
||||
)
|
||||
|
||||
StatusChip.propTypes = {
|
||||
stateColor: PropTypes.string,
|
||||
text: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
clipboard: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
|
||||
forceWhiteColor: PropTypes.bool,
|
||||
dataCy: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
onDelete: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
|
||||
}
|
||||
|
||||
StatusChip.displayName = 'StatusChip'
|
||||
|
@ -15,6 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import api from 'client/features/OneApi/datastore'
|
||||
import { DatastoreCard } from 'client/components/Cards'
|
||||
|
||||
|
@ -13,133 +13,116 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { useEffect, useMemo, JSXElementConstructor } from 'react'
|
||||
import { useMemo, ReactElement } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {
|
||||
List,
|
||||
ListSubheader,
|
||||
ListItemButton,
|
||||
Typography,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from '@mui/material'
|
||||
import { Cancel } from 'iconoir-react'
|
||||
import { styled, TextField, Popper, Chip } from '@mui/material'
|
||||
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete'
|
||||
import { UseFiltersInstanceProps } from 'react-table'
|
||||
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
|
||||
const StyledAutocompletePopper = styled(Popper)(
|
||||
({ theme: { palette, zIndex } }) => ({
|
||||
[`& .${autocompleteClasses.paper}`]: {
|
||||
boxShadow: 'none',
|
||||
margin: 0,
|
||||
color: 'inherit',
|
||||
fontSize: '0.75rem',
|
||||
border: `1px solid ${palette.secondary[palette.mode]}`,
|
||||
},
|
||||
[`& .${autocompleteClasses.listbox}`]: {
|
||||
padding: 0,
|
||||
backgroundColor: palette.background.default,
|
||||
[`& .${autocompleteClasses.option}`]: {
|
||||
minHeight: 'auto',
|
||||
alignItems: 'flex-start',
|
||||
padding: '0.7em',
|
||||
'&[aria-selected="true"]': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
'&[data-focus="true"], &[data-focus="true"][aria-selected="true"]': {
|
||||
backgroundColor: palette.action.hover,
|
||||
},
|
||||
[`&:not(:last-child)`]: {
|
||||
borderBottom: `1px solid ${palette.divider}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
[`&.${autocompleteClasses.popper}`]: {
|
||||
zIndex: zIndex.modal + 2,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* Render category filter to table.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {string} props.title - Title category
|
||||
* @param {UseFiltersInstanceProps} props.column - Column to filter by
|
||||
* @param {string} [props.accessorOption] - Name of property option
|
||||
* @param {boolean} [props.multiple] - If `true`, can be more than one filter
|
||||
* @returns {JSXElementConstructor} Component JSX
|
||||
* @param {UseFiltersInstanceProps} props.column - Props
|
||||
* @returns {ReactElement} Component JSX
|
||||
*/
|
||||
const CategoryFilter = ({
|
||||
title,
|
||||
column,
|
||||
accessorOption,
|
||||
multiple = false,
|
||||
column: { Header, filterValue = [], setFilter, preFilteredRows, id },
|
||||
}) => {
|
||||
const {
|
||||
setFilter,
|
||||
id,
|
||||
preFilteredRows,
|
||||
filterValue = multiple ? [] : undefined,
|
||||
} = column
|
||||
|
||||
useEffect(() => () => setFilter(undefined), [])
|
||||
|
||||
// Calculate the options for filtering using the preFilteredRows
|
||||
const options = useMemo(() => {
|
||||
const filteredOptions = {}
|
||||
const uniqueOptions = new Set()
|
||||
|
||||
preFilteredRows?.forEach((row) => {
|
||||
const value = row.values[id]
|
||||
|
||||
if (!value) return
|
||||
|
||||
const count = filteredOptions[value[accessorOption] ?? value] || 0
|
||||
filteredOptions[value[accessorOption] ?? value] = count + 1
|
||||
const rowValue = row.values[id]
|
||||
rowValue !== undefined && uniqueOptions.add(rowValue)
|
||||
})
|
||||
|
||||
return filteredOptions
|
||||
return [...uniqueOptions.values()]
|
||||
}, [id, preFilteredRows])
|
||||
|
||||
const handleSelect = (value) => {
|
||||
setFilter(multiple ? [...filterValue, value] : value)
|
||||
}
|
||||
|
||||
const handleUnselect = (value) => {
|
||||
setFilter(multiple ? filterValue.filter((v) => v !== value) : undefined)
|
||||
}
|
||||
|
||||
const handleClear = () => setFilter(multiple ? [] : undefined)
|
||||
|
||||
const isFiltered = useMemo(
|
||||
() => (multiple ? filterValue?.length > 0 : filterValue !== undefined),
|
||||
[filterValue]
|
||||
)
|
||||
|
||||
if (Object.keys(options).length === 0) {
|
||||
if (options.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<List>
|
||||
{title && (
|
||||
<ListSubheader
|
||||
disableSticky
|
||||
disableGutters
|
||||
sx={{ display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<Translate word={title} />
|
||||
{isFiltered && (
|
||||
<Tooltip title={<Translate word={T.Clear} />}>
|
||||
<IconButton disableRipple size="small" onClick={handleClear}>
|
||||
<Cancel />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</ListSubheader>
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
multiple
|
||||
disableCloseOnSelect
|
||||
limitTags={2}
|
||||
color="secondary"
|
||||
value={filterValue}
|
||||
sx={{ minWidth: 300, position: 'relative' }}
|
||||
options={options}
|
||||
onChange={(_, newValue) => setFilter(newValue)}
|
||||
PopperComponent={StyledAutocompletePopper}
|
||||
renderInput={({ inputProps, ...inputParams }) => (
|
||||
<TextField
|
||||
label={Tr(Header)}
|
||||
ref={inputParams.InputProps.ref}
|
||||
inputProps={{ ...inputProps, 'data-cy': id }}
|
||||
{...inputParams}
|
||||
/>
|
||||
)}
|
||||
renderTags={(value, getTagProps) =>
|
||||
value.map((option, index) => {
|
||||
const { key, ...tagProps } = getTagProps({ index })
|
||||
|
||||
{Object.entries(options).map(([option, count], i) => {
|
||||
const value = option[accessorOption] ?? option
|
||||
|
||||
const isSelected = multiple
|
||||
? filterValue?.includes?.(value)
|
||||
: value === filterValue
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
key={i}
|
||||
selected={isSelected}
|
||||
onClick={() =>
|
||||
isSelected ? handleUnselect(value) : handleSelect(value)
|
||||
}
|
||||
>
|
||||
<Typography noWrap variant="subtitle2" title={value}>
|
||||
{`${value} (${count})`}
|
||||
</Typography>
|
||||
</ListItemButton>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
return (
|
||||
<Chip
|
||||
key={key}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
label={option}
|
||||
onClick={tagProps.onDelete}
|
||||
{...tagProps}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
CategoryFilter.propTypes = {
|
||||
column: PropTypes.object,
|
||||
accessorOption: PropTypes.string,
|
||||
icon: PropTypes.node,
|
||||
title: PropTypes.string,
|
||||
multiple: PropTypes.bool,
|
||||
}
|
||||
|
||||
export default CategoryFilter
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { memo, ReactElement, useCallback } from 'react'
|
||||
import { memo, ReactElement, useCallback, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { Row } from 'react-table'
|
||||
@ -69,15 +69,28 @@ const ActionItem = memo(
|
||||
action,
|
||||
disabled,
|
||||
useQuery,
|
||||
selected = false,
|
||||
} = item
|
||||
|
||||
const isDisabledByNumberOfSelectedRows = useMemo(() => {
|
||||
const numberOfRowSelected = selectedRows.length
|
||||
const min = selected?.min ?? 1
|
||||
const max = selected?.max ?? Number.MAX_SAFE_INTEGER
|
||||
|
||||
return (
|
||||
(selected === true && !numberOfRowSelected) ||
|
||||
(selected && min > numberOfRowSelected && numberOfRowSelected < max)
|
||||
)
|
||||
}, [selectedRows.length, selected])
|
||||
|
||||
const buttonProps = {
|
||||
color,
|
||||
variant,
|
||||
'data-cy':
|
||||
(dataCy && `action-${dataCy}`) ?? (accessor && `action-${accessor}`),
|
||||
disabled:
|
||||
typeof disabled === 'function' ? disabled(selectedRows) : disabled,
|
||||
isDisabledByNumberOfSelectedRows ||
|
||||
(typeof disabled === 'function' ? disabled(selectedRows) : disabled),
|
||||
icon: Icon && <Icon />,
|
||||
label: label && Tr(label),
|
||||
title: tooltip && Tr(tooltip),
|
||||
|
@ -13,10 +13,11 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { JSXElementConstructor, useMemo } from 'react'
|
||||
import { ReactElement } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Checkbox } from '@mui/material'
|
||||
import { Stack, Checkbox } from '@mui/material'
|
||||
import { RefreshDouble } from 'iconoir-react'
|
||||
import {
|
||||
UseTableInstanceProps,
|
||||
UseRowSelectState,
|
||||
@ -28,6 +29,7 @@ import {
|
||||
Action,
|
||||
GlobalAction,
|
||||
} from 'client/components/Tables/Enhanced/Utils/GlobalActions/Action'
|
||||
import { SubmitButton } from 'client/components/FormControl'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
@ -35,78 +37,80 @@ import { T } from 'client/constants'
|
||||
* Render bulk actions.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {function():Promise} props.refetch - Function to refetch data
|
||||
* @param {object} [props.className] - Class name for the container
|
||||
* @param {boolean} props.isLoading - The data is fetching
|
||||
* @param {boolean} props.singleSelect - If true, only one row can be selected
|
||||
* @param {boolean} props.disableRowSelect - Rows can't select
|
||||
* @param {GlobalAction[]} props.globalActions - Possible bulk actions
|
||||
* @param {UseTableInstanceProps} props.useTableProps - Table props
|
||||
* @param {boolean} props.disableRowSelect - Rows can't select
|
||||
* @returns {JSXElementConstructor} Component JSX with all actions
|
||||
* @returns {ReactElement} Component JSX with all actions
|
||||
*/
|
||||
const GlobalActions = ({
|
||||
refetch,
|
||||
className,
|
||||
isLoading,
|
||||
singleSelect = false,
|
||||
disableRowSelect = false,
|
||||
globalActions = [],
|
||||
useTableProps,
|
||||
useTableProps = {},
|
||||
}) => {
|
||||
/** @type {UseRowSelectInstanceProps} */
|
||||
const { getToggleAllPageRowsSelectedProps, getToggleAllRowsSelectedProps } =
|
||||
useTableProps
|
||||
|
||||
/** @type {UseRowSelectState} */
|
||||
const { selectedRowIds } = useTableProps?.state ?? {}
|
||||
|
||||
/** @type {UseFiltersInstanceProps} */
|
||||
const { preFilteredRows } = useTableProps ?? {}
|
||||
const { preFilteredRows } = useTableProps
|
||||
|
||||
/** @type {UseRowSelectState} */
|
||||
const { selectedRowIds } = useTableProps?.state
|
||||
|
||||
const selectedRows = preFilteredRows.filter((row) => !!selectedRowIds[row.id])
|
||||
const numberOfRowSelected = selectedRows.length
|
||||
|
||||
const [actionsSelected, actionsNoSelected] = useMemo(
|
||||
() =>
|
||||
globalActions.reduce(
|
||||
(memoResult, item) => {
|
||||
const { selected = false } = item
|
||||
|
||||
selected ? memoResult[0].push(item) : memoResult[1].push(item)
|
||||
|
||||
return memoResult
|
||||
},
|
||||
[[], []]
|
||||
),
|
||||
[globalActions]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{!disableRowSelect && (
|
||||
<Checkbox
|
||||
{...getToggleAllPageRowsSelectedProps()}
|
||||
title={Tr(T.ToggleAllCurrentPageRowsSelected)}
|
||||
indeterminate={getToggleAllRowsSelectedProps().indeterminate}
|
||||
color="secondary"
|
||||
<Stack
|
||||
className={className}
|
||||
direction="row"
|
||||
flexWrap="wrap"
|
||||
alignItems="center"
|
||||
gap="0.5em"
|
||||
>
|
||||
{refetch && (
|
||||
<SubmitButton
|
||||
data-cy="refresh"
|
||||
icon={<RefreshDouble />}
|
||||
tooltip={Tr(T.Refresh)}
|
||||
isSubmitting={isLoading}
|
||||
onClick={refetch}
|
||||
/>
|
||||
)}
|
||||
{actionsNoSelected?.map((item) => (
|
||||
<Action key={item.accessor} item={item} />
|
||||
))}
|
||||
{!disableRowSelect &&
|
||||
numberOfRowSelected > 0 &&
|
||||
actionsSelected?.map((item, idx) => {
|
||||
const { min = 1, max = Number.MAX_SAFE_INTEGER } =
|
||||
item?.selected ?? {}
|
||||
const key = item.accessor ?? item.label ?? item.tooltip ?? idx
|
||||
{!singleSelect && !disableRowSelect && (
|
||||
<>
|
||||
<Checkbox
|
||||
{...getToggleAllPageRowsSelectedProps()}
|
||||
title={Tr(T.ToggleAllCurrentPageRowsSelected)}
|
||||
indeterminate={getToggleAllRowsSelectedProps().indeterminate}
|
||||
color="secondary"
|
||||
/>
|
||||
{globalActions?.map((item, idx) => {
|
||||
const key = item.accessor ?? item.label ?? item.tooltip ?? idx
|
||||
|
||||
if (min < numberOfRowSelected && numberOfRowSelected > max) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <Action key={key} item={item} selectedRows={selectedRows} />
|
||||
})}
|
||||
</>
|
||||
return <Action key={key} item={item} selectedRows={selectedRows} />
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
GlobalActions.propTypes = {
|
||||
refetch: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
isLoading: PropTypes.bool,
|
||||
singleSelect: PropTypes.bool,
|
||||
disableRowSelect: PropTypes.bool,
|
||||
globalActions: PropTypes.array,
|
||||
useTableProps: PropTypes.object,
|
||||
disableRowSelect: PropTypes.bool,
|
||||
}
|
||||
|
||||
export default GlobalActions
|
||||
|
@ -13,103 +13,71 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { JSXElementConstructor, useState, useCallback } from 'react'
|
||||
import { Fragment, useMemo, ReactElement } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import clsx from 'clsx'
|
||||
import { alpha, debounce, InputBase } from '@mui/material'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { Search as SearchIcon } from 'iconoir-react'
|
||||
import {
|
||||
UseGlobalFiltersInstanceProps,
|
||||
UseGlobalFiltersState,
|
||||
} from 'react-table'
|
||||
import { Stack, Button } from '@mui/material'
|
||||
import { Filter } from 'iconoir-react'
|
||||
import { TableInstance, UseTableInstanceProps } from 'react-table'
|
||||
|
||||
const useStyles = makeStyles(({ spacing, palette, shape, breakpoints }) => ({
|
||||
search: {
|
||||
position: 'relative',
|
||||
borderRadius: shape.borderRadius,
|
||||
backgroundColor: alpha(palette.divider, 0.15),
|
||||
'&:hover': {
|
||||
backgroundColor: alpha(palette.divider, 0.25),
|
||||
},
|
||||
width: '100%',
|
||||
[breakpoints.up('sm')]: {
|
||||
width: 'auto',
|
||||
},
|
||||
},
|
||||
searchIcon: {
|
||||
padding: spacing(0, 2),
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
inputRoot: {
|
||||
color: 'inherit',
|
||||
width: '100%',
|
||||
},
|
||||
inputInput: {
|
||||
padding: spacing(1, 1, 1, 0),
|
||||
// vertical padding + font size from searchIcon
|
||||
paddingLeft: `calc(1em + ${spacing(4)})`,
|
||||
},
|
||||
}))
|
||||
import HeaderPopover from 'client/components/Header/Popover'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
/**
|
||||
* Render search input.
|
||||
* Render all selected sorters.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {string} props.className - Class to wrapper root
|
||||
* @param {object} props.searchProps - Props for search input
|
||||
* @param {UseGlobalFiltersInstanceProps} props.useTableProps - Table props
|
||||
* @returns {JSXElementConstructor} Component JSX
|
||||
* @param {string} [props.className] - Class name for the container
|
||||
* @param {TableInstance} props.useTableProps - Table props
|
||||
* @returns {ReactElement} Component JSX
|
||||
*/
|
||||
const GlobalFilter = ({ useTableProps, className, searchProps }) => {
|
||||
const classes = useStyles()
|
||||
const GlobalFilter = ({ className, useTableProps }) => {
|
||||
/** @type {UseTableInstanceProps} */
|
||||
const { rows, columns, setAllFilters } = useTableProps
|
||||
|
||||
const { setGlobalFilter, state } = useTableProps
|
||||
|
||||
/** @type {UseGlobalFiltersState} */
|
||||
const { globalFilter } = state
|
||||
|
||||
const [value, setValue] = useState(() => globalFilter)
|
||||
|
||||
const handleChange = useCallback(
|
||||
// Set undefined to remove the filter entirely
|
||||
debounce((newFilter) => setGlobalFilter(newFilter || undefined), 200),
|
||||
[setGlobalFilter]
|
||||
const columnsCanFilter = useMemo(
|
||||
() => columns.filter(({ canFilter }) => canFilter),
|
||||
[columns]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.search, className)}>
|
||||
<div className={classes.searchIcon}>
|
||||
<SearchIcon />
|
||||
</div>
|
||||
<InputBase
|
||||
value={value ?? ''}
|
||||
type="search"
|
||||
onChange={(event) => {
|
||||
setValue(event.target.value)
|
||||
handleChange(event.target.value)
|
||||
return !columnsCanFilter.length ? null : (
|
||||
<Stack className={className} direction="row" gap="0.5em" flexWrap="wrap">
|
||||
<HeaderPopover
|
||||
id="filter-by-button"
|
||||
icon={<Filter />}
|
||||
buttonLabel={T.FilterBy}
|
||||
buttonProps={{
|
||||
'data-cy': 'filter-by-button',
|
||||
variant: 'outlined',
|
||||
color: 'secondary',
|
||||
disabled: rows?.length === 0,
|
||||
}}
|
||||
placeholder={'Search...'}
|
||||
classes={{
|
||||
root: classes.inputRoot,
|
||||
input: classes.inputInput,
|
||||
}}
|
||||
inputProps={{ 'aria-label': 'search', ...(searchProps ?? {}) }}
|
||||
/>
|
||||
</div>
|
||||
popperProps={{ placement: 'bottom-end' }}
|
||||
>
|
||||
{() => (
|
||||
<Stack sx={{ width: { xs: '100%', md: 500 }, p: 2 }}>
|
||||
{columnsCanFilter.map((column, idx) => (
|
||||
<Fragment key={idx}>{column.render('Filter')}</Fragment>
|
||||
))}
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => setAllFilters([])}
|
||||
sx={{ mt: 2, alignSelf: 'flex-end' }}
|
||||
>
|
||||
<Translate word={T.Clear} />
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
</HeaderPopover>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
GlobalFilter.propTypes = {
|
||||
className: PropTypes.string,
|
||||
useTableProps: PropTypes.object.isRequired,
|
||||
searchProps: PropTypes.object,
|
||||
}
|
||||
|
||||
export default GlobalFilter
|
||||
|
@ -0,0 +1,115 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { JSXElementConstructor, useState, useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import clsx from 'clsx'
|
||||
import { alpha, debounce, InputBase } from '@mui/material'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { Search as SearchIcon } from 'iconoir-react'
|
||||
import {
|
||||
UseGlobalFiltersInstanceProps,
|
||||
UseGlobalFiltersState,
|
||||
} from 'react-table'
|
||||
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const useStyles = makeStyles(({ spacing, palette, shape, breakpoints }) => ({
|
||||
search: {
|
||||
position: 'relative',
|
||||
borderRadius: shape.borderRadius,
|
||||
backgroundColor: alpha(palette.divider, 0.15),
|
||||
'&:hover': {
|
||||
backgroundColor: alpha(palette.divider, 0.25),
|
||||
},
|
||||
width: '100%',
|
||||
[breakpoints.up('sm')]: {
|
||||
width: 'auto',
|
||||
},
|
||||
},
|
||||
searchIcon: {
|
||||
padding: spacing(0, 2),
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
inputRoot: {
|
||||
color: 'inherit',
|
||||
width: '100%',
|
||||
},
|
||||
inputInput: {
|
||||
padding: spacing(1, 1, 1, 0),
|
||||
// vertical padding + font size from searchIcon
|
||||
paddingLeft: `calc(1em + ${spacing(4)})`,
|
||||
},
|
||||
}))
|
||||
|
||||
/**
|
||||
* Render search input.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {string} [props.className] - Class name for the container
|
||||
* @param {object} props.searchProps - Props for search input
|
||||
* @param {UseGlobalFiltersInstanceProps} props.useTableProps - Table props
|
||||
* @returns {JSXElementConstructor} Component JSX
|
||||
*/
|
||||
const GlobalSearch = ({ className, useTableProps, searchProps }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const { setGlobalFilter, state } = useTableProps
|
||||
|
||||
/** @type {UseGlobalFiltersState} */
|
||||
const { globalFilter } = state
|
||||
|
||||
const [value, setValue] = useState(() => globalFilter)
|
||||
|
||||
const handleChange = useCallback(
|
||||
// Set undefined to remove the filter entirely
|
||||
debounce((newFilter) => setGlobalFilter(newFilter || undefined), 200),
|
||||
[setGlobalFilter]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.search, className)}>
|
||||
<div className={classes.searchIcon}>
|
||||
<SearchIcon />
|
||||
</div>
|
||||
<InputBase
|
||||
value={value ?? ''}
|
||||
type="search"
|
||||
onChange={(event) => {
|
||||
setValue(event.target.value)
|
||||
handleChange(event.target.value)
|
||||
}}
|
||||
placeholder={`${Tr(T.Search)}...`}
|
||||
classes={{ root: classes.inputRoot, input: classes.inputInput }}
|
||||
inputProps={{ 'aria-label': 'search', ...(searchProps ?? {}) }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
GlobalSearch.propTypes = {
|
||||
className: PropTypes.string,
|
||||
useTableProps: PropTypes.object.isRequired,
|
||||
searchProps: PropTypes.object,
|
||||
}
|
||||
|
||||
export default GlobalSearch
|
@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { useEffect, useMemo, JSXElementConstructor } from 'react'
|
||||
import { useEffect, useMemo, ReactElement } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { SortDown, ArrowDown, ArrowUp } from 'iconoir-react'
|
||||
@ -25,16 +25,18 @@ import {
|
||||
} from 'react-table'
|
||||
|
||||
import HeaderPopover from 'client/components/Header/Popover'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
/**
|
||||
* Render all selected sorters.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {string} [props.className] - Class name for the container
|
||||
* @param {TableInstance} props.useTableProps - Table props
|
||||
* @returns {JSXElementConstructor} Component JSX
|
||||
* @returns {ReactElement} Component JSX
|
||||
*/
|
||||
const GlobalSort = ({ useTableProps }) => {
|
||||
const GlobalSort = ({ className, useTableProps }) => {
|
||||
const { headers, state } = useTableProps
|
||||
|
||||
/** @type {UseSortByInstanceProps} */
|
||||
@ -66,7 +68,7 @@ const GlobalSort = ({ useTableProps }) => {
|
||||
useEffect(() => () => setSortBy([]), [])
|
||||
|
||||
return !headersNotSorted.length && !sortBy.length ? null : (
|
||||
<Stack direction="row" gap="0.5em" flexWrap="wrap">
|
||||
<Stack className={className} direction="row" gap="0.5em" flexWrap="wrap">
|
||||
{useMemo(
|
||||
() => (
|
||||
<HeaderPopover
|
||||
@ -79,24 +81,15 @@ const GlobalSort = ({ useTableProps }) => {
|
||||
variant: 'outlined',
|
||||
color: 'secondary',
|
||||
}}
|
||||
popperProps={{ placement: 'bottom-start' }}
|
||||
popperProps={{ placement: 'bottom-end' }}
|
||||
>
|
||||
{() => (
|
||||
<MenuList>
|
||||
{headersNotSorted.length ? (
|
||||
headersNotSorted?.map(({ id, Header: name }) => (
|
||||
<MenuItem
|
||||
key={id}
|
||||
onClick={() => {
|
||||
handleClick(id, name)
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))
|
||||
) : (
|
||||
<span>{T.Empty}</span>
|
||||
)}
|
||||
{headersNotSorted?.map(({ id, Header: name }) => (
|
||||
<MenuItem key={id} onClick={() => handleClick(id, name)}>
|
||||
<Translate word={name} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
)}
|
||||
</HeaderPopover>
|
||||
@ -122,6 +115,7 @@ const GlobalSort = ({ useTableProps }) => {
|
||||
}
|
||||
|
||||
GlobalSort.propTypes = {
|
||||
className: PropTypes.string,
|
||||
useTableProps: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
|
@ -1,143 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Cancel } from 'iconoir-react'
|
||||
import { List, ListSubheader, IconButton } from '@mui/material'
|
||||
import { TreeView, TreeItem } from '@mui/lab'
|
||||
import { UseFiltersInstanceProps } from 'react-table'
|
||||
|
||||
import { Translate } from 'client/components/HOC'
|
||||
|
||||
const buildTree = (data = [], separator = '/') => {
|
||||
const mapper = {}
|
||||
const tree = {
|
||||
id: 'root',
|
||||
name: 'Labels',
|
||||
children: [],
|
||||
}
|
||||
|
||||
for (const labelString of data) {
|
||||
const splits = labelString.split(separator)
|
||||
let label = ''
|
||||
|
||||
splits.reduce((parent, place) => {
|
||||
if (label) {
|
||||
label += `${separator}${place}`
|
||||
} else {
|
||||
label = place
|
||||
}
|
||||
|
||||
if (place && !mapper[label]) {
|
||||
const o = { id: label }
|
||||
mapper[label] = o
|
||||
mapper[label].name = place
|
||||
parent.children = parent.children || []
|
||||
parent.children.push(o)
|
||||
}
|
||||
|
||||
return mapper[label]
|
||||
}, tree)
|
||||
}
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
const LabelFilter = ({ title, column }) => {
|
||||
/** @type {UseFiltersInstanceProps} */
|
||||
const { setFilter, id, preFilteredRows, filterValue = [] } = column
|
||||
|
||||
useEffect(() => () => setFilter([]), [])
|
||||
|
||||
const labels = useMemo(() => {
|
||||
const uniqueLabels = new Set()
|
||||
|
||||
preFilteredRows?.forEach((row) => {
|
||||
const labelsFromTemplate = row.values[id]
|
||||
|
||||
labelsFromTemplate.forEach(uniqueLabels.add, uniqueLabels)
|
||||
})
|
||||
|
||||
return [...uniqueLabels.values()]
|
||||
}, [id, preFilteredRows])
|
||||
|
||||
const tree = useMemo(() => buildTree(labels), [labels])
|
||||
|
||||
const handleSelect = (value) => setFilter([...filterValue, value])
|
||||
|
||||
const handleUnselect = (value) =>
|
||||
setFilter(filterValue.filter((v) => v !== value))
|
||||
|
||||
const handleClear = () => setFilter(undefined)
|
||||
|
||||
const isFiltered = useMemo(() => filterValue?.length > 0, [filterValue])
|
||||
|
||||
const renderTree = ({ id: treeId, name, children }) => (
|
||||
<TreeItem key={treeId} nodeId={treeId} label={name}>
|
||||
{Array.isArray(children)
|
||||
? children.map((node) => renderTree(node))
|
||||
: null}
|
||||
</TreeItem>
|
||||
)
|
||||
|
||||
return (
|
||||
<List>
|
||||
{title && (
|
||||
<ListSubheader
|
||||
disableSticky
|
||||
disableGutters
|
||||
sx={{ display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<Translate word={title} />
|
||||
{isFiltered && (
|
||||
<IconButton
|
||||
disableRipple
|
||||
disablePadding
|
||||
size="small"
|
||||
onClick={handleClear}
|
||||
>
|
||||
<Cancel />
|
||||
</IconButton>
|
||||
)}
|
||||
</ListSubheader>
|
||||
)}
|
||||
|
||||
<TreeView
|
||||
selected={filterValue}
|
||||
defaultExpanded={['root', ...labels]}
|
||||
onNodeSelect={(evt, value) => {
|
||||
evt.preventDefault()
|
||||
|
||||
filterValue.includes?.(value)
|
||||
? handleUnselect(value)
|
||||
: handleSelect(value)
|
||||
}}
|
||||
>
|
||||
{renderTree(tree)}
|
||||
</TreeView>
|
||||
</List>
|
||||
)
|
||||
}
|
||||
|
||||
LabelFilter.propTypes = {
|
||||
column: PropTypes.object,
|
||||
icon: PropTypes.node,
|
||||
title: PropTypes.string,
|
||||
}
|
||||
|
||||
export default LabelFilter
|
@ -13,43 +13,50 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useCallback } from 'react'
|
||||
import { ReactElement } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Row as RowType } from 'react-table'
|
||||
|
||||
const Row = ({ row, handleClick }) => {
|
||||
/** @type {RowType} */
|
||||
const { getRowProps, cells, isSelected } = row
|
||||
import { Stack, TextField } from '@mui/material'
|
||||
import { UseFiltersInstanceProps } from 'react-table'
|
||||
|
||||
const renderCell = useCallback(
|
||||
(cell) => (
|
||||
<div {...cell.getCellProps()} data-header={cell.column.Header}>
|
||||
{cell.render('Cell')}
|
||||
</div>
|
||||
),
|
||||
[]
|
||||
)
|
||||
import { Tr } from 'client/components/HOC'
|
||||
|
||||
return (
|
||||
<div
|
||||
{...getRowProps()}
|
||||
className={isSelected ? 'selected' : ''}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{cells?.map(renderCell)}
|
||||
</div>
|
||||
)
|
||||
/**
|
||||
* Render category filter to table.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {UseFiltersInstanceProps} props.column - Props
|
||||
* @returns {ReactElement} Component JSX
|
||||
*/
|
||||
const CategoryFilter = ({ column: { Header, filterValue, setFilter, id } }) => (
|
||||
<Stack direction="row">
|
||||
<TextField
|
||||
fullWidth
|
||||
label={Tr(Header)}
|
||||
value={new Date(filterValue)}
|
||||
onChange={(evt) => {
|
||||
console.log(evt.target.value)
|
||||
}}
|
||||
color="secondary"
|
||||
type="date"
|
||||
inputProps={{ 'data-cy': `after-${id}` }}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={Tr(Header)}
|
||||
value={new Date(filterValue)}
|
||||
onChange={(evt) => {
|
||||
console.log(evt.target.value)
|
||||
}}
|
||||
color="secondary"
|
||||
type="date"
|
||||
inputProps={{ 'data-cy': `before-${id}` }}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
|
||||
CategoryFilter.propTypes = {
|
||||
column: PropTypes.object,
|
||||
}
|
||||
|
||||
Row.propTypes = {
|
||||
row: PropTypes.object,
|
||||
handleClick: PropTypes.func,
|
||||
}
|
||||
|
||||
Row.defaultProps = {
|
||||
row: {},
|
||||
handleClick: undefined,
|
||||
}
|
||||
|
||||
export default Row
|
||||
export default CategoryFilter
|
@ -16,9 +16,10 @@
|
||||
import CategoryFilter from 'client/components/Tables/Enhanced/Utils/CategoryFilter'
|
||||
import GlobalActions from 'client/components/Tables/Enhanced/Utils/GlobalActions'
|
||||
import GlobalFilter from 'client/components/Tables/Enhanced/Utils/GlobalFilter'
|
||||
import GlobalSearch from 'client/components/Tables/Enhanced/Utils/GlobalSearch'
|
||||
import GlobalSelectedRows from 'client/components/Tables/Enhanced/Utils/GlobalSelectedRows'
|
||||
import GlobalSort from 'client/components/Tables/Enhanced/Utils/GlobalSort'
|
||||
import LabelFilter from 'client/components/Tables/Enhanced/Utils/LabelFilter'
|
||||
import TimeFilter from 'client/components/Tables/Enhanced/Utils/TimeFilter'
|
||||
|
||||
export * from 'client/components/Tables/Enhanced/Utils/GlobalActions/Action'
|
||||
export * from 'client/components/Tables/Enhanced/Utils/utils'
|
||||
@ -27,7 +28,8 @@ export {
|
||||
CategoryFilter,
|
||||
GlobalActions,
|
||||
GlobalFilter,
|
||||
GlobalSearch,
|
||||
GlobalSelectedRows,
|
||||
GlobalSort,
|
||||
LabelFilter,
|
||||
TimeFilter,
|
||||
}
|
||||
|
@ -14,22 +14,30 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { Column } from 'react-table'
|
||||
import CategoryFilter from 'client/components/Tables/Enhanced/Utils/CategoryFilter'
|
||||
import { GlobalAction } from 'client/components/Tables/Enhanced/Utils/GlobalActions/Action'
|
||||
|
||||
import {
|
||||
GlobalAction,
|
||||
CategoryFilter,
|
||||
TimeFilter,
|
||||
} from 'client/components/Tables/Enhanced/Utils'
|
||||
|
||||
/**
|
||||
* Add filters defined in view yaml to columns.
|
||||
*
|
||||
* @param {object} config - Config
|
||||
* @param {object} config.filters - List of criteria to filter the columns.
|
||||
* @param {object} config.filters - List of criteria to filter the columns
|
||||
* @param {Column[]} config.columns - Columns
|
||||
* @returns {object} Column with filters
|
||||
* @returns {Column[]} Column with filters
|
||||
*/
|
||||
export const createColumns = ({ filters = {}, columns = [] }) => {
|
||||
if (Object.keys(filters).length === 0) return columns
|
||||
|
||||
return columns.map((column) => {
|
||||
const { Header, id = '', accessor } = column
|
||||
const { id = '', accessor, noFilterIds = [] } = column
|
||||
|
||||
// noFilterIds is a list of column ids that should not have a filter
|
||||
// it's defined in the resource columns definition
|
||||
if (noFilterIds.includes(id)) return column
|
||||
|
||||
const filterById = !!filters[String(id.toLowerCase())]
|
||||
|
||||
@ -38,26 +46,53 @@ export const createColumns = ({ filters = {}, columns = [] }) => {
|
||||
|
||||
return {
|
||||
...column,
|
||||
...((filterById || filterByAccessor) && createCategoryFilter(Header)),
|
||||
...((filterById || filterByAccessor) &&
|
||||
(
|
||||
{
|
||||
// TODO: Add label to filters
|
||||
// label: createLabelFilter,
|
||||
time: createTimeFilter,
|
||||
}[`${id}`.toLowerCase()] ?? createCategoryFilter
|
||||
)(column)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create label filter as column.
|
||||
*
|
||||
* @param {Column} column - Column
|
||||
* @returns {Column} - Label filter
|
||||
*/
|
||||
// export const createLabelFilter = (column) => ({
|
||||
// disableFilters: false,
|
||||
// Filter: LabelFilter,
|
||||
// ...column,
|
||||
// })
|
||||
|
||||
/**
|
||||
* Create time filter as column.
|
||||
*
|
||||
* @param {Column} column - Column
|
||||
* @returns {Column} - Time filter
|
||||
*/
|
||||
export const createTimeFilter = (column) => ({
|
||||
disableFilters: false,
|
||||
Filter: TimeFilter,
|
||||
...column,
|
||||
})
|
||||
|
||||
/**
|
||||
* Create category filter as column.
|
||||
*
|
||||
* @param {string} title - Title
|
||||
* @param {Column} column - Column
|
||||
* @returns {Column} - Category filter
|
||||
*/
|
||||
export const createCategoryFilter = (title) => ({
|
||||
export const createCategoryFilter = (column) => ({
|
||||
disableFilters: false,
|
||||
Filter: ({ column }) =>
|
||||
CategoryFilter({
|
||||
column,
|
||||
multiple: true,
|
||||
title,
|
||||
}),
|
||||
Filter: CategoryFilter,
|
||||
filter: 'includesValue',
|
||||
...column,
|
||||
})
|
||||
|
||||
/**
|
||||
|
@ -1,117 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
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 { GlobalFilter } from 'client/components/Tables/Enhanced/Utils'
|
||||
|
||||
const useToolbarStyles = makeStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
},
|
||||
rootNoFilters: {
|
||||
gridColumn: '1 / -1',
|
||||
'& ~ div': {
|
||||
gridColumn: '1 / -1',
|
||||
},
|
||||
},
|
||||
content: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
contentWithFilter: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1em',
|
||||
},
|
||||
filters: {
|
||||
flexGrow: 1,
|
||||
overflow: 'auto',
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {object} props - Props
|
||||
* @param {boolean} props.onlyGlobalSearch - Show only the global search
|
||||
* @param {object} props.searchProps - Props for search input
|
||||
* @param {UseTableInstanceProps} props.useTableProps - Table props
|
||||
* @returns {JSXElementConstructor} Returns table toolbar
|
||||
*/
|
||||
const Filters = ({ onlyGlobalSearch, useTableProps, searchProps }) => {
|
||||
const classes = useToolbarStyles()
|
||||
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('md'))
|
||||
|
||||
/** @type {UseTableInstanceProps} */
|
||||
const { rows, columns } = useTableProps
|
||||
|
||||
const filters = useMemo(
|
||||
() =>
|
||||
columns
|
||||
.filter(({ canFilter }) => canFilter)
|
||||
.map((column, idx) =>
|
||||
column.canFilter ? (
|
||||
<Fragment key={idx}>{column.render('Filter')}</Fragment>
|
||||
) : null
|
||||
),
|
||||
[rows]
|
||||
)
|
||||
|
||||
if (isMobile || onlyGlobalSearch) {
|
||||
return (
|
||||
<GlobalFilter
|
||||
className={classes.rootNoFilters}
|
||||
useTableProps={useTableProps}
|
||||
searchProps={searchProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const noFilters = filters.length === 0
|
||||
|
||||
return (
|
||||
<Card
|
||||
variant="outlined"
|
||||
className={clsx(classes.root, { [classes.rootNoFilters]: noFilters })}
|
||||
>
|
||||
<CardContent
|
||||
className={clsx(classes.content, {
|
||||
[classes.contentWithFilter]: !noFilters,
|
||||
})}
|
||||
>
|
||||
<GlobalFilter useTableProps={useTableProps} searchProps={searchProps} />
|
||||
|
||||
{!noFilters && <div className={classes.filters}>{filters}</div>}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
Filters.propTypes = {
|
||||
onlyGlobalSearch: PropTypes.bool,
|
||||
useTableProps: PropTypes.object,
|
||||
searchProps: PropTypes.object,
|
||||
}
|
||||
|
||||
Filters.defaultProps = {
|
||||
onlyGlobalSearch: false,
|
||||
useTableProps: {},
|
||||
}
|
||||
|
||||
export default Filters
|
@ -32,9 +32,14 @@ import {
|
||||
UseRowSelectRowProps,
|
||||
} from 'react-table'
|
||||
|
||||
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 {
|
||||
GlobalActions,
|
||||
GlobalSearch,
|
||||
GlobalFilter,
|
||||
GlobalSort,
|
||||
GlobalSelectedRows,
|
||||
} from 'client/components/Tables/Enhanced/Utils'
|
||||
import EnhancedTableStyles from 'client/components/Tables/Enhanced/styles'
|
||||
|
||||
import { Translate } from 'client/components/HOC'
|
||||
@ -50,8 +55,7 @@ const EnhancedTable = ({
|
||||
initialState,
|
||||
refetch,
|
||||
isLoading,
|
||||
onlyGlobalSearch,
|
||||
onlyGlobalSelectedRows,
|
||||
displaySelectedRows,
|
||||
disableRowSelect,
|
||||
disableGlobalSort,
|
||||
onSelectedRowsChange,
|
||||
@ -68,7 +72,7 @@ const EnhancedTable = ({
|
||||
|
||||
const isUninitialized = useMemo(
|
||||
() => isLoading && data === undefined,
|
||||
[isLoading, data]
|
||||
[isLoading, data?.length]
|
||||
)
|
||||
|
||||
const defaultColumn = useMemo(() => ({ disableFilters: true }), [])
|
||||
@ -149,80 +153,88 @@ const EnhancedTable = ({
|
||||
{...rootProps}
|
||||
>
|
||||
<div className={styles.toolbar}>
|
||||
{/* TOOLBAR */}
|
||||
<Toolbar
|
||||
{/* ACTIONS */}
|
||||
<GlobalActions
|
||||
className={styles.actions}
|
||||
refetch={refetch}
|
||||
isLoading={isLoading}
|
||||
globalActions={globalActions}
|
||||
singleSelect={singleSelect}
|
||||
disableRowSelect={disableRowSelect}
|
||||
disableGlobalSort={disableGlobalSort}
|
||||
onlyGlobalSelectedRows={onlyGlobalSelectedRows}
|
||||
globalActions={globalActions}
|
||||
useTableProps={useTableProps}
|
||||
/>
|
||||
|
||||
{/* PAGINATION */}
|
||||
<div className={styles.pagination}>
|
||||
<Pagination
|
||||
handleChangePage={handleChangePage}
|
||||
useTableProps={useTableProps}
|
||||
count={rows.length}
|
||||
showPageCount={showPageCount}
|
||||
/>
|
||||
<Pagination
|
||||
className={styles.pagination}
|
||||
handleChangePage={handleChangePage}
|
||||
useTableProps={useTableProps}
|
||||
count={rows.length}
|
||||
showPageCount={showPageCount}
|
||||
/>
|
||||
|
||||
{/* SEARCH */}
|
||||
<GlobalSearch
|
||||
className={styles.search}
|
||||
useTableProps={useTableProps}
|
||||
searchProps={searchProps}
|
||||
/>
|
||||
|
||||
{/* FILTERS */}
|
||||
<div className={styles.filters}>
|
||||
<GlobalFilter useTableProps={useTableProps} />
|
||||
{!disableGlobalSort && <GlobalSort useTableProps={useTableProps} />}
|
||||
</div>
|
||||
|
||||
{/* SELECTED ROWS */}
|
||||
{displaySelectedRows && (
|
||||
<div>
|
||||
<GlobalSelectedRows useTableProps={useTableProps} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.table}>
|
||||
{/* FILTERS */}
|
||||
{!isUninitialized && (
|
||||
<Filters
|
||||
onlyGlobalSearch={onlyGlobalSearch}
|
||||
useTableProps={useTableProps}
|
||||
searchProps={searchProps}
|
||||
/>
|
||||
<div className={clsx(styles.body, classes.body)}>
|
||||
{/* NO DATA MESSAGE */}
|
||||
{!isUninitialized && page?.length === 0 && (
|
||||
<span className={styles.noDataMessage}>
|
||||
<InfoEmpty />
|
||||
<Translate word={T.NoDataAvailable} />
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className={clsx(styles.body, classes.body)}>
|
||||
{/* NO DATA MESSAGE */}
|
||||
{!isUninitialized && page?.length === 0 && (
|
||||
<span className={styles.noDataMessage}>
|
||||
<InfoEmpty />
|
||||
<Translate word={T.NoDataAvailable} />
|
||||
</span>
|
||||
)}
|
||||
{/* DATALIST PER PAGE */}
|
||||
{page.map((row) => {
|
||||
prepareRow(row)
|
||||
|
||||
{/* DATALIST PER PAGE */}
|
||||
{page.map((row) => {
|
||||
prepareRow(row)
|
||||
/** @type {UseRowSelectRowProps} */
|
||||
const {
|
||||
getRowProps,
|
||||
original,
|
||||
values,
|
||||
toggleRowSelected,
|
||||
isSelected,
|
||||
} = row
|
||||
const { key, ...rowProps } = getRowProps()
|
||||
|
||||
/** @type {UseRowSelectRowProps} */
|
||||
const {
|
||||
getRowProps,
|
||||
original,
|
||||
values,
|
||||
toggleRowSelected,
|
||||
isSelected,
|
||||
} = row
|
||||
const { key, ...rowProps } = getRowProps()
|
||||
return (
|
||||
<RowComponent
|
||||
{...rowProps}
|
||||
key={key}
|
||||
original={original}
|
||||
value={values}
|
||||
className={isSelected ? 'selected' : ''}
|
||||
onClick={() => {
|
||||
typeof onRowClick === 'function' && onRowClick(original)
|
||||
|
||||
return (
|
||||
<RowComponent
|
||||
{...rowProps}
|
||||
key={key}
|
||||
original={original}
|
||||
value={values}
|
||||
className={isSelected ? 'selected' : ''}
|
||||
onClick={() => {
|
||||
typeof onRowClick === 'function' && onRowClick(original)
|
||||
|
||||
if (!disableRowSelect) {
|
||||
singleSelect && toggleAllRowsSelected?.(false)
|
||||
toggleRowSelected?.(!isSelected)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
if (!disableRowSelect) {
|
||||
singleSelect && toggleAllRowsSelected?.(false)
|
||||
toggleRowSelected?.(!isSelected)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Box>
|
||||
)
|
||||
@ -251,8 +263,7 @@ EnhancedTable.propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
disableGlobalSort: PropTypes.bool,
|
||||
disableRowSelect: PropTypes.bool,
|
||||
onlyGlobalSearch: PropTypes.bool,
|
||||
onlyGlobalSelectedRows: PropTypes.bool,
|
||||
displaySelectedRows: PropTypes.bool,
|
||||
onSelectedRowsChange: PropTypes.func,
|
||||
onRowClick: PropTypes.func,
|
||||
pageSize: PropTypes.number,
|
||||
|
@ -17,13 +17,14 @@
|
||||
import { useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Button, Typography } from '@mui/material'
|
||||
import { Stack, Button, Typography } from '@mui/material'
|
||||
import { NavArrowLeft, NavArrowRight } from 'iconoir-react'
|
||||
import { UsePaginationState } from 'react-table'
|
||||
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const Pagination = ({
|
||||
className,
|
||||
count = 0,
|
||||
handleChangePage,
|
||||
useTableProps,
|
||||
@ -43,7 +44,13 @@ const Pagination = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
className={className}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="end"
|
||||
gap="1em"
|
||||
>
|
||||
<Button
|
||||
aria-label="previous page"
|
||||
disabled={pageIndex === 0}
|
||||
@ -67,11 +74,12 @@ const Pagination = ({
|
||||
{T.Next}
|
||||
<NavArrowRight />
|
||||
</Button>
|
||||
</>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
Pagination.propTypes = {
|
||||
className: PropTypes.string,
|
||||
handleChangePage: PropTypes.func.isRequired,
|
||||
useTableProps: PropTypes.object.isRequired,
|
||||
count: PropTypes.number.isRequired,
|
||||
|
@ -26,39 +26,35 @@ export default makeStyles(({ palette, typography, breakpoints }) => ({
|
||||
...typography.body1,
|
||||
marginBottom: 16,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr auto',
|
||||
gridTemplateRows: 'auto auto',
|
||||
gridTemplateAreas: `
|
||||
'actions actions pagination'
|
||||
'search search filters'`,
|
||||
alignItems: 'start',
|
||||
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',
|
||||
},
|
||||
[breakpoints.down('md')]: {
|
||||
gridTemplateAreas: `
|
||||
'actions actions actions'
|
||||
'pagination pagination pagination'
|
||||
'search search filters'`,
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
gridArea: 'actions',
|
||||
},
|
||||
pagination: {
|
||||
gridArea: 'pagination',
|
||||
},
|
||||
search: {
|
||||
gridArea: 'search',
|
||||
},
|
||||
filters: {
|
||||
gridArea: 'filters',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'end',
|
||||
justifySelf: 'end',
|
||||
gap: '1em',
|
||||
},
|
||||
loading: {
|
||||
transition: '200ms',
|
||||
},
|
||||
table: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'minmax(auto, 300px) 1fr',
|
||||
gap: '0.8em',
|
||||
overflow: 'auto',
|
||||
[breakpoints.down('md')]: {
|
||||
gridTemplateColumns: 'minmax(0, 1fr)',
|
||||
},
|
||||
},
|
||||
body: {
|
||||
overflow: 'auto',
|
||||
display: 'grid',
|
||||
|
@ -1,114 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { JSXElementConstructor } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Stack, useMediaQuery } from '@mui/material'
|
||||
import { UseTableInstanceProps, UseRowSelectState } from 'react-table'
|
||||
import { RefreshDouble } from 'iconoir-react'
|
||||
|
||||
import {
|
||||
GlobalActions,
|
||||
GlobalAction,
|
||||
GlobalSelectedRows,
|
||||
GlobalSort,
|
||||
} from 'client/components/Tables/Enhanced/Utils'
|
||||
import { SubmitButton } from 'client/components/FormControl'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
/**
|
||||
* @param {object} props - Props
|
||||
* @param {GlobalAction[]} props.globalActions - Global actions
|
||||
* @param {boolean} props.onlyGlobalSelectedRows - Show only the selected rows
|
||||
* @param {boolean} props.disableRowSelect - Rows can't select
|
||||
* @param {boolean} props.disableGlobalSort - Hide the sort filters
|
||||
* @param {UseTableInstanceProps} props.useTableProps - Table props
|
||||
* @param {function():Promise} props.refetch - Function to refetch data
|
||||
* @param {boolean} props.isLoading - The data is fetching
|
||||
* @returns {JSXElementConstructor} Returns table toolbar
|
||||
*/
|
||||
const Toolbar = ({
|
||||
globalActions,
|
||||
onlyGlobalSelectedRows,
|
||||
disableGlobalSort = false,
|
||||
disableRowSelect = false,
|
||||
useTableProps,
|
||||
refetch,
|
||||
isLoading,
|
||||
}) => {
|
||||
const isSmallDevice = useMediaQuery((theme) => theme.breakpoints.down('md'))
|
||||
|
||||
/** @type {UseRowSelectState} */
|
||||
const { selectedRowIds } = useTableProps?.state ?? {}
|
||||
|
||||
const enableGlobalSort = !isSmallDevice && !disableGlobalSort
|
||||
const enableGlobalSelect =
|
||||
!onlyGlobalSelectedRows && !!Object.keys(selectedRowIds).length
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" flexWrap="wrap" alignItems="center" gap="1em">
|
||||
{refetch && (
|
||||
<SubmitButton
|
||||
data-cy="refresh"
|
||||
icon={<RefreshDouble />}
|
||||
title={T.Tooltip}
|
||||
isSubmitting={isLoading}
|
||||
onClick={refetch}
|
||||
/>
|
||||
)}
|
||||
{onlyGlobalSelectedRows && !disableRowSelect ? (
|
||||
<GlobalSelectedRows useTableProps={useTableProps} />
|
||||
) : (
|
||||
<GlobalActions
|
||||
refetch={refetch}
|
||||
isLoading={isLoading}
|
||||
disableRowSelect={disableRowSelect}
|
||||
globalActions={globalActions}
|
||||
useTableProps={useTableProps}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
{(enableGlobalSort || enableGlobalSelect) && (
|
||||
<Stack
|
||||
className="summary"
|
||||
direction="row"
|
||||
flexWrap="wrap"
|
||||
alignItems="center"
|
||||
gap={'1em'}
|
||||
width={1}
|
||||
>
|
||||
{enableGlobalSort && <GlobalSort useTableProps={useTableProps} />}
|
||||
{enableGlobalSelect && (
|
||||
<GlobalSelectedRows withAlert useTableProps={useTableProps} />
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Toolbar.propTypes = {
|
||||
globalActions: PropTypes.array,
|
||||
onlyGlobalSelectedRows: PropTypes.bool,
|
||||
disableRowSelect: PropTypes.bool,
|
||||
disableGlobalSort: PropTypes.bool,
|
||||
useTableProps: PropTypes.object,
|
||||
refetch: PropTypes.func,
|
||||
isLoading: PropTypes.bool,
|
||||
}
|
||||
|
||||
export default Toolbar
|
@ -13,54 +13,25 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { CategoryFilter } from 'client/components/Tables/Enhanced/Utils'
|
||||
import * as MarketplaceAppModel from 'client/models/MarketplaceApp'
|
||||
import { Column } from 'react-table'
|
||||
|
||||
export default [
|
||||
{ Header: 'ID', accessor: 'ID', sortType: 'number' },
|
||||
{ Header: 'Name', accessor: 'NAME' },
|
||||
{ Header: 'Owner', accessor: 'UNAME' },
|
||||
{ Header: 'Group', accessor: 'GNAME' },
|
||||
{
|
||||
Header: 'State',
|
||||
id: 'STATE',
|
||||
accessor: (row) => MarketplaceAppModel.getState(row)?.name,
|
||||
disableFilters: false,
|
||||
Filter: ({ column }) =>
|
||||
CategoryFilter({
|
||||
column,
|
||||
multiple: true,
|
||||
title: 'State',
|
||||
}),
|
||||
filter: 'includesValue',
|
||||
},
|
||||
{
|
||||
Header: 'Type',
|
||||
id: 'TYPE',
|
||||
accessor: (row) => MarketplaceAppModel.getType(row),
|
||||
disableFilters: false,
|
||||
Filter: ({ column }) =>
|
||||
CategoryFilter({
|
||||
column,
|
||||
multiple: true,
|
||||
title: 'Type',
|
||||
}),
|
||||
filter: 'includesValue',
|
||||
},
|
||||
{ Header: 'Size', accessor: 'SIZE' },
|
||||
{ Header: 'Registration Time', accessor: 'REGTIME' },
|
||||
{
|
||||
Header: 'Marketplace',
|
||||
accessor: 'MARKETPLACE',
|
||||
disableFilters: false,
|
||||
Filter: ({ column }) =>
|
||||
CategoryFilter({
|
||||
column,
|
||||
multiple: true,
|
||||
title: 'Marketplace',
|
||||
}),
|
||||
filter: 'includesValue',
|
||||
},
|
||||
{ Header: 'Zone ID', accessor: 'ZONE_ID' },
|
||||
import { getState, getType } from 'client/models/MarketplaceApp'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
/** @type {Column[]} Marketplace Apps columns */
|
||||
const COLUMNS = [
|
||||
{ Header: T.ID, id: 'id', accessor: 'ID', sortType: 'number' },
|
||||
{ Header: T.Name, id: 'name', accessor: 'NAME' },
|
||||
{ Header: T.Owner, id: 'owner', accessor: 'UNAME' },
|
||||
{ Header: T.Group, id: 'group', accessor: 'GNAME' },
|
||||
{ Header: T.State, id: 'state', accessor: (row) => getState(row)?.name },
|
||||
{ Header: T.Type, id: 'type', accessor: getType },
|
||||
{ Header: T.Size, id: 'size', accessor: 'SIZE' },
|
||||
{ Header: T.RegistrationTime, id: 'time', accessor: 'REGTIME' },
|
||||
{ Header: T.Marketplace, id: 'marketplace', accessor: 'MARKETPLACE' },
|
||||
{ Header: T.Zone, id: 'zone', accessor: 'ZONE_ID' },
|
||||
]
|
||||
|
||||
COLUMNS.noFilterIds = ['id', 'name', 'time', 'size']
|
||||
|
||||
export default COLUMNS
|
||||
|
@ -14,75 +14,23 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Lock, User, Group, Cart } from 'iconoir-react'
|
||||
import { Typography } from '@mui/material'
|
||||
import api from 'client/features/OneApi/marketplaceApp'
|
||||
import { MarketplaceAppCard } from 'client/components/Cards'
|
||||
|
||||
import { StatusCircle, StatusChip } from 'client/components/Status'
|
||||
import { rowStyles } from 'client/components/Tables/styles'
|
||||
const Row = memo(
|
||||
({ original, value, ...props }) => {
|
||||
const state = api.endpoints.getMarketplaceApps.useQueryState(undefined, {
|
||||
selectFromResult: ({ data = [] }) =>
|
||||
data.find((app) => +app.ID === +original.ID),
|
||||
})
|
||||
|
||||
import * as MarketplaceAppModel from 'client/models/MarketplaceApp'
|
||||
import * as Helper from 'client/models/Helper'
|
||||
import { prettyBytes } from 'client/utils'
|
||||
|
||||
const Row = ({ original, value, ...props }) => {
|
||||
const classes = rowStyles()
|
||||
const {
|
||||
ID,
|
||||
NAME,
|
||||
UNAME,
|
||||
GNAME,
|
||||
LOCK,
|
||||
TYPE,
|
||||
REGTIME,
|
||||
MARKETPLACE,
|
||||
ZONE_ID,
|
||||
SIZE,
|
||||
} = value
|
||||
|
||||
const { color: stateColor, name: stateName } =
|
||||
MarketplaceAppModel.getState(original)
|
||||
|
||||
const time = Helper.timeFromMilliseconds(+REGTIME)
|
||||
const timeAgo = `registered ${time.toRelative()}`
|
||||
|
||||
return (
|
||||
<div {...props} data-cy={`app-${ID}`}>
|
||||
<div className={classes.main}>
|
||||
<div className={classes.title}>
|
||||
<StatusCircle color={stateColor} tooltip={stateName} />
|
||||
<Typography component="span">{NAME}</Typography>
|
||||
{LOCK && <Lock />}
|
||||
<span className={classes.labels}>
|
||||
<StatusChip text={TYPE} />
|
||||
</span>
|
||||
</div>
|
||||
<div className={classes.caption}>
|
||||
<span title={time.toFormat('ff')}>{`#${ID} ${timeAgo}`}</span>
|
||||
<span title={`Owner: ${UNAME}`}>
|
||||
<User />
|
||||
<span>{` ${UNAME}`}</span>
|
||||
</span>
|
||||
<span title={`Group: ${GNAME}`}>
|
||||
<Group />
|
||||
<span>{` ${GNAME}`}</span>
|
||||
</span>
|
||||
<span title={`Marketplace: ${MARKETPLACE}`}>
|
||||
<Cart />
|
||||
<span>{` ${MARKETPLACE}`}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.secondary}>
|
||||
<span className={classes.labels}>
|
||||
<StatusChip text={`Zone ${ZONE_ID}`} />
|
||||
<StatusChip text={prettyBytes(+SIZE, 'MB')} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <MarketplaceAppCard app={state ?? original} rootProps={props} />
|
||||
},
|
||||
(prev, next) => prev.className === next.className
|
||||
)
|
||||
|
||||
Row.propTypes = {
|
||||
original: PropTypes.object,
|
||||
@ -91,4 +39,6 @@ Row.propTypes = {
|
||||
handleClick: PropTypes.func,
|
||||
}
|
||||
|
||||
Row.displayName = 'MarketplaceAppRow'
|
||||
|
||||
export default Row
|
||||
|
@ -136,7 +136,7 @@ const Actions = () => {
|
||||
dialogProps: {
|
||||
title: (rows) => {
|
||||
const isMultiple = rows?.length > 1
|
||||
const { ID, NAME } = rows?.[0]?.original
|
||||
const { ID, NAME } = rows?.[0]?.original ?? {}
|
||||
|
||||
return [
|
||||
Tr(
|
||||
|
@ -13,34 +13,30 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { CategoryFilter } from 'client/components/Tables/Enhanced/Utils'
|
||||
import * as Helper from 'client/models/Helper'
|
||||
import {} from 'client/constants'
|
||||
import { Column } from 'react-table'
|
||||
|
||||
export default [
|
||||
{ Header: 'ID', accessor: 'ID', sortType: 'number' },
|
||||
{ Header: 'Name', accessor: 'NAME' },
|
||||
{ Header: 'Owner', accessor: 'UNAME' },
|
||||
{ Header: 'Group', accessor: 'GNAME' },
|
||||
{ Header: 'Start Time', accessor: 'REGTIME' },
|
||||
{ Header: 'Locked', accessor: 'LOCK' },
|
||||
import { T } from 'client/constants'
|
||||
|
||||
/** @type {Column[]} VM Template columns */
|
||||
const COLUMNS = [
|
||||
{ Header: T.ID, id: 'id', accessor: 'ID', sortType: 'number' },
|
||||
{ Header: T.Name, id: 'name', accessor: 'NAME' },
|
||||
{ Header: T.Owner, id: 'owner', accessor: 'UNAME' },
|
||||
{ Header: T.Group, id: 'group', accessor: 'GNAME' },
|
||||
{ Header: T.RegistrationTime, id: 'time', accessor: 'REGTIME' },
|
||||
{ Header: T.Locked, id: 'locked', accessor: 'LOCK' },
|
||||
{
|
||||
Header: 'Logo',
|
||||
id: 'LOGO',
|
||||
accessor: (row) => row?.TEMPLATE?.LOGO,
|
||||
Header: T.Logo,
|
||||
id: 'logo',
|
||||
accessor: 'TEMPLATE.LOGO',
|
||||
},
|
||||
{
|
||||
Header: 'Virtual Router',
|
||||
id: 'VROUTER',
|
||||
accessor: (row) =>
|
||||
Helper.stringToBoolean(row?.TEMPLATE?.VROUTER) && 'VROUTER',
|
||||
disableFilters: false,
|
||||
Filter: ({ column }) =>
|
||||
CategoryFilter({
|
||||
column,
|
||||
title: 'Virtual Router',
|
||||
}),
|
||||
filter: 'exact',
|
||||
Header: T.VirtualRouter,
|
||||
id: 'vrouter',
|
||||
accessor: 'TEMPLATE.VROUTER',
|
||||
},
|
||||
]
|
||||
|
||||
COLUMNS.noFilterIds = ['id', 'name', 'time', 'logo']
|
||||
|
||||
export default COLUMNS
|
||||
|
@ -13,13 +13,19 @@
|
||||
* 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 vmTemplateApi from 'client/features/OneApi/vmTemplate'
|
||||
|
||||
import vmTemplateApi, {
|
||||
useUpdateTemplateMutation,
|
||||
} from 'client/features/OneApi/vmTemplate'
|
||||
import { VmTemplateCard } from 'client/components/Cards'
|
||||
import { jsonToXml } from 'client/models/Helper'
|
||||
|
||||
const Row = memo(
|
||||
({ original, value, ...props }) => {
|
||||
const [update] = useUpdateTemplateMutation()
|
||||
|
||||
const state = vmTemplateApi.endpoints.getTemplates.useQueryState(
|
||||
undefined,
|
||||
{
|
||||
@ -28,7 +34,27 @@ const Row = memo(
|
||||
}
|
||||
)
|
||||
|
||||
return <VmTemplateCard template={state ?? original} rootProps={props} />
|
||||
const memoTemplate = useMemo(() => state ?? original, [state, original])
|
||||
|
||||
const handleDeleteLabel = useCallback(
|
||||
(label) => {
|
||||
const currentLabels = memoTemplate.TEMPLATE?.LABELS?.split(',')
|
||||
const newLabels = currentLabels.filter((l) => l !== label).join(',')
|
||||
const newUserTemplate = { ...memoTemplate.TEMPLATE, LABELS: newLabels }
|
||||
const templateXml = jsonToXml(newUserTemplate)
|
||||
|
||||
update({ id: original.ID, template: templateXml, replace: 0 })
|
||||
},
|
||||
[memoTemplate.TEMPLATE?.LABELS, update]
|
||||
)
|
||||
|
||||
return (
|
||||
<VmTemplateCard
|
||||
template={memoTemplate}
|
||||
rootProps={props}
|
||||
onDeleteLabel={handleDeleteLabel}
|
||||
/>
|
||||
)
|
||||
},
|
||||
(prev, next) => prev.className === next.className
|
||||
)
|
||||
|
@ -66,7 +66,7 @@ const useTableStyles = makeStyles({
|
||||
})
|
||||
|
||||
const isDisabled = (action) => (rows) =>
|
||||
isAvailableAction(action)(rows, ({ values }) => values?.STATE)
|
||||
isAvailableAction(action)(rows, ({ values }) => values?.state)
|
||||
|
||||
const ListVmNames = ({ rows = [] }) => {
|
||||
const { data: datastores = [] } = useGetDatastoresQuery()
|
||||
@ -147,7 +147,6 @@ const Actions = () => {
|
||||
|
||||
return (
|
||||
<VmTemplatesTable
|
||||
onlyGlobalSearch
|
||||
disableGlobalSort
|
||||
disableRowSelect
|
||||
classes={classes}
|
||||
|
@ -13,31 +13,43 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import * as VirtualMachineModel from 'client/models/VirtualMachine'
|
||||
import { Column } from 'react-table'
|
||||
|
||||
export default [
|
||||
{ Header: 'ID', accessor: 'ID', sortType: 'number' },
|
||||
{ Header: 'Name', accessor: 'NAME' },
|
||||
import {
|
||||
getState,
|
||||
getIps,
|
||||
getType,
|
||||
getLastHistory,
|
||||
} from 'client/models/VirtualMachine'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
/** @type {Column[]} VM columns */
|
||||
const COLUMNS = [
|
||||
{ Header: T.ID, id: 'id', accessor: 'ID', sortType: 'number' },
|
||||
{ Header: T.Name, id: 'name', accessor: 'NAME' },
|
||||
{
|
||||
Header: 'State',
|
||||
id: 'STATE',
|
||||
accessor: (row) => VirtualMachineModel.getState(row)?.name,
|
||||
Header: T.State,
|
||||
id: 'state',
|
||||
accessor: (row) => getState(row)?.name,
|
||||
},
|
||||
{ Header: 'Owner', accessor: 'UNAME' },
|
||||
{ Header: 'Group', accessor: 'GNAME' },
|
||||
{ Header: 'Start Time', accessor: 'STIME' },
|
||||
{ Header: 'End Time', accessor: 'ETIME' },
|
||||
{ Header: 'Locked', accessor: 'LOCK' },
|
||||
{ Header: T.Owner, id: 'owner', accessor: 'UNAME' },
|
||||
{ Header: T.Group, id: 'group', accessor: 'GNAME' },
|
||||
{ Header: T.StartTime, id: 'time', accessor: 'STIME' },
|
||||
{ Header: T.Locked, id: 'locked', accessor: 'LOCK' },
|
||||
{ Header: T.Type, id: 'type', accessor: getType },
|
||||
{
|
||||
Header: 'Ips',
|
||||
id: 'IPS',
|
||||
accessor: (row) => VirtualMachineModel.getIps(row).join(','),
|
||||
Header: T.IP,
|
||||
id: 'ips',
|
||||
accessor: (row) => getIps(row).join(),
|
||||
sortType: 'length',
|
||||
},
|
||||
{
|
||||
Header: 'Hostname',
|
||||
id: 'HOSTNAME',
|
||||
accessor: (row) => VirtualMachineModel.getLastHistory(row)?.HOSTNAME,
|
||||
Header: T.Hostname,
|
||||
id: 'hostname',
|
||||
accessor: (row) => getLastHistory(row)?.HOSTNAME,
|
||||
},
|
||||
]
|
||||
|
||||
COLUMNS.noFilterIds = ['id', 'name', 'ips', 'time']
|
||||
|
||||
export default COLUMNS
|
||||
|
@ -13,19 +13,22 @@
|
||||
* 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 vmApi from 'client/features/OneApi/vm'
|
||||
import vmApi, { useUpdateUserTemplateMutation } from 'client/features/OneApi/vm'
|
||||
import { VirtualMachineCard } from 'client/components/Cards'
|
||||
import { ConsoleButton } from 'client/components/Buttons'
|
||||
import { jsonToXml } from 'client/models/Helper'
|
||||
import { VM_ACTIONS } from 'client/constants'
|
||||
|
||||
const { VNC, RDP, SSH, VMRC } = VM_ACTIONS
|
||||
const CONNECTION_TYPES = [VNC, RDP, SSH, VMRC]
|
||||
|
||||
const Row = memo(
|
||||
({ original, ...props }) => {
|
||||
({ original, value, ...props }) => {
|
||||
const [update] = useUpdateUserTemplateMutation()
|
||||
|
||||
const state = vmApi.endpoints.getVms.useQueryState(undefined, {
|
||||
selectFromResult: ({ data = [] }) =>
|
||||
data.find((vm) => +vm.ID === +original.ID),
|
||||
@ -33,10 +36,23 @@ const Row = memo(
|
||||
|
||||
const memoVm = useMemo(() => state ?? original, [state, original])
|
||||
|
||||
const handleDeleteLabel = useCallback(
|
||||
(label) => {
|
||||
const currentLabels = memoVm.USER_TEMPLATE?.LABELS?.split(',')
|
||||
const newLabels = currentLabels.filter((l) => l !== label).join(',')
|
||||
const newUserTemplate = { ...memoVm.USER_TEMPLATE, LABELS: newLabels }
|
||||
const templateXml = jsonToXml(newUserTemplate)
|
||||
|
||||
update({ id: original.ID, template: templateXml, replace: 0 })
|
||||
},
|
||||
[memoVm.USER_TEMPLATE?.LABELS, update]
|
||||
)
|
||||
|
||||
return (
|
||||
<VirtualMachineCard
|
||||
vm={memoVm}
|
||||
rootProps={props}
|
||||
onDeleteLabel={handleDeleteLabel}
|
||||
actions={
|
||||
<>
|
||||
{CONNECTION_TYPES.map((connectionType) => (
|
||||
|
@ -32,18 +32,13 @@ export const rowStyles = makeStyles(
|
||||
},
|
||||
},
|
||||
figure: {
|
||||
flexBasis: '20%',
|
||||
paddingTop: '12.5%',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
flexBasis: '10%',
|
||||
aspectRatio: '16/9',
|
||||
},
|
||||
image: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
position: 'absolute',
|
||||
userSelect: 'none',
|
||||
},
|
||||
main: {
|
||||
@ -68,18 +63,18 @@ export const rowStyles = makeStyles(
|
||||
color: palette.text.secondary,
|
||||
marginTop: 4,
|
||||
display: 'flex',
|
||||
gap: '0.75em',
|
||||
gap: '0.5em',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
wordWrap: 'break-word',
|
||||
'& > .full-width': {
|
||||
flexBasis: '100%',
|
||||
},
|
||||
},
|
||||
captionItem: {
|
||||
display: 'flex',
|
||||
gap: '0.5em',
|
||||
alignItems: 'center',
|
||||
'& > span': {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5em',
|
||||
},
|
||||
},
|
||||
secondary: {
|
||||
width: '25%',
|
||||
|
@ -80,8 +80,8 @@ const ClusterInfoTab = ({ tabProps = {}, id }) => {
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(480px, 1fr))"
|
||||
padding="0.8em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
|
||||
padding={{ sm: '0.8em' }}
|
||||
>
|
||||
{informationPanel?.enabled && (
|
||||
<Information
|
||||
|
@ -52,7 +52,7 @@ const ClusterTabs = memo(({ id }) => {
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
<Tabs tabs={tabsAvailable ?? []} />
|
||||
<Tabs addBorder tabs={tabsAvailable ?? []} />
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -102,8 +102,8 @@ const DatastoreInfoTab = ({ tabProps = {}, id }) => {
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(480px, 1fr))"
|
||||
padding="0.8em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
|
||||
padding={{ sm: '0.8em' }}
|
||||
>
|
||||
{informationPanel?.enabled && (
|
||||
<Information
|
||||
|
@ -52,7 +52,7 @@ const DatastoreTabs = memo(({ id }) => {
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
<Tabs tabs={tabsAvailable ?? []} />
|
||||
<Tabs addBorder tabs={tabsAvailable ?? []} />
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -50,7 +50,7 @@ const GroupInfoTab = ({ tabProps = {}, id }) => {
|
||||
} = tabProps
|
||||
|
||||
const [update] = useUpdateGroupMutation()
|
||||
const { data: group } = useGetGroupQuery(id)
|
||||
const { data: group } = useGetGroupQuery({ id })
|
||||
const { TEMPLATE } = group
|
||||
|
||||
const handleAttributeInXml = async (path, newValue) => {
|
||||
@ -80,8 +80,8 @@ const GroupInfoTab = ({ tabProps = {}, id }) => {
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(480px, 1fr))"
|
||||
padding="0.8em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
|
||||
padding={{ sm: '0.8em' }}
|
||||
>
|
||||
{informationPanel?.enabled && (
|
||||
<Information
|
||||
|
@ -32,7 +32,7 @@ const getTabComponent = (tabName) =>
|
||||
|
||||
const GroupTabs = memo(({ id }) => {
|
||||
const { view, getResourceView } = useViews()
|
||||
const { isLoading, isError, error } = useGetGroupQuery(id)
|
||||
const { isLoading, isError, error } = useGetGroupQuery({ id })
|
||||
|
||||
const tabsAvailable = useMemo(() => {
|
||||
const resource = RESOURCE_NAMES.GROUP
|
||||
@ -52,7 +52,7 @@ const GroupTabs = memo(({ id }) => {
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
<Tabs tabs={tabsAvailable ?? []} />
|
||||
<Tabs addBorder tabs={tabsAvailable ?? []} />
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -93,8 +93,8 @@ const HostInfoTab = ({ tabProps = {}, id }) => {
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(480px, 1fr))"
|
||||
padding="0.8em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
|
||||
padding={{ sm: '0.8em' }}
|
||||
>
|
||||
{informationPanel?.enabled && (
|
||||
<Information
|
||||
|
@ -21,6 +21,7 @@ import { Box, List, ListItem, Paper, Typography } from '@mui/material'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { useStyles } from 'client/components/Tabs/Host/Numa/Hugepage/styles'
|
||||
|
||||
import { prettyBytes } from 'client/utils'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
/**
|
||||
@ -55,7 +56,7 @@ const NumaHugepage = ({ hugepage }) => {
|
||||
{hugepage.length > 0 &&
|
||||
hugepage.map(({ FREE, PAGES, SIZE, USAGE }, index) => (
|
||||
<ListItem key={index} className={classes.item} dense>
|
||||
<Typography noWrap>{SIZE}</Typography>
|
||||
<Typography noWrap>{prettyBytes(SIZE)}</Typography>
|
||||
<Typography noWrap>{FREE}</Typography>
|
||||
<Typography noWrap>{PAGES}</Typography>
|
||||
<Typography noWrap>{USAGE}</Typography>
|
||||
|
@ -13,24 +13,20 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
import { ReactElement, useEffect } from 'react'
|
||||
import { FormProvider, useForm } from 'react-hook-form'
|
||||
import { ReactElement, useEffect, useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { debounce } from '@mui/material'
|
||||
import { FormProvider, useForm } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import { useUpdateHostMutation } from 'client/features/OneApi/host'
|
||||
|
||||
import {
|
||||
FORM_FIELDS_PIN_POLICY,
|
||||
FORM_SCHEMA_PIN_POLICY,
|
||||
} from 'client/components/Tabs/Host/Numa/UpdatePinPolicy/schema'
|
||||
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
|
||||
import { useUpdateHostMutation } from 'client/features/OneApi/host'
|
||||
|
||||
import { FormWithSchema } from 'client/components/Forms'
|
||||
import { T, Host } from 'client/constants'
|
||||
/**
|
||||
* @param {object} props - Props
|
||||
@ -43,26 +39,31 @@ const UpdatePinPolicyForm = ({ host }) => {
|
||||
const { enqueueError } = useGeneralApi()
|
||||
const [updateUserTemplate] = useUpdateHostMutation()
|
||||
|
||||
const { watch, ...methods } = useForm({
|
||||
const { watch, handleSubmit, ...methods } = useForm({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
PIN_POLICY: TEMPLATE.PIN_POLICY,
|
||||
},
|
||||
defaultValues: { PIN_POLICY: TEMPLATE.PIN_POLICY },
|
||||
resolver: yupResolver(FORM_SCHEMA_PIN_POLICY),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
watch((data) => {
|
||||
const handleUpdatePinPolicy = useCallback(
|
||||
debounce(async ({ PIN_POLICY } = {}) => {
|
||||
try {
|
||||
updateUserTemplate({
|
||||
await updateUserTemplate({
|
||||
id: host.ID,
|
||||
template: `PIN_POLICY = ${data.PIN_POLICY}`,
|
||||
template: `PIN_POLICY = ${PIN_POLICY}`,
|
||||
replace: 1,
|
||||
})
|
||||
} catch {
|
||||
enqueueError(T.SomethingWrong)
|
||||
}
|
||||
})
|
||||
}, 500),
|
||||
[updateUserTemplate]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch(() => handleSubmit(handleUpdatePinPolicy)())
|
||||
|
||||
return () => subscription.unsubscribe()
|
||||
}, [watch])
|
||||
|
||||
return (
|
||||
|
@ -37,8 +37,8 @@ const InformationPanel = ({ node = {} }) => {
|
||||
<>
|
||||
<Stack
|
||||
gap="1em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(480px, 1fr))"
|
||||
padding="0.8em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
|
||||
padding={{ sm: '0.8em' }}
|
||||
>
|
||||
<Typography gutterBottom variant="h2" component="h2">
|
||||
<Translate word={T.NumaNodeItem} values={node.NODE_ID} />
|
||||
@ -56,8 +56,8 @@ const InformationPanel = ({ node = {} }) => {
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(480px, 1fr))"
|
||||
padding="0.8em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
|
||||
padding={{ sm: '0.8em' }}
|
||||
>
|
||||
<NumaHugepage hugepage={[HUGEPAGE].flat()} />
|
||||
<NumaMemory node={node} />
|
||||
|
@ -37,8 +37,8 @@ const WildsInfoTab = ({ id }) => {
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(480px, 1fr))"
|
||||
padding="0.8em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
|
||||
padding={{ sm: '0.8em' }}
|
||||
>
|
||||
<WildsTable wilds={wilds} />
|
||||
</Stack>
|
||||
|
@ -37,8 +37,8 @@ const ZombiesInfoTab = ({ id }) => {
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(480px, 1fr))"
|
||||
padding="0.8em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
|
||||
padding={{ sm: '0.8em' }}
|
||||
>
|
||||
<ZombiesTable disableRowSelect disableGlobalSort zombies={zombies} />
|
||||
</Stack>
|
||||
|
@ -60,7 +60,7 @@ const HostTabs = memo(({ id }) => {
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
<Tabs tabs={tabsAvailable} />
|
||||
<Tabs addBorder tabs={tabsAvailable} />
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -89,8 +89,8 @@ const ImageInfoTab = ({ tabProps = {}, id }) => {
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(480px, 1fr))"
|
||||
padding="0.8em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
|
||||
padding={{ sm: '0.8em' }}
|
||||
>
|
||||
{informationPanel?.enabled && (
|
||||
<Information
|
||||
|
@ -52,7 +52,7 @@ const ImageTabs = memo(({ id }) => {
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
<Tabs tabs={tabsAvailable ?? []} />
|
||||
<Tabs addBorder tabs={tabsAvailable ?? []} />
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -89,8 +89,8 @@ const MarketplaceInfoTab = ({ tabProps = {}, id }) => {
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(480px, 1fr))"
|
||||
padding="0.8em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
|
||||
padding={{ sm: '0.8em' }}
|
||||
>
|
||||
{informationPanel?.enabled && (
|
||||
<Information
|
||||
|
@ -52,7 +52,7 @@ const MarketplaceTabs = memo(({ id }) => {
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
<Tabs tabs={tabsAvailable ?? []} />
|
||||
<Tabs addBorder tabs={tabsAvailable ?? []} />
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -92,8 +92,8 @@ const MarketplaceAppInfoTab = ({ tabProps = {}, id }) => {
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(480px, 1fr))"
|
||||
padding="0.8em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
|
||||
padding={{ sm: '0.8em' }}
|
||||
>
|
||||
{informationPanel?.enabled && (
|
||||
<Information
|
||||
|
@ -54,7 +54,7 @@ const MarketplaceAppTabs = memo(({ id }) => {
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
<Tabs tabs={tabsAvailable ?? []} />
|
||||
<Tabs addBorder tabs={tabsAvailable ?? []} />
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -47,7 +47,7 @@ const UserInfoTab = ({ tabProps = {}, id }) => {
|
||||
} = tabProps
|
||||
|
||||
const [updateUser] = useUpdateUserMutation()
|
||||
const { data: user = {} } = useGetUserQuery(id)
|
||||
const { data: user = {} } = useGetUserQuery({ id })
|
||||
const { TEMPLATE } = user
|
||||
|
||||
const handleAttributeInXml = async (path, newValue) => {
|
||||
@ -74,8 +74,8 @@ const UserInfoTab = ({ tabProps = {}, id }) => {
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(480px, 1fr))"
|
||||
padding="0.8em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
|
||||
padding={{ sm: '0.8em' }}
|
||||
>
|
||||
{informationPanel?.enabled && (
|
||||
<Information
|
||||
|
@ -32,7 +32,7 @@ const getTabComponent = (tabName) =>
|
||||
|
||||
const UserTabs = memo(({ id }) => {
|
||||
const { view, getResourceView } = useViews()
|
||||
const { isLoading, isError, error } = useGetUserQuery(id)
|
||||
const { isLoading, isError, error } = useGetUserQuery({ id })
|
||||
|
||||
const tabsAvailable = useMemo(() => {
|
||||
const resource = RESOURCE_NAMES.USER
|
||||
@ -52,7 +52,7 @@ const UserTabs = memo(({ id }) => {
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
<Tabs tabs={tabsAvailable ?? []} />
|
||||
<Tabs addBorder tabs={tabsAvailable ?? []} />
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -112,8 +112,8 @@ const VNetworkInfoTab = ({ tabProps = {}, id }) => {
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(480px, 1fr))"
|
||||
padding="0.8em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
|
||||
padding={{ sm: '0.8em' }}
|
||||
>
|
||||
{informationPanel?.enabled && (
|
||||
<Information
|
||||
|
@ -52,7 +52,7 @@ const VNetworkTabs = memo(({ id }) => {
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
<Tabs tabs={tabsAvailable ?? []} />
|
||||
<Tabs addBorder tabs={tabsAvailable ?? []} />
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -112,8 +112,8 @@ const VNetTemplateInfoTab = ({ tabProps = {}, id }) => {
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(480px, 1fr))"
|
||||
padding="0.8em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
|
||||
padding={{ sm: '0.8em' }}
|
||||
>
|
||||
{informationPanel?.enabled && (
|
||||
<Information
|
||||
|
@ -52,7 +52,7 @@ const VNetTemplateTabs = memo(({ id }) => {
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
<Tabs tabs={tabsAvailable ?? []} />
|
||||
<Tabs addBorder tabs={tabsAvailable ?? []} />
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -13,91 +13,132 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement } from 'react'
|
||||
import { ReactElement, memo, useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Folder, User } from 'iconoir-react'
|
||||
import { Typography, Paper } from '@mui/material'
|
||||
import { Folder, User, Group, InfoEmpty } from 'iconoir-react'
|
||||
import { Typography, Paper, Stack, Divider } from '@mui/material'
|
||||
|
||||
import { useGetDatastoresQuery } from 'client/features/OneApi/datastore'
|
||||
import { useGetUsersQuery } from 'client/features/OneApi/user'
|
||||
import { useGetGroupsQuery } from 'client/features/OneApi/group'
|
||||
import { Tr, Translate } from 'client/components/HOC'
|
||||
import { rowStyles } from 'client/components/Tables/styles'
|
||||
import { getHistoryAction } from 'client/models/VirtualMachine'
|
||||
import { timeFromMilliseconds, timeDiff } from 'client/models/Helper'
|
||||
import { HistoryRecord } from 'client/constants'
|
||||
import { T, HistoryRecord } from 'client/constants'
|
||||
|
||||
/**
|
||||
* Renders history record card.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {HistoryRecord} props.history - History
|
||||
* @returns {ReactElement} History record card component
|
||||
*/
|
||||
const HistoryRecordCard = ({ history }) => {
|
||||
const classes = rowStyles()
|
||||
const HistoryRecordCard = memo(
|
||||
/**
|
||||
* Renders history record card.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {HistoryRecord} props.history - History
|
||||
* @returns {ReactElement} History record card component
|
||||
*/
|
||||
({ history }) => {
|
||||
const classes = rowStyles()
|
||||
|
||||
const {
|
||||
SEQ,
|
||||
UID,
|
||||
GID,
|
||||
REQUEST_ID,
|
||||
HOSTNAME,
|
||||
HID,
|
||||
DS_ID,
|
||||
ACTION,
|
||||
STIME,
|
||||
ETIME,
|
||||
PSTIME,
|
||||
PETIME,
|
||||
} = history
|
||||
const {
|
||||
SEQ,
|
||||
UID,
|
||||
GID,
|
||||
REQUEST_ID,
|
||||
HOSTNAME,
|
||||
HID,
|
||||
DS_ID,
|
||||
ACTION,
|
||||
STIME,
|
||||
ETIME,
|
||||
PSTIME,
|
||||
PETIME,
|
||||
} = history
|
||||
|
||||
const now = Math.round(Date.now() / 1000)
|
||||
const now = Math.round(Date.now() / 1000)
|
||||
const startTime = timeFromMilliseconds(+STIME)
|
||||
|
||||
const startTime = timeFromMilliseconds(+STIME)
|
||||
const monitorEndTime = +ETIME === 0 ? now : +ETIME
|
||||
const monitorDiffTime = timeDiff(+STIME, monitorEndTime)
|
||||
|
||||
const monitorEndTime = +ETIME === 0 ? now : +ETIME
|
||||
const monitorDiffTime = timeDiff(+STIME, monitorEndTime)
|
||||
const prologEndTime = +PSTIME === 0 ? 0 : +PETIME === 0 ? now : +PETIME
|
||||
const prologDiffTime = timeDiff(+PSTIME, prologEndTime)
|
||||
|
||||
const prologEndTime = +PSTIME === 0 ? 0 : +PETIME === 0 ? now : +PETIME
|
||||
const prologDiffTime = timeDiff(+PSTIME, prologEndTime)
|
||||
const action = getHistoryAction(+ACTION)
|
||||
|
||||
const ownerInfo = `${UID} | ${GID} | ${REQUEST_ID}`
|
||||
const getNameFromResult = useCallback(
|
||||
(id, result) => ({
|
||||
name: result?.find((item) => +item.ID === +id)?.NAME ?? '...',
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const action = getHistoryAction(+ACTION)
|
||||
const { name: dsName } = useGetDatastoresQuery(undefined, {
|
||||
selectFromResult: ({ data }) => getNameFromResult(DS_ID, data),
|
||||
})
|
||||
|
||||
return (
|
||||
<Paper variant="outlined" className={classes.root}>
|
||||
<div className={classes.main}>
|
||||
<div className={classes.title}>
|
||||
<Typography component="span">
|
||||
{`#${SEQ} | #${HID} ${HOSTNAME} | Action: ${action}`}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.caption}>
|
||||
<span title={`Datastore ID: ${DS_ID}`}>
|
||||
<Folder />
|
||||
<span>{` ${DS_ID}`}</span>
|
||||
</span>
|
||||
{+UID !== -1 && (
|
||||
<span title={`Owner | Group | Request ID: ${ownerInfo}`}>
|
||||
<User />
|
||||
<span>{` ${ownerInfo}`}</span>
|
||||
const { name: userName } = useGetUsersQuery(undefined, {
|
||||
selectFromResult: ({ data }) => getNameFromResult(UID, data),
|
||||
})
|
||||
|
||||
const { name: groupName } = useGetGroupsQuery(undefined, {
|
||||
selectFromResult: ({ data }) => getNameFromResult(GID, data),
|
||||
})
|
||||
|
||||
return (
|
||||
<Paper variant="outlined" className={classes.root}>
|
||||
<div className={classes.main}>
|
||||
<div className={classes.title}>
|
||||
<Typography component="span">
|
||||
{`#${SEQ} | #${HID} ${HOSTNAME} | ${Tr(T.Action)}: ${action}`}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.caption}>
|
||||
<span title={`${Tr(T.Datastore)}: #${DS_ID} ${dsName}`}>
|
||||
<Folder />
|
||||
<span data-cy="datastore">{`#${DS_ID} ${dsName}`}</span>
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
title={`Time when the state changed: ${startTime.toFormat('ff')}`}
|
||||
>
|
||||
{`| start ${startTime.toRelative()}`}
|
||||
</span>
|
||||
<span title={'Total time in this state'}>
|
||||
{`| total ${monitorDiffTime}`}
|
||||
</span>
|
||||
<span title={'Prolog time for this state'}>
|
||||
{`| prolog ${prologDiffTime}`}
|
||||
</span>
|
||||
{+UID !== -1 && (
|
||||
<>
|
||||
<span title={`${Tr(T.Owner)}: #${UID} ${userName}`}>
|
||||
<User />
|
||||
<span data-cy="owner">{`#${UID} ${userName}`}</span>
|
||||
</span>
|
||||
<span title={`${Tr(T.Group)}: #${GID} ${groupName}`}>
|
||||
<Group />
|
||||
<span data-cy="group">{`#${GID} ${groupName}`}</span>
|
||||
</span>
|
||||
<span title={`${Tr(T.RequestId)}: ${REQUEST_ID}`}>
|
||||
<InfoEmpty />
|
||||
<span data-cy="request">{`#${REQUEST_ID}`}</span>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<Stack
|
||||
direction="row"
|
||||
component="span"
|
||||
divider={<Divider orientation="vertical" flexItem />}
|
||||
>
|
||||
<span title={Tr(T.TimeWhenTheStateChanged)}>
|
||||
<Translate
|
||||
word={T.StartedOnTime}
|
||||
values={[startTime.toFormat('ff')]}
|
||||
/>
|
||||
</span>
|
||||
<span title={Tr(T.TotalTimeInThisState)}>
|
||||
<Translate word={T.Total} />
|
||||
{` ${monitorDiffTime}`}
|
||||
</span>
|
||||
<span title={Tr(T.PrologTimeForThisState)}>
|
||||
<Translate word={T.Prolog} />
|
||||
{` ${prologDiffTime}`}
|
||||
</span>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
HistoryRecordCard.propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
|
@ -132,8 +132,8 @@ const VmInfoTab = ({ tabProps = {}, id }) => {
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(380px, 1fr))"
|
||||
padding="0.8em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
|
||||
padding={{ sm: '0.8em' }}
|
||||
>
|
||||
<Fade in={!!error} unmountOnExit>
|
||||
<Alert
|
||||
|
@ -13,24 +13,31 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, useMemo } from 'react'
|
||||
import { ReactElement } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useHistory, generatePath } from 'react-router-dom'
|
||||
import { Card, CardActionArea, CardMedia } from '@mui/material'
|
||||
import { generatePath } from 'react-router-dom'
|
||||
|
||||
import { useGetClusterQuery } from 'client/features/OneApi/cluster'
|
||||
import { useRenameVmMutation } from 'client/features/OneApi/vm'
|
||||
import { useGuacamole } from 'client/features/Guacamole'
|
||||
|
||||
import { StatusChip } from 'client/components/Status'
|
||||
import { List } from 'client/components/Tabs/Common'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import MultipleTags from 'client/components/MultipleTags'
|
||||
|
||||
import { getState, getLastHistory, getIps } from 'client/models/VirtualMachine'
|
||||
import * as Helper from 'client/models/Helper'
|
||||
import {
|
||||
getState,
|
||||
getLastHistory,
|
||||
getIps,
|
||||
getNicWithPortForwarding,
|
||||
} from 'client/models/VirtualMachine'
|
||||
import {
|
||||
booleanToString,
|
||||
levelLockToString,
|
||||
timeToString,
|
||||
} from 'client/models/Helper'
|
||||
import { T, VM, VM_ACTIONS } from 'client/constants'
|
||||
import { PATH } from 'client/apps/sunstone/routesOne'
|
||||
import { PATH as DEFAULT_PATH } from 'client/apps/sunstone/routes'
|
||||
|
||||
/**
|
||||
* Renders mainly information tab.
|
||||
@ -41,21 +48,15 @@ import { PATH as DEFAULT_PATH } from 'client/apps/sunstone/routes'
|
||||
* @returns {ReactElement} Information tab
|
||||
*/
|
||||
const InformationPanel = ({ vm = {}, actions }) => {
|
||||
const history = useHistory()
|
||||
const [renameVm] = useRenameVmMutation()
|
||||
const sessions = useGuacamole(vm?.ID)
|
||||
|
||||
const [connectionType, { thumbnail: firstThumbnail } = {}] = useMemo(
|
||||
() =>
|
||||
Object.entries(sessions).find(
|
||||
([_, { thumbnail }]) => !!thumbnail?.canvas
|
||||
) ?? [],
|
||||
[sessions]
|
||||
)
|
||||
|
||||
const { ID, NAME, RESCHED, STIME, ETIME, LOCK, DEPLOY_ID } = vm
|
||||
const { name: stateName, color: stateColor } = getState(vm)
|
||||
|
||||
const ips = getIps(vm)
|
||||
const { EXTERNAL_PORT_RANGE, INTERNAL_PORT_RANGE } =
|
||||
getNicWithPortForwarding(vm) ?? {}
|
||||
|
||||
const {
|
||||
HID: hostId,
|
||||
HOSTNAME: hostname = '--',
|
||||
@ -90,27 +91,42 @@ const InformationPanel = ({ vm = {}, actions }) => {
|
||||
},
|
||||
{
|
||||
name: T.Reschedule,
|
||||
value: Helper.booleanToString(+RESCHED),
|
||||
value: booleanToString(+RESCHED),
|
||||
dataCy: 'reschedule',
|
||||
},
|
||||
{
|
||||
name: T.Locked,
|
||||
value: Helper.levelLockToString(LOCK?.LOCKED),
|
||||
value: levelLockToString(LOCK?.LOCKED),
|
||||
dataCy: 'locked',
|
||||
},
|
||||
{
|
||||
name: T.IP,
|
||||
value: ips?.length ? <MultipleTags tags={ips} /> : '--',
|
||||
value: ips?.length ? <MultipleTags tags={ips} clipboard /> : '--',
|
||||
dataCy: 'ips',
|
||||
},
|
||||
EXTERNAL_PORT_RANGE &&
|
||||
INTERNAL_PORT_RANGE && {
|
||||
name: T.PortForwarding,
|
||||
value: (
|
||||
<Translate
|
||||
word={T.HostnamePortsForwardedToVmPorts}
|
||||
values={[
|
||||
hostname,
|
||||
EXTERNAL_PORT_RANGE,
|
||||
INTERNAL_PORT_RANGE?.split('/')[0]?.replace('-', ':'),
|
||||
]}
|
||||
/>
|
||||
),
|
||||
dataCy: 'port_forwarding',
|
||||
},
|
||||
{
|
||||
name: T.StartTime,
|
||||
value: Helper.timeToString(STIME),
|
||||
value: timeToString(STIME),
|
||||
dataCy: 'starttime',
|
||||
},
|
||||
{
|
||||
name: T.EndTime,
|
||||
value: Helper.timeToString(ETIME),
|
||||
value: timeToString(ETIME),
|
||||
dataCy: 'endtime',
|
||||
},
|
||||
hostId && {
|
||||
@ -134,32 +150,6 @@ const InformationPanel = ({ vm = {}, actions }) => {
|
||||
value: DEPLOY_ID,
|
||||
dataCy: 'deployid',
|
||||
},
|
||||
firstThumbnail && {
|
||||
name: T.LastConnection,
|
||||
value: (
|
||||
<Card sx={{ my: 1 }} data-cy={`${vm.ID}-${connectionType}-thumbnail`}>
|
||||
<CardActionArea
|
||||
disableTouchRipple={false}
|
||||
onClick={() =>
|
||||
history.push(
|
||||
generatePath(DEFAULT_PATH.GUACAMOLE, {
|
||||
id: vm.ID,
|
||||
type: connectionType,
|
||||
})
|
||||
)
|
||||
}
|
||||
>
|
||||
<CardMedia
|
||||
component="img"
|
||||
sx={{ bgcolor: 'text.primary', opacity: 0.8 }}
|
||||
src={firstThumbnail?.canvas}
|
||||
alt={`thumbnail-${vm.ID}-${connectionType}`}
|
||||
/>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
),
|
||||
dataCy: 'last_connection',
|
||||
},
|
||||
].filter(Boolean)
|
||||
|
||||
return (
|
||||
|
@ -68,7 +68,7 @@ const VmTabs = memo(({ id }) => {
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
<Tabs tabs={tabsAvailable} />
|
||||
<Tabs addBorder tabs={tabsAvailable} />
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -60,8 +60,8 @@ const VmTemplateInfoTab = ({ tabProps = {}, id }) => {
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(480px, 1fr))"
|
||||
padding="0.8em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
|
||||
padding={{ sm: '0.8em' }}
|
||||
>
|
||||
{informationPanel?.enabled && (
|
||||
<Information
|
||||
|
@ -54,7 +54,7 @@ const VmTemplateTabs = memo(({ id }) => {
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
<Tabs tabs={tabsAvailable ?? []} />
|
||||
<Tabs addBorder tabs={tabsAvailable ?? []} />
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -70,8 +70,8 @@ const ZoneInfoTab = ({ tabProps = {}, id }) => {
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(480px, 1fr))"
|
||||
padding="0.8em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
|
||||
padding={{ sm: '0.8em' }}
|
||||
>
|
||||
{informationPanel?.enabled && (
|
||||
<Information
|
||||
|
@ -52,7 +52,7 @@ const ZoneTabs = memo(({ id }) => {
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
<Tabs tabs={tabsAvailable ?? []} />
|
||||
<Tabs addBorder tabs={tabsAvailable ?? []} />
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { useState, useMemo, JSXElementConstructor } from 'react'
|
||||
import { useState, useMemo, ReactElement } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {
|
||||
@ -21,7 +21,6 @@ import {
|
||||
Tabs as MTabs,
|
||||
TabsProps,
|
||||
Tab as MTab,
|
||||
Box,
|
||||
Fade,
|
||||
} from '@mui/material'
|
||||
import { WarningCircledOutline } from 'iconoir-react'
|
||||
@ -30,33 +29,57 @@ const WarningIcon = styled(WarningCircledOutline)(({ theme }) => ({
|
||||
color: theme.palette.error.main,
|
||||
}))
|
||||
|
||||
const Content = ({ name, renderContent: RenderContent, hidden }) => (
|
||||
<Fade in timeout={400} key={`tab-${name}`}>
|
||||
<Box
|
||||
sx={{
|
||||
p: (theme) => theme.spacing(2, 1),
|
||||
height: '100%',
|
||||
display: hidden ? 'none' : 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{typeof RenderContent === 'function' ? <RenderContent /> : RenderContent}
|
||||
</Box>
|
||||
</Fade>
|
||||
const TabContent = styled('div')(({ hidden, addBorder, theme }) => ({
|
||||
height: '100%',
|
||||
display: hidden ? 'none' : 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'auto',
|
||||
...(addBorder && {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
border: `thin solid ${theme.palette.secondary.main}`,
|
||||
borderTop: 'none',
|
||||
borderRadius: `0 0 8px 8px`,
|
||||
}),
|
||||
}))
|
||||
|
||||
const Content = ({
|
||||
id,
|
||||
name,
|
||||
renderContent: RenderContent,
|
||||
hidden,
|
||||
addBorder = false,
|
||||
}) => (
|
||||
<TabContent
|
||||
key={`tab-${id ?? name}`}
|
||||
data-cy={`tab-content-${id ?? name}`}
|
||||
hidden={hidden}
|
||||
addBorder={addBorder}
|
||||
>
|
||||
<Fade in timeout={400}>
|
||||
<TabContent sx={{ p: '1em .5em' }}>
|
||||
{typeof RenderContent === 'function' ? (
|
||||
<RenderContent />
|
||||
) : (
|
||||
RenderContent
|
||||
)}
|
||||
</TabContent>
|
||||
</Fade>
|
||||
</TabContent>
|
||||
)
|
||||
|
||||
/**
|
||||
* @param {object} props - Props
|
||||
* @param {Array} props.tabs - Tabs
|
||||
* @param {TabsProps} props.tabsProps - Props to tabs component
|
||||
* @param {boolean} props.renderHiddenTabs - If `true`, will be render hidden tabs
|
||||
* @returns {JSXElementConstructor} Tabs component with content
|
||||
* @param {boolean} [props.renderHiddenTabs] - If `true`, will be render hidden tabs
|
||||
* @param {boolean} [props.addBorder] - If `true`, will be add a border to tab content
|
||||
* @returns {ReactElement} Tabs component with content
|
||||
*/
|
||||
const Tabs = ({
|
||||
tabs = [],
|
||||
tabsProps: { sx, ...tabsProps } = {},
|
||||
renderHiddenTabs = false,
|
||||
addBorder = false,
|
||||
}) => {
|
||||
const [tabSelected, setTab] = useState(() => 0)
|
||||
|
||||
@ -70,7 +93,7 @@ const Tabs = ({
|
||||
sx={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: (theme) => theme.zIndex.appBar,
|
||||
zIndex: ({ zIndex }) => zIndex.appBar,
|
||||
...sx,
|
||||
}}
|
||||
{...tabsProps}
|
||||
@ -110,6 +133,7 @@ const Tabs = ({
|
||||
renderAllHiddenTabContents
|
||||
) : (
|
||||
<Content
|
||||
addBorder={addBorder}
|
||||
{...tabs.find(({ value }, idx) => (value ?? idx) === tabSelected)}
|
||||
/>
|
||||
)}
|
||||
@ -124,12 +148,15 @@ Tabs.propTypes = {
|
||||
tabs: PropTypes.array,
|
||||
tabsProps: PropTypes.object,
|
||||
renderHiddenTabs: PropTypes.bool,
|
||||
addBorder: PropTypes.bool,
|
||||
}
|
||||
|
||||
Content.propTypes = {
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
name: PropTypes.string,
|
||||
renderContent: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
|
||||
hidden: PropTypes.bool,
|
||||
addBorder: PropTypes.bool,
|
||||
}
|
||||
|
||||
export default Tabs
|
||||
|
@ -24,6 +24,9 @@ export const ADD_ATTRIBUTE = 'add'
|
||||
export const EDIT_ATTRIBUTE = 'edit'
|
||||
export const DELETE_ATTRIBUTE = 'delete'
|
||||
|
||||
// LABELS
|
||||
export const EDIT_LABELS = 'edit_labels'
|
||||
|
||||
// PERMISSION
|
||||
export const CHANGE_MODE = 'chmod'
|
||||
|
||||
|
@ -19,6 +19,7 @@ module.exports = {
|
||||
Previous: 'Previous',
|
||||
Next: 'Next',
|
||||
SortBy: 'Sort by',
|
||||
FilterBy: 'Filter by',
|
||||
Filter: 'Filter',
|
||||
Filters: 'Filters',
|
||||
All: 'All',
|
||||
@ -201,6 +202,9 @@ module.exports = {
|
||||
HoursBetween0_168: 'Hours should be between 0 and 168',
|
||||
WhenYouWantThatTheActionFinishes: 'When you want that the action finishes',
|
||||
|
||||
/* dashboard */
|
||||
MadeWith: 'Made with',
|
||||
|
||||
/* dashboard */
|
||||
InTotal: 'In Total',
|
||||
Used: 'Used',
|
||||
@ -272,7 +276,9 @@ module.exports = {
|
||||
ConfigurationUI: 'Configuration UI',
|
||||
Authentication: 'Authentication',
|
||||
SshPrivateKey: 'SSH private key',
|
||||
AddUserSshPrivateKey: 'Add user SSH private key',
|
||||
SshPassphraseKey: 'SSH private key passphrase',
|
||||
AddUserSshPassphraseKey: 'Add user SSH private key passphrase',
|
||||
|
||||
/* sections - system */
|
||||
User: 'User',
|
||||
@ -361,7 +367,10 @@ module.exports = {
|
||||
Description: 'Description',
|
||||
RegistrationTime: 'Registration time',
|
||||
StartTime: 'Start time',
|
||||
Started: 'Started',
|
||||
StartedOnTime: 'Started on %s',
|
||||
Total: 'Total',
|
||||
Prolog: 'Prolog',
|
||||
EndTime: 'End time',
|
||||
Locked: 'Locked',
|
||||
Attributes: 'Attributes',
|
||||
@ -461,6 +470,8 @@ module.exports = {
|
||||
AttachSecurityGroup: 'Attach Security Group',
|
||||
DetachSecurityGroup: 'Detach Security Group',
|
||||
DetachSecurityGroupFromNic: 'Detach Security Group %1$s from NIC %2$s',
|
||||
PortForwarding: 'Port forwarding',
|
||||
HostnamePortsForwardedToVmPorts: '%1$s ports %2$s forwarded to VM ports %3$s',
|
||||
/* VM schema - snapshot */
|
||||
VmSnapshotNameConcept: 'The new snapshot name. It can be empty',
|
||||
/* VM schema - actions */
|
||||
@ -484,6 +495,11 @@ module.exports = {
|
||||
OperationConceptDeleteDb: `
|
||||
No recover action possible, delete the VM from the DB.
|
||||
It does not trigger any action on the hypervisor`,
|
||||
/* VM schema - history */
|
||||
RequestId: 'Request ID',
|
||||
TimeWhenTheStateChanged: 'Time when the state changed',
|
||||
TotalTimeInThisState: 'Total time in this state',
|
||||
PrologTimeForThisState: 'Prolog time for this state',
|
||||
|
||||
/* VM Template schema */
|
||||
/* VM Template schema - general */
|
||||
@ -783,6 +799,7 @@ module.exports = {
|
||||
Wilds: 'Wilds',
|
||||
Zombies: 'Zombies',
|
||||
Numa: 'NUMA',
|
||||
Hostname: 'Hostname',
|
||||
/* Host schema - capacity */
|
||||
AllocatedMemory: 'Allocated Memory',
|
||||
AllocatedCpu: 'Allocated CPU',
|
||||
@ -801,6 +818,7 @@ module.exports = {
|
||||
|
||||
/* Marketplace App schema */
|
||||
/* Marketplace App - general */
|
||||
RegisteredAt: 'Registered %s',
|
||||
Version: 'Version',
|
||||
AppTemplate: 'App Template',
|
||||
TemplatesForTheApp: 'Templates for the App',
|
||||
|
@ -819,6 +819,7 @@ export const VM_ACTIONS_BY_STATE = {
|
||||
STATES.INIT,
|
||||
STATES.PENDING,
|
||||
STATES.HOLD,
|
||||
STATES.ACTIVE,
|
||||
STATES.STOPPED,
|
||||
STATES.SUSPENDED,
|
||||
STATES.POWEROFF,
|
||||
@ -1126,40 +1127,24 @@ export const HISTORY_ACTIONS = [
|
||||
]
|
||||
|
||||
/**
|
||||
* @enum {(
|
||||
* 'IP'|
|
||||
* 'IP6'|
|
||||
* 'IP6_GLOBAL'|
|
||||
* 'IP6_ULA'|
|
||||
* 'VROUTER_IP'|
|
||||
* 'VROUTER_IP6_GLOBAL'|
|
||||
* 'VROUTER_IP6_ULA'
|
||||
* )} Possible attribute names for nic alias ip
|
||||
* @type {(string|string[])[]} Possible attribute names for nic alias ip
|
||||
*/
|
||||
export const NIC_ALIAS_IP_ATTRS = [
|
||||
export const NIC_IP_ATTRS = [
|
||||
'EXTERNAL_IP', // external IP must be first
|
||||
'IP',
|
||||
'IP6',
|
||||
'IP6_GLOBAL',
|
||||
'IP6_ULA',
|
||||
'VROUTER_IP',
|
||||
'VROUTER_IP6_GLOBAL',
|
||||
'VROUTER_IP6_ULA',
|
||||
['IP6_ULA', 'IP6_GLOBAL'],
|
||||
'MAC',
|
||||
]
|
||||
|
||||
/**
|
||||
* @enum {(
|
||||
* 'GUEST_IP'|
|
||||
* 'GUEST_IP_ADDRESSES'|
|
||||
* 'AWS_IP_ADDRESS'|
|
||||
* 'AWS_PUBLIC_IP_ADDRESS'|
|
||||
* 'AWS_PRIVATE_IP_ADDRESS'|
|
||||
* 'AZ_IPADDRESS'|
|
||||
* 'SL_PRIMARYIPADDRESS'
|
||||
* )} Possible attribute names for external ip
|
||||
* @type {string[]} Possible attribute names for external ip
|
||||
*/
|
||||
export const EXTERNAL_IP_ATTRS = [
|
||||
'GUEST_IP',
|
||||
'GUEST_IP_ADDRESSES',
|
||||
|
||||
// unsupported by OpenNebula
|
||||
'AWS_IP_ADDRESS',
|
||||
'AWS_PUBLIC_IP_ADDRESS',
|
||||
'AWS_PRIVATE_IP_ADDRESS',
|
||||
|
@ -42,7 +42,7 @@ function MarketplaceApps() {
|
||||
{selectedRows?.length > 0 && (
|
||||
<Stack overflow="auto">
|
||||
{selectedRows?.length === 1 ? (
|
||||
<MarketplaceAppsTabs id={selectedRows[0]?.values.ID} />
|
||||
<MarketplaceAppsTabs id={selectedRows[0]?.original.ID} />
|
||||
) : (
|
||||
<Stack
|
||||
direction="row"
|
||||
|
@ -44,9 +44,10 @@ const Datastores = memo(
|
||||
|
||||
return (
|
||||
<DatastoresTable
|
||||
onlyGlobalSearch
|
||||
disableRowSelect
|
||||
disableGlobalSort
|
||||
disableRowSelect
|
||||
displaySelectedRows
|
||||
pageSize={5}
|
||||
useQuery={() =>
|
||||
useGetProvisionResourceQuery(
|
||||
{ resource: 'datastore' },
|
||||
|
@ -83,9 +83,10 @@ const Hosts = memo(({ id }) => {
|
||||
</Stack>
|
||||
</Stack>
|
||||
<HostsTable
|
||||
onlyGlobalSearch
|
||||
disableRowSelect
|
||||
disableGlobalSort
|
||||
disableRowSelect
|
||||
displaySelectedRows
|
||||
pageSize={5}
|
||||
useQuery={() =>
|
||||
useGetProvisionResourceQuery(
|
||||
{ resource: 'host' },
|
||||
|
@ -75,9 +75,10 @@ const Networks = memo(({ id }) => {
|
||||
</Stack>
|
||||
</Stack>
|
||||
<VNetworksTable
|
||||
onlyGlobalSearch
|
||||
disableRowSelect
|
||||
disableGlobalSort
|
||||
disableRowSelect
|
||||
displaySelectedRows
|
||||
pageSize={5}
|
||||
useQuery={() =>
|
||||
useGetProvisionResourceQuery(
|
||||
{ resource: 'network' },
|
||||
|
@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, useMemo, memo, useState } from 'react'
|
||||
import { ReactElement, memo, useState, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
Paper,
|
||||
@ -24,134 +24,208 @@ import {
|
||||
TextField,
|
||||
} from '@mui/material'
|
||||
import { Edit } from 'iconoir-react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import { useForm, FormProvider, useFormContext } from 'react-hook-form'
|
||||
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import { useUpdateUserMutation } from 'client/features/OneApi/user'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
|
||||
import {
|
||||
FIELDS,
|
||||
SCHEMA,
|
||||
} from 'client/containers/Settings/Authentication/schema'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { Legend } from 'client/components/Forms'
|
||||
import { jsonToXml } from 'client/models/Helper'
|
||||
import { sanitize } from 'client/utils'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
/** @returns {ReactElement} Settings authentication */
|
||||
const Settings = () => (
|
||||
<Paper variant="outlined" sx={{ py: '1.5em' }}>
|
||||
<Stack gap="1em">
|
||||
{FIELDS.map((field) => (
|
||||
<FieldComponent
|
||||
key={'settings-authentication-field-' + field.name}
|
||||
field={field}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
const FIELDS = [
|
||||
{
|
||||
name: 'SSH_PUBLIC_KEY',
|
||||
label: T.SshPublicKey,
|
||||
tooltip: T.AddUserSshPublicKey,
|
||||
},
|
||||
{
|
||||
name: 'SSH_PRIVATE_KEY',
|
||||
label: T.SshPrivateKey,
|
||||
tooltip: T.AddUserSshPrivateKey,
|
||||
},
|
||||
{
|
||||
name: 'SSH_PASSPHRASE',
|
||||
label: T.SshPassphraseKey,
|
||||
tooltip: T.AddUserSshPassphraseKey,
|
||||
},
|
||||
]
|
||||
|
||||
const removeProperty = (propKey, { [propKey]: _, ...rest }) => rest
|
||||
|
||||
// -------------------------------------
|
||||
// FIELD COMPONENT
|
||||
// -------------------------------------
|
||||
|
||||
const FieldComponent = memo(
|
||||
({ field, defaultValue, onSubmit, setIsEnabled }) => {
|
||||
const { register, reset, handleSubmit } = useFormContext()
|
||||
const { name } = field
|
||||
|
||||
useEffect(() => {
|
||||
reset({ [name]: defaultValue })
|
||||
}, [defaultValue])
|
||||
|
||||
const handleKeyDown = (evt) => {
|
||||
if (evt.key === 'Escape') {
|
||||
setIsEnabled(false)
|
||||
reset({ [name]: defaultValue })
|
||||
evt.stopPropagation()
|
||||
}
|
||||
|
||||
if (evt.key === 'Enter') {
|
||||
handleSubmit(onSubmit)(evt)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBlur = (evt) => handleSubmit(onSubmit)(evt)
|
||||
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
autoFocus
|
||||
multiline
|
||||
rows={5}
|
||||
defaultValue={defaultValue}
|
||||
variant="outlined"
|
||||
onKeyDown={handleKeyDown}
|
||||
helperText={<Translate word={T.PressEscapeToCancel} />}
|
||||
{...register(name, { onBlur: handleBlur, shouldUnregister: true })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const FieldComponent = memo(({ field }) => {
|
||||
const [isEnabled, setIsEnabled] = useState(false)
|
||||
const { name, label, tooltip } = field
|
||||
|
||||
const { user, settings } = useAuth()
|
||||
const [updateUser, { isLoading }] = useUpdateUserMutation()
|
||||
const { enqueueError } = useGeneralApi()
|
||||
|
||||
const defaultValues = useMemo(() => SCHEMA.cast(settings), [settings])
|
||||
|
||||
const { watch, register, reset } = useForm({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues,
|
||||
resolver: yupResolver(SCHEMA),
|
||||
})
|
||||
|
||||
const sanitizedValue = useMemo(() => sanitize`${watch(name)}`, [isEnabled])
|
||||
|
||||
const handleUpdateUser = async () => {
|
||||
try {
|
||||
if (isLoading || !isEnabled) return
|
||||
|
||||
const castedData = SCHEMA.cast(watch(), { isSubmit: true })
|
||||
const template = jsonToXml(castedData)
|
||||
|
||||
updateUser({ id: user.ID, template })
|
||||
setIsEnabled(false)
|
||||
} catch {
|
||||
enqueueError(T.SomethingWrong)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBlur = () => {
|
||||
handleUpdateUser()
|
||||
}
|
||||
|
||||
const handleKeyDown = (evt) => {
|
||||
if (evt.key === 'Escape') {
|
||||
reset(defaultValues)
|
||||
setIsEnabled(false)
|
||||
evt.stopPropagation()
|
||||
}
|
||||
|
||||
if (evt.key === 'Enter') {
|
||||
handleUpdateUser()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack component="fieldset" sx={{ minInlineSize: 'auto' }}>
|
||||
<Legend title={label} tooltip={tooltip} />
|
||||
{isEnabled ? (
|
||||
<>
|
||||
<TextField
|
||||
fullWidth
|
||||
autoFocus
|
||||
multiline
|
||||
rows={5}
|
||||
variant="outlined"
|
||||
onKeyDown={handleKeyDown}
|
||||
helperText={<Translate word={T.PressEscapeToCancel} />}
|
||||
{...register(field.name, { onBlur: handleBlur })}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
gap="1em"
|
||||
paddingX={1}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Skeleton variant="text" width="100%" height={36} />
|
||||
<Skeleton variant="circular" width={28} height={28} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography noWrap title={sanitizedValue}>
|
||||
{sanitizedValue}
|
||||
</Typography>
|
||||
<IconButton onClick={() => setIsEnabled(true)}>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
})
|
||||
|
||||
FieldComponent.propTypes = {
|
||||
field: PropTypes.object.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
setIsEnabled: PropTypes.func.isRequired,
|
||||
defaultValue: PropTypes.string,
|
||||
}
|
||||
|
||||
FieldComponent.displayName = 'FieldComponent'
|
||||
|
||||
// -------------------------------------
|
||||
// STATIC COMPONENT
|
||||
// -------------------------------------
|
||||
|
||||
const StaticComponent = memo(
|
||||
({ field, defaultValue, isEnabled, setIsEnabled }) => {
|
||||
const { formState } = useFormContext()
|
||||
const { name, tooltip } = field
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
gap="1em"
|
||||
paddingX={1}
|
||||
>
|
||||
{formState.isSubmitting ? (
|
||||
<>
|
||||
<Skeleton variant="text" width="100%" height={36} />
|
||||
<Skeleton variant="circular" width={28} height={28} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography
|
||||
noWrap
|
||||
title={sanitize`${defaultValue}`}
|
||||
color="text.secondary"
|
||||
>
|
||||
{sanitize`${defaultValue}` || <Translate word={tooltip} />}
|
||||
</Typography>
|
||||
|
||||
<IconButton
|
||||
disabled={isEnabled && !isEnabled?.[name]}
|
||||
onClick={() => setIsEnabled({ [name]: true })}
|
||||
>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
StaticComponent.propTypes = {
|
||||
field: PropTypes.object.isRequired,
|
||||
isEnabled: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
|
||||
setIsEnabled: PropTypes.func.isRequired,
|
||||
defaultValue: PropTypes.string,
|
||||
}
|
||||
|
||||
StaticComponent.displayName = 'StaticComponent'
|
||||
|
||||
/**
|
||||
* Section to change user settings about SSH keys and passphrase.
|
||||
*
|
||||
* @returns {ReactElement} Settings authentication
|
||||
*/
|
||||
const Settings = () => {
|
||||
const [isEnabled, setIsEnabled] = useState(false)
|
||||
const { user, settings } = useAuth()
|
||||
const { enqueueError } = useGeneralApi()
|
||||
const [updateUser] = useUpdateUserMutation()
|
||||
|
||||
const { ...methods } = useForm({ reValidateMode: 'onSubmit' })
|
||||
|
||||
const handleUpdateUser = async (formData) => {
|
||||
try {
|
||||
setIsEnabled(false)
|
||||
|
||||
const newSettings = FIELDS.reduce(
|
||||
(result, { name }) =>
|
||||
result[name] === '' ? removeProperty(name, result) : result,
|
||||
{ ...settings, ...formData }
|
||||
)
|
||||
|
||||
const template = jsonToXml(newSettings)
|
||||
|
||||
await updateUser({ id: user.ID, template, replace: 0 })
|
||||
} catch {
|
||||
isEnabled && setIsEnabled(false)
|
||||
enqueueError(T.SomethingWrong)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper variant="outlined" sx={{ py: '1.5em' }}>
|
||||
<FormProvider {...methods}>
|
||||
<Stack gap="1em">
|
||||
{FIELDS.map((field) => (
|
||||
<Stack
|
||||
component="fieldset"
|
||||
key={'settings-authentication-field-' + field.name}
|
||||
sx={{ minInlineSize: 'auto' }}
|
||||
>
|
||||
<Legend title={field.label} />
|
||||
{isEnabled[field.name] ? (
|
||||
<FieldComponent
|
||||
field={field}
|
||||
defaultValue={settings[field.name]}
|
||||
onSubmit={handleUpdateUser}
|
||||
setIsEnabled={setIsEnabled}
|
||||
/>
|
||||
) : (
|
||||
<StaticComponent
|
||||
field={field}
|
||||
defaultValue={settings[field.name]}
|
||||
isEnabled={isEnabled}
|
||||
setIsEnabled={setIsEnabled}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</FormProvider>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
export default Settings
|
||||
|
@ -1,53 +0,0 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { object, string } from 'yup'
|
||||
import { getValidationFromFields } from 'client/utils'
|
||||
import { T, INPUT_TYPES } from 'client/constants'
|
||||
|
||||
const PUBLIC_KEY_FIELD = {
|
||||
name: 'SSH_PUBLIC_KEY',
|
||||
label: T.SshPublicKey,
|
||||
tooltip: T.AddUserSshPublicKey,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => undefined),
|
||||
}
|
||||
|
||||
const PRIVATE_KEY_FIELD = {
|
||||
name: 'SSH_PRIVATE_KEY',
|
||||
label: T.SshPrivateKey,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => undefined),
|
||||
}
|
||||
|
||||
const PASSPHRASE_FIELD = {
|
||||
name: 'SSH_PASSPHRASE',
|
||||
label: T.SshPassphraseKey,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => undefined),
|
||||
}
|
||||
|
||||
export const FIELDS = [PUBLIC_KEY_FIELD, PRIVATE_KEY_FIELD, PASSPHRASE_FIELD]
|
||||
|
||||
export const SCHEMA = object(getValidationFromFields(FIELDS))
|
@ -13,12 +13,11 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, useEffect, useMemo, useRef } from 'react'
|
||||
import { Paper, Stack, CircularProgress } from '@mui/material'
|
||||
import { ReactElement, useEffect, useMemo, useCallback } from 'react'
|
||||
import { Paper, debounce } from '@mui/material'
|
||||
import { useForm, FormProvider } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import { useAuth, useAuthApi } from 'client/features/Auth'
|
||||
import { useUpdateUserMutation } from 'client/features/OneApi/user'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
|
||||
@ -27,44 +26,58 @@ import {
|
||||
SCHEMA,
|
||||
} from 'client/containers/Settings/ConfigurationUI/schema'
|
||||
import { FormWithSchema } from 'client/components/Forms'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { jsonToXml } from 'client/models/Helper'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
/** @returns {ReactElement} Settings configuration UI */
|
||||
/**
|
||||
* Section to change user configuration about UI.
|
||||
*
|
||||
* @returns {ReactElement} Settings configuration UI
|
||||
*/
|
||||
const Settings = () => {
|
||||
const fieldsetRef = useRef([])
|
||||
const { user, settings } = useAuth()
|
||||
const [updateUser, { isLoading }] = useUpdateUserMutation()
|
||||
const { changeAuthUser } = useAuthApi()
|
||||
const { enqueueError } = useGeneralApi()
|
||||
const [updateUser] = useUpdateUserMutation()
|
||||
|
||||
const { watch, ...methods } = useForm({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: useMemo(() => SCHEMA.cast(settings), [settings]),
|
||||
resolver: yupResolver(SCHEMA),
|
||||
const { watch, handleSubmit, ...methods } = useForm({
|
||||
reValidateMode: 'onChange',
|
||||
defaultValues: useMemo(
|
||||
() => SCHEMA.cast(settings, { stripUnknown: true }),
|
||||
[settings]
|
||||
),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
watch((formData) => {
|
||||
const handleUpdateUser = useCallback(
|
||||
debounce(async (formData) => {
|
||||
try {
|
||||
if (isLoading) return
|
||||
if (methods?.formState?.isSubmitting) return
|
||||
|
||||
const castedData = SCHEMA.cast(formData, { isSubmit: true })
|
||||
const template = jsonToXml(castedData)
|
||||
|
||||
updateUser({ id: user.ID, template })
|
||||
const template = jsonToXml(formData)
|
||||
await updateUser({ id: user.ID, template, replace: 1 })
|
||||
} catch {
|
||||
enqueueError(T.SomethingWrong)
|
||||
}
|
||||
})
|
||||
}, [watch])
|
||||
}, 1000),
|
||||
[updateUser]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
fieldsetRef.current.disabled = isLoading
|
||||
}, [isLoading])
|
||||
const subscription = watch((formData) => {
|
||||
// update user settings before submit
|
||||
const newSettings = { TEMPLATE: { ...user.TEMPLATE, ...formData } }
|
||||
changeAuthUser({ ...user, ...newSettings })
|
||||
|
||||
handleSubmit(handleUpdateUser)()
|
||||
})
|
||||
|
||||
return () => subscription.unsubscribe()
|
||||
}, [watch])
|
||||
|
||||
return (
|
||||
<Paper
|
||||
component="form"
|
||||
onSubmit={handleSubmit(handleUpdateUser)}
|
||||
variant="outlined"
|
||||
sx={{ p: '1em', maxWidth: { sm: 'auto', md: 550 } }}
|
||||
>
|
||||
@ -72,13 +85,7 @@ const Settings = () => {
|
||||
<FormWithSchema
|
||||
cy={'settings-ui'}
|
||||
fields={FIELDS}
|
||||
rootProps={{ ref: fieldsetRef }}
|
||||
legend={
|
||||
<Stack direction="row" alignItems="center" gap="1em">
|
||||
<Translate word={T.ConfigurationUI} />
|
||||
{isLoading && <CircularProgress size={20} color="secondary" />}
|
||||
</Stack>
|
||||
}
|
||||
legend={T.ConfigurationUI}
|
||||
/>
|
||||
</FormProvider>
|
||||
</Paper>
|
||||
|
@ -38,7 +38,7 @@ function VirtualMachines() {
|
||||
{selectedRows?.length > 0 && (
|
||||
<Stack overflow="auto" data-cy={'detail'}>
|
||||
{selectedRows?.length === 1 ? (
|
||||
<VmTabs id={selectedRows[0]?.values.ID} />
|
||||
<VmTabs id={selectedRows[0]?.original.ID} />
|
||||
) : (
|
||||
<Stack
|
||||
direction="row"
|
||||
@ -52,7 +52,7 @@ function VirtualMachines() {
|
||||
({ original, id, toggleRowSelected }) => (
|
||||
<Chip
|
||||
key={id}
|
||||
variant="text"
|
||||
variant="outlined"
|
||||
label={original?.NAME ?? id}
|
||||
onDelete={() => toggleRowSelected(false)}
|
||||
/>
|
||||
|
@ -38,7 +38,7 @@ function VmTemplates() {
|
||||
{selectedRows?.length > 0 && (
|
||||
<Stack overflow="auto">
|
||||
{selectedRows?.length === 1 ? (
|
||||
<VmTemplateTabs id={selectedRows[0]?.values.ID} />
|
||||
<VmTemplateTabs id={selectedRows[0]?.original.ID} />
|
||||
) : (
|
||||
<Stack
|
||||
direction="row"
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user