1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-23 22:50:09 +03:00

F #5706: Implements caching to client (#1761)

(cherry picked from commit 869d2a85dbcd35af5f7f65dcc5755ab9f0d844cd)
This commit is contained in:
Sergio Betanzos 2022-02-11 17:59:01 +01:00 committed by Tino Vazquez
parent f5d00c97bf
commit 1c23dfcd7a
No known key found for this signature in database
GPG Key ID: 14201E424D02047E
360 changed files with 14940 additions and 12669 deletions

File diff suppressed because it is too large Load Diff

View File

@ -54,7 +54,6 @@
"@babel/preset-react": "7.16.0",
"@emotion/react": "11.6.0",
"@emotion/styled": "11.6.0",
"@hookform/devtools": "4.0.1",
"@hookform/resolvers": "2.8.2",
"@loadable/babel-plugin": "5.13.2",
"@loadable/component": "5.15.0",

View File

@ -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 Router from 'client/router'
import { ENDPOINTS, PATH } from 'client/apps/provision/routes'
@ -21,7 +21,9 @@ import { ENDPOINTS as DEV_ENDPOINTS } from 'client/router/dev'
import { useGeneral, useGeneralApi } from 'client/features/General'
import { useAuth, useAuthApi } from 'client/features/Auth'
import { useProvisionTemplate, useProvisionApi } from 'client/features/One'
import provisionApi from 'client/features/OneApi/provision'
import providerApi from 'client/features/OneApi/provider'
import { useSocket } from 'client/hooks'
import Sidebar from 'client/components/Sidebar'
import Notifier from 'client/components/Notifier'
@ -31,37 +33,54 @@ import { _APPS } from 'client/constants'
export const APP_NAME = _APPS.provision.name
const MESSAGE_PROVISION_SUCCESS_CREATED = 'Provision successfully created'
/**
* Provision App component.
*
* @returns {JSXElementConstructor} App rendered.
* @returns {ReactElement} App rendered.
*/
const ProvisionApp = () => {
const { isLogged, jwt, firstRender, providerConfig } = useAuth()
const { getAuthUser, logout, getProviderConfig } = useAuthApi()
const { getProvisionSocket } = useSocket()
const { isLogged, jwt, firstRender } = useAuth()
const { getAuthUser, logout } = useAuthApi()
const provisionTemplate = useProvisionTemplate()
const { getProvisionsTemplates } = useProvisionApi()
const { appTitle, zone } = useGeneral()
const { changeAppTitle, enqueueSuccess } = useGeneralApi()
const { appTitle } = useGeneral()
const { changeAppTitle } = useGeneralApi()
const queryProps = [undefined, { skip: !jwt }]
provisionApi.endpoints.getProvisionTemplates.useQuery(...queryProps)
providerApi.endpoints.getProviderConfig.useQuery(...queryProps)
useEffect(() => {
;(async () => {
appTitle !== APP_NAME && changeAppTitle(APP_NAME)
try {
if (jwt) {
getAuthUser()
!providerConfig && (await getProviderConfig())
!provisionTemplate?.length && (await getProvisionsTemplates())
}
jwt && getAuthUser()
} catch {
logout()
}
})()
}, [jwt])
useEffect(() => {
if (!jwt || !zone) return
const socket = getProvisionSocket((payload) => {
const { command, data } = payload
// Dispatch successfully notification when one provision is created
if (command === 'create' && data === MESSAGE_PROVISION_SUCCESS_CREATED) {
enqueueSuccess(MESSAGE_PROVISION_SUCCESS_CREATED)
}
})
socket?.on()
return () => socket?.off()
}, [jwt, zone])
const endpoints = useMemo(
() => [...ENDPOINTS, ...(isDevelopment() ? DEV_ENDPOINTS : [])],
[]

View File

@ -21,7 +21,6 @@ import { StaticRouter, BrowserRouter } from 'react-router-dom'
import { Provider as ReduxProvider } from 'react-redux'
import { Store } from 'redux'
import SocketProvider from 'client/providers/socketProvider'
import MuiProvider from 'client/providers/muiProvider'
import NotistackProvider from 'client/providers/notistackProvider'
import { TranslateProvider } from 'client/components/HOC'
@ -42,25 +41,23 @@ buildTranslationLocale()
*/
const Provision = ({ store = {}, location = '', context = {} }) => (
<ReduxProvider store={store}>
<SocketProvider>
<TranslateProvider>
<MuiProvider theme={theme}>
<NotistackProvider>
{location && context ? (
// server build
<StaticRouter location={location} context={context}>
<App />
</StaticRouter>
) : (
// browser build
<BrowserRouter basename={`${APP_URL}/${ProvisionAppName}`}>
<App />
</BrowserRouter>
)}
</NotistackProvider>
</MuiProvider>
</TranslateProvider>
</SocketProvider>
<TranslateProvider>
<MuiProvider theme={theme}>
<NotistackProvider>
{location && context ? (
// server build
<StaticRouter location={location} context={context}>
<App />
</StaticRouter>
) : (
// browser build
<BrowserRouter basename={`${APP_URL}/${ProvisionAppName}`}>
<App />
</BrowserRouter>
)}
</NotistackProvider>
</MuiProvider>
</TranslateProvider>
</ReduxProvider>
)

View File

@ -26,7 +26,7 @@ import { ENDPOINTS as DEV_ENDPOINTS } from 'client/router/dev'
import { useGeneral, useGeneralApi } from 'client/features/General'
import { useAuth, useAuthApi } from 'client/features/Auth'
import { useSystem, useSystemApi } from 'client/features/One'
import systemApi from 'client/features/OneApi/system'
import Sidebar from 'client/components/Sidebar'
import Notifier from 'client/components/Notifier'
@ -42,26 +42,23 @@ export const APP_NAME = _APPS.sunstone.name
* @returns {JSXElementConstructor} App rendered.
*/
const SunstoneApp = () => {
const { isLogged, jwt, firstRender, view, views, config } = useAuth()
const { getAuthUser, logout, getSunstoneViews, getSunstoneConfig } =
useAuthApi()
const { isLogged, jwt, firstRender, view } = useAuth()
const { getAuthUser, logout } = useAuthApi()
const { appTitle } = useGeneral()
const { changeAppTitle } = useGeneralApi()
const { config: oneConfig } = useSystem()
const { getOneConfig } = useSystemApi()
const queryProps = [undefined, { skip: !jwt }]
systemApi.endpoints.getOneConfig.useQuery(...queryProps)
systemApi.endpoints.getSunstoneConfig.useQuery(...queryProps)
const views = systemApi.endpoints.getSunstoneViews.useQuery(...queryProps)
useEffect(() => {
;(async () => {
appTitle !== APP_NAME && changeAppTitle(APP_NAME)
try {
if (jwt) {
getAuthUser()
!view && (await getSunstoneViews())
!config && (await getSunstoneConfig())
!oneConfig && getOneConfig()
}
jwt && getAuthUser()
} catch {
logout()
}
@ -71,7 +68,7 @@ const SunstoneApp = () => {
const endpoints = useMemo(
() => [
...ENDPOINTS,
...(view ? getEndpointsByView(views?.[view], ONE_ENDPOINTS) : []),
...(view ? getEndpointsByView(views?.data?.[view], ONE_ENDPOINTS) : []),
...(isDevelopment() ? DEV_ENDPOINTS : []),
],
[view]

View File

@ -13,11 +13,22 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo } from 'react'
import PropTypes from 'prop-types'
const SERVICE = 'service'
const SERVICE_TEMPLATE = 'service_template'
import { SubmitButton } from 'client/components/FormControl'
module.exports = {
SERVICE,
SERVICE_TEMPLATE,
const QueryButton = memo(({ useQuery, ...props }) => {
const { refetch, isFetching } = useQuery?.() ?? {}
return <SubmitButton isSubmitting={isFetching} onClick={refetch} {...props} />
})
QueryButton.propTypes = {
useQuery: PropTypes.func,
...SubmitButton.propTypes,
}
QueryButton.displayName = 'QueryButton'
export default QueryButton

View File

@ -18,7 +18,7 @@ import PropTypes from 'prop-types'
import { Trash, Edit, ClockOutline } from 'iconoir-react'
import { useAuth } from 'client/features/Auth'
import { useGetSunstoneConfigQuery } from 'client/features/OneApi/system'
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
import {
CreateCharterForm,
@ -28,9 +28,13 @@ import {
} from 'client/components/Forms/Vm'
import { Tr, Translate } from 'client/components/HOC'
import { ScheduledAction } from 'client/models/Scheduler'
import { sentenceCase } from 'client/utils'
import { T, VM_ACTIONS, VM_ACTIONS_IN_CHARTER } from 'client/constants'
import {
T,
VM_ACTIONS,
VM_ACTIONS_IN_CHARTER,
ScheduleAction,
} from 'client/constants'
/**
* Returns a button to trigger form to create a scheduled action.
@ -53,7 +57,7 @@ const CreateSchedButton = memo(({ vm, relative, onSubmit }) => (
{
name: T.PunctualAction,
dialogProps: {
title: T.ScheduledAction,
title: T.ScheduleAction,
dataCy: 'modal-sched-actions',
},
form: () =>
@ -71,7 +75,7 @@ const CreateSchedButton = memo(({ vm, relative, onSubmit }) => (
*
* @param {object} props - Props
* @param {object} props.vm - Vm resource
* @param {ScheduledAction} props.schedule - Schedule action
* @param {ScheduleAction} props.schedule - Schedule action
* @param {boolean} [props.relative] - Applies to the form relative format
* @param {function():Promise} props.onSubmit - Submit function
* @returns {ReactElement} Button
@ -91,10 +95,7 @@ const UpdateSchedButton = memo(({ vm, schedule, relative, onSubmit }) => {
{
dialogProps: {
title: (
<Translate
word={T.UpdateScheduledAction}
values={[titleAction]}
/>
<Translate word={T.UpdateScheduleAction} values={[titleAction]} />
),
dataCy: 'modal-sched-actions',
},
@ -113,7 +114,7 @@ const UpdateSchedButton = memo(({ vm, schedule, relative, onSubmit }) => {
* Returns a button to trigger modal to delete a scheduled action.
*
* @param {object} props - Props
* @param {ScheduledAction} props.schedule - Schedule action
* @param {ScheduleAction} props.schedule - Schedule action
* @param {function():Promise} props.onSubmit - Submit function
* @returns {ReactElement} Button
*/
@ -156,7 +157,7 @@ const DeleteSchedButton = memo(({ onSubmit, schedule }) => {
* @returns {ReactElement} Button
*/
const CharterButton = memo(({ relative, onSubmit }) => {
const { config } = useAuth()
const { data: config } = useGetSunstoneConfigQuery()
const leases = useMemo(
() =>
@ -164,7 +165,7 @@ const CharterButton = memo(({ relative, onSubmit }) => {
Object.entries(config?.leases ?? {}).filter(([action]) =>
VM_ACTIONS_IN_CHARTER.includes(action)
),
[config.leases]
[config?.leases]
)
return (
@ -178,7 +179,7 @@ const CharterButton = memo(({ relative, onSubmit }) => {
options={[
{
dialogProps: {
title: T.ScheduledAction,
title: T.ScheduleAction,
dataCy: 'modal-sched-actions',
},
form: () =>

View File

@ -13,106 +13,90 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo } from 'react'
import { memo, ReactElement } from 'react'
import PropTypes from 'prop-types'
import { Typography } from '@mui/material'
import makeStyles from '@mui/styles/makeStyles'
import { Folder as DatastoreIcon } from 'iconoir-react'
import { User, Group, Lock, Cloud, Server } from 'iconoir-react'
import SelectCard, { Action } from 'client/components/Cards/SelectCard'
import {
StatusBadge,
StatusCircle,
StatusChip,
LinearProgressWithLabel,
} from 'client/components/Status'
import * as DatastoreModel from 'client/models/Datastore'
const useStyles = makeStyles({
title: {
display: 'flex',
gap: '0.5rem',
},
content: {
padding: '2em',
display: 'flex',
flexFlow: 'column',
gap: '1em',
},
})
import { getState, getType, getCapacityInfo } from 'client/models/Datastore'
import { rowStyles } from 'client/components/Tables/styles'
import { Datastore } from 'client/constants'
const DatastoreCard = memo(
({ value, isSelected, handleClick, actions }) => {
const classes = useStyles()
/**
* @param {object} props - Props
* @param {Datastore} props.datastore - Datastore resource
* @param {object} props.rootProps - Props to root component
* @param {ReactElement} props.actions - Actions
* @returns {ReactElement} - Card
*/
({ datastore, rootProps, actions }) => {
const classes = rowStyles()
const { ID, NAME } = value
const { ID, NAME, UNAME, GNAME, CLUSTERS, LOCK, PROVISION_ID } = datastore
const type = DatastoreModel.getType(value)
const state = DatastoreModel.getState(value)
const { percentOfUsed, percentLabel } =
DatastoreModel.getCapacityInfo(value)
const type = getType(datastore)
const { color: stateColor, name: stateName } = getState(datastore)
const { percentOfUsed, percentLabel } = getCapacityInfo(datastore)
const totalClusters = [CLUSTERS?.ID ?? []].flat().join(',')
return (
<SelectCard
action={actions?.map((action) => (
<Action key={action?.cy} {...action} />
))}
icon={
<StatusBadge stateColor={state.color}>
<DatastoreIcon />
</StatusBadge>
}
title={
<span className={classes.title}>
<Typography title={NAME} noWrap component="span">
{NAME}
</Typography>
<StatusChip text={type} />
</span>
}
subheader={`#${ID}`}
isSelected={isSelected}
handleClick={handleClick}
>
<div className={classes.content}>
<div {...rootProps} data-cy={`datastore-${ID}`}>
<div>
<StatusCircle color={stateColor} tooltip={stateName} />
</div>
<div className={classes.main}>
<div className={classes.title}>
<Typography component="span">{NAME}</Typography>
<span className={classes.labels}>
{LOCK && <Lock />}
<StatusChip text={type} />
</span>
</div>
<div className={classes.caption}>
<span>{`#${ID}`}</span>
<span title={`Owner: ${UNAME}`}>
<User />
<span>{` ${UNAME}`}</span>
</span>
<span title={`Group: ${GNAME}`}>
<Group />
<span>{` ${GNAME}`}</span>
</span>
{PROVISION_ID && (
<span title={`Provision ID: #${PROVISION_ID}`}>
<Cloud />
<span>{` ${PROVISION_ID}`}</span>
</span>
)}
<span title={`Cluster IDs: ${totalClusters}`}>
<Server />
<span>{` ${totalClusters}`}</span>
</span>
</div>
</div>
<div className={classes.secondary}>
<LinearProgressWithLabel value={percentOfUsed} label={percentLabel} />
</div>
</SelectCard>
{actions && <div className={classes.actions}>{actions}</div>}
</div>
)
},
(prev, next) =>
prev.isSelected === next.isSelected &&
prev.value?.STATE === next.value?.STATE
}
)
DatastoreCard.propTypes = {
value: PropTypes.shape({
ID: PropTypes.string.isRequired,
NAME: PropTypes.string.isRequired,
TYPE: PropTypes.string,
STATE: PropTypes.string,
TOTAL_MB: PropTypes.string,
FREE_MB: PropTypes.string,
USED_MB: PropTypes.string,
datastore: PropTypes.object,
rootProps: PropTypes.shape({
className: PropTypes.string,
}),
isSelected: PropTypes.bool,
handleClick: PropTypes.func,
actions: PropTypes.arrayOf(
PropTypes.shape({
handleClick: PropTypes.func.isRequired,
icon: PropTypes.node.isRequired,
cy: PropTypes.string,
})
),
}
DatastoreCard.defaultProps = {
value: {},
isSelected: false,
handleClick: undefined,
actions: undefined,
actions: PropTypes.any,
}
DatastoreCard.displayName = 'DatastoreCard'

View File

@ -0,0 +1,174 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, 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, useMemo } from 'react'
import PropTypes from 'prop-types'
import { DatabaseSettings, Folder, ModernTv } from 'iconoir-react'
import { Box, Typography, Paper } from '@mui/material'
import DiskSnapshotCard from 'client/components/Cards/DiskSnapshotCard'
import { StatusChip } from 'client/components/Status'
import { rowStyles } from 'client/components/Tables/styles'
import { stringToBoolean } from 'client/models/Helper'
import { prettyBytes } from 'client/utils'
import { Disk } from 'client/constants'
const DiskCard = memo(
({
disk = {},
actions = [],
extraActionProps = {},
snapshotActions = [],
extraSnapshotActionProps = {},
}) => {
const classes = rowStyles()
/** @type {Disk} */
const {
DISK_ID,
DATASTORE,
TARGET,
IMAGE,
TYPE,
FORMAT,
SIZE,
MONITOR_SIZE,
READONLY,
PERSISTENT,
SAVE,
CLONE,
IS_CONTEXT,
SNAPSHOTS,
} = disk
const size = +SIZE ? prettyBytes(+SIZE, 'MB') : '-'
const monitorSize = +MONITOR_SIZE ? prettyBytes(+MONITOR_SIZE, 'MB') : '-'
const type = String(TYPE).toLowerCase()
const image =
IMAGE ??
{
fs: `${FORMAT} - ${size}`,
swap: size,
}[type]
const labels = useMemo(
() =>
[
{ label: TYPE, dataCy: 'type' },
{
label: stringToBoolean(PERSISTENT) && 'PERSISTENT',
dataCy: 'persistent',
},
{
label: stringToBoolean(READONLY) && 'READONLY',
dataCy: 'readonly',
},
{
label: stringToBoolean(SAVE) && 'SAVE',
dataCy: 'save',
},
{
label: stringToBoolean(CLONE) && 'CLONE',
dataCy: 'clone',
},
].filter(({ label } = {}) => Boolean(label)),
[TYPE, PERSISTENT, READONLY, SAVE, CLONE]
)
return (
<Paper
variant="outlined"
className={classes.root}
sx={{ flexWrap: 'wrap' }}
data-cy={`disk-${DISK_ID}`}
>
<div className={classes.main}>
<div className={classes.title}>
<Typography component="span" data-cy="name">
{image}
</Typography>
<span className={classes.labels}>
{labels.map(({ label, dataCy }) => (
<StatusChip
key={label}
text={label}
{...(dataCy && { dataCy: dataCy })}
/>
))}
</span>
</div>
<div className={classes.caption}>
<span>{`#${DISK_ID}`}</span>
{TARGET && (
<span title={`Target: ${TARGET}`}>
<DatabaseSettings />
<span data-cy="target">{` ${TARGET}`}</span>
</span>
)}
{DATASTORE && (
<span title={`Datastore Name: ${DATASTORE}`}>
<Folder />
<span data-cy="datastore">{` ${DATASTORE}`}</span>
</span>
)}
<span title={`Monitor Size / Disk Size: ${monitorSize}/${size}`}>
<ModernTv />
<span data-cy="monitorsize">{` ${monitorSize}/${size}`}</span>
</span>
</div>
</div>
{!IS_CONTEXT && !!actions.length && (
<div className={classes.actions}>
{actions.map((Action, idx) => (
<Action
key={`${Action.displayName ?? idx}-${DISK_ID}`}
{...extraActionProps}
name={image}
disk={disk}
/>
))}
</div>
)}
{!!SNAPSHOTS?.length && (
<Box flexBasis="100%">
{SNAPSHOTS?.map((snapshot) => (
<DiskSnapshotCard
key={`${DISK_ID}-${snapshot.ID}`}
snapshot={snapshot}
actions={snapshotActions}
extraActionProps={extraSnapshotActionProps}
/>
))}
</Box>
)}
</Paper>
)
}
)
DiskCard.propTypes = {
disk: PropTypes.object.isRequired,
actions: PropTypes.any,
extraActionProps: PropTypes.object,
extraSnapshotActionProps: PropTypes.object,
snapshotActions: PropTypes.any,
}
DiskCard.displayName = 'DiskCard'
export default DiskCard

View File

@ -0,0 +1,94 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, 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 PropTypes from 'prop-types'
import { ModernTv } from 'iconoir-react'
import { Typography, Paper } from '@mui/material'
import { StatusChip } from 'client/components/Status'
import { rowStyles } from 'client/components/Tables/styles'
import { Translate } from 'client/components/HOC'
import * as Helper from 'client/models/Helper'
import { prettyBytes } from 'client/utils'
import { T, DiskSnapshot } from 'client/constants'
const DiskSnapshotCard = memo(
({ snapshot = {}, actions = [], extraActionProps = {} }) => {
const classes = rowStyles()
/** @type {DiskSnapshot} */
const {
ID,
NAME,
ACTIVE,
DATE,
SIZE: SNAPSHOT_SIZE,
MONITOR_SIZE: SNAPSHOT_MONITOR_SIZE,
} = snapshot
const isActive = Helper.stringToBoolean(ACTIVE)
const time = Helper.timeFromMilliseconds(+DATE)
const timeAgo = `created ${time.toRelative()}`
const size = +SNAPSHOT_SIZE ? prettyBytes(+SNAPSHOT_SIZE, 'MB') : '-'
const monitorSize = +SNAPSHOT_MONITOR_SIZE
? prettyBytes(+SNAPSHOT_MONITOR_SIZE, 'MB')
: '-'
return (
<Paper variant="outlined" className={classes.root}>
<div className={classes.main}>
<div className={classes.title}>
<Typography component="span">{NAME}</Typography>
<span className={classes.labels}>
{isActive && <StatusChip text={<Translate word={T.Active} />} />}
<StatusChip text={<Translate word={T.Snapshot} />} />
</span>
</div>
<div className={classes.caption}>
<span title={time.toFormat('ff')}>{`#${ID} ${timeAgo}`}</span>
<span title={`Monitor Size / Disk Size: ${monitorSize}/${size}`}>
<ModernTv />
<span>{` ${monitorSize}/${size}`}</span>
</span>
</div>
</div>
{!!actions.length && (
<div className={classes.actions}>
{actions.map((Action, idx) => (
<Action
key={`${Action.displayName ?? idx}-${ID}`}
{...extraActionProps}
snapshot={snapshot}
/>
))}
</div>
)}
</Paper>
)
}
)
DiskSnapshotCard.propTypes = {
snapshot: PropTypes.object.isRequired,
extraActionProps: PropTypes.object,
actions: PropTypes.arrayOf(PropTypes.string),
}
DiskSnapshotCard.displayName = 'DiskSnapshotCard'
export default DiskSnapshotCard

View File

@ -13,119 +13,96 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo } from 'react'
import { memo, ReactElement } from 'react'
import PropTypes from 'prop-types'
import { Server, ModernTv } from 'iconoir-react'
import { Typography } from '@mui/material'
import makeStyles from '@mui/styles/makeStyles'
import { HardDrive as HostIcon } from 'iconoir-react'
import SelectCard, { Action } from 'client/components/Cards/SelectCard'
import {
StatusBadge,
StatusCircle,
StatusChip,
LinearProgressWithLabel,
} from 'client/components/Status'
import { rowStyles } from 'client/components/Tables/styles'
import { Tr } from 'client/components/HOC'
import * as HostModel from 'client/models/Host'
const useStyles = makeStyles({
title: {
display: 'flex',
gap: '0.5rem',
},
content: {
padding: '2em',
display: 'flex',
flexFlow: 'column',
gap: '1em',
},
})
import { getAllocatedInfo, getState } from 'client/models/Host'
import { T, Host } from 'client/constants'
const HostCard = memo(
({ value, isSelected, handleClick, actions }) => {
const classes = useStyles()
const { ID, NAME, IM_MAD, VM_MAD } = value
/**
* @param {object} props - Props
* @param {Host} props.host - Host resource
* @param {object} props.rootProps - Props to root component
* @param {ReactElement} props.actions - Actions
* @returns {ReactElement} - Card
*/
({ host, rootProps, actions }) => {
const classes = rowStyles()
const { ID, NAME, IM_MAD, VM_MAD, HOST_SHARE, CLUSTER, TEMPLATE } = host
const { percentCpuUsed, percentCpuLabel, percentMemUsed, percentMemLabel } =
HostModel.getAllocatedInfo(value)
getAllocatedInfo(host)
const state = HostModel.getState(value)
const runningVms = HOST_SHARE?.RUNNING_VMS || 0
const totalVms = [host?.VMS?.ID ?? []].flat().length || 0
const { color: stateColor, name: stateName } = getState(host)
const mad = IM_MAD === VM_MAD ? IM_MAD : `${IM_MAD}/${VM_MAD}`
const labels = [...new Set([IM_MAD, VM_MAD])]
return (
<SelectCard
action={actions?.map((action) => (
<Action key={action?.cy} {...action} />
))}
icon={
<StatusBadge title={state?.name} stateColor={state.color}>
<HostIcon />
</StatusBadge>
}
title={
<span className={classes.title}>
<Typography title={NAME} noWrap component="span">
{NAME}
<div {...rootProps} data-cy={`host-${ID}`}>
<div>
<StatusCircle color={stateColor} tooltip={stateName} />
</div>
<div className={classes.main}>
<div className={classes.title}>
<Typography noWrap component="span">
{TEMPLATE?.NAME ?? NAME}
</Typography>
<StatusChip text={mad} />
</span>
}
subheader={`#${ID}`}
isSelected={isSelected}
handleClick={handleClick}
>
<div className={classes.content}>
<span className={classes.labels}>
{labels.map((label) => (
<StatusChip key={label} text={label} />
))}
</span>
</div>
<div className={classes.caption}>
<span>{`#${ID}`}</span>
<span data-cy="cluster" title={`Cluster: ${CLUSTER}`}>
<Server />
<span>{` ${CLUSTER}`}</span>
</span>
<span title={`Running VMs: ${runningVms} / ${totalVms}`}>
<ModernTv />
<span>{` ${runningVms} / ${totalVms}`}</span>
</span>
</div>
</div>
<div className={classes.secondary}>
<LinearProgressWithLabel
value={percentCpuUsed}
label={percentCpuLabel}
title={`${Tr(T.AllocatedCpu)}`}
/>
<LinearProgressWithLabel
value={percentMemUsed}
label={percentMemLabel}
title={`${Tr(T.AllocatedMemory)}`}
/>
</div>
</SelectCard>
{actions && <div className={classes.actions}>{actions}</div>}
</div>
)
},
(prev, next) =>
prev.isSelected === next.isSelected &&
prev.value?.STATE === next.value?.STATE
}
)
HostCard.propTypes = {
value: PropTypes.shape({
ID: PropTypes.string.isRequired,
NAME: PropTypes.string.isRequired,
TYPE: PropTypes.string,
STATE: PropTypes.string,
IM_MAD: PropTypes.string,
VM_MAD: PropTypes.string,
HOST_SHARE: PropTypes.shape({
CPU_USAGE: PropTypes.string,
TOTAL_CPU: PropTypes.string,
MEM_USAGE: PropTypes.string,
TOTAL_MEM: PropTypes.string,
}),
host: PropTypes.object,
rootProps: PropTypes.shape({
className: PropTypes.string,
}),
isSelected: PropTypes.bool,
handleClick: PropTypes.func,
actions: PropTypes.arrayOf(
PropTypes.shape({
handleClick: PropTypes.func.isRequired,
icon: PropTypes.node.isRequired,
cy: PropTypes.string,
})
),
}
HostCard.defaultProps = {
value: {},
isSelected: false,
handleClick: undefined,
actions: undefined,
actions: PropTypes.any,
}
HostCard.displayName = 'HostCard'

View File

@ -13,73 +13,82 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo } from 'react'
import { memo, ReactElement } from 'react'
import PropTypes from 'prop-types'
import { Typography } from '@mui/material'
import { User, Group, Lock, Server, Cloud } from 'iconoir-react'
import { NetworkAlt as NetworkIcon } from 'iconoir-react'
import SelectCard, { Action } from 'client/components/Cards/SelectCard'
import { LinearProgressWithLabel } from 'client/components/Status'
import { getLeasesInfo, getTotalLeases } from 'client/models/VirtualNetwork'
import { rowStyles } from 'client/components/Tables/styles'
const NetworkCard = memo(
({ value, isSelected, handleClick, actions }) => {
const { ID, NAME, USED_LEASES = '', AR_POOL } = value
/**
* @param {object} props - Props
* @param {object} props.network - Network resource
* @param {object} props.rootProps - Props to root component
* @param {ReactElement} props.actions - Actions
* @returns {ReactElement} - Card
*/
({ network, rootProps, actions }) => {
const classes = rowStyles()
const addresses = [AR_POOL?.AR ?? []].flat()
const totalLeases = addresses.reduce((res, ar) => +ar.SIZE + res, 0)
const { ID, NAME, UNAME, GNAME, LOCK, CLUSTERS, USED_LEASES, TEMPLATE } =
network
const percentOfUsed = (+USED_LEASES * 100) / +totalLeases || 0
const percentLabel = `${USED_LEASES} / ${totalLeases} (${Math.round(
percentOfUsed
)}%)`
const totalLeases = getTotalLeases(network)
const { percentOfUsed, percentLabel } = getLeasesInfo(network)
const totalClusters = [CLUSTERS?.ID ?? []].flat().length || 0
const provisionId = TEMPLATE?.PROVISION?.ID
return (
<SelectCard
action={actions?.map((action) => (
<Action key={action?.cy} {...action} />
))}
icon={<NetworkIcon />}
title={NAME}
subheader={`#${ID}`}
isSelected={isSelected}
handleClick={handleClick}
>
<div style={{ padding: '2em' }}>
<LinearProgressWithLabel value={percentOfUsed} label={percentLabel} />
<div {...rootProps} data-cy={`network-${ID}`}>
<div className={classes.main}>
<div className={classes.title}>
<Typography component="span">{NAME}</Typography>
<span className={classes.labels}>{LOCK && <Lock />}</span>
</div>
<div className={classes.caption}>
<span>{`#${ID}`}</span>
<span title={`Owner: ${UNAME}`}>
<User />
<span>{` ${UNAME}`}</span>
</span>
<span title={`Group: ${GNAME}`}>
<Group />
<span>{` ${GNAME}`}</span>
</span>
<span title={`Total Clusters: ${totalClusters}`}>
<Server />
<span>{` ${totalClusters}`}</span>
</span>
{provisionId && (
<span title={`Provision ID: #${provisionId}`}>
<Cloud />
<span>{` ${provisionId}`}</span>
</span>
)}
</div>
</div>
</SelectCard>
<div className={classes.secondary}>
<LinearProgressWithLabel
title={`Used / Total Leases: ${USED_LEASES} / ${totalLeases}`}
value={percentOfUsed}
label={percentLabel}
/>
</div>
{actions && <div className={classes.actions}>{actions}</div>}
</div>
)
},
(prev, next) => prev.isSelected === next.isSelected
}
)
NetworkCard.propTypes = {
value: PropTypes.shape({
ID: PropTypes.string.isRequired,
NAME: PropTypes.string.isRequired,
TYPE: PropTypes.string,
STATE: PropTypes.string,
USED_LEASES: PropTypes.string,
AR_POOL: PropTypes.shape({
AR: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
}),
network: PropTypes.object,
rootProps: PropTypes.shape({
className: PropTypes.string,
}),
isSelected: PropTypes.bool,
handleClick: PropTypes.func,
actions: PropTypes.arrayOf(
PropTypes.shape({
handleClick: PropTypes.func.isRequired,
icon: PropTypes.node.isRequired,
cy: PropTypes.string,
})
),
}
NetworkCard.defaultProps = {
value: {},
isSelected: false,
handleClick: undefined,
actions: undefined,
actions: PropTypes.any,
}
NetworkCard.displayName = 'NetworkCard'

View File

@ -0,0 +1,161 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, 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 PropTypes from 'prop-types'
import {
useMediaQuery,
Typography,
Box,
Paper,
Stack,
Divider,
} from '@mui/material'
import { rowStyles } from 'client/components/Tables/styles'
import MultipleTags from 'client/components/MultipleTags'
import SecurityGroupCard from 'client/components/Cards/SecurityGroupCard'
import { Translate } from 'client/components/HOC'
import { T, Nic, NicAlias } from 'client/constants'
const NicCard = memo(
({
nic = {},
actions = [],
extraActionProps = {},
aliasActions = [],
extraAliasActionProps = {},
}) => {
const classes = rowStyles()
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('md'))
/** @type {Nic|NicAlias} */
const {
NIC_ID,
NETWORK = '-',
IP,
MAC,
PCI_ID,
PARENT,
ADDRESS,
ALIAS,
SECURITY_GROUPS,
} = nic
const isAlias = !!PARENT?.length
const isPciDevice = PCI_ID !== undefined
const dataCy = isAlias ? 'alias' : 'nic'
const tags = [
{ text: IP, dataCy: `${dataCy}-ip` },
{ text: MAC, dataCy: `${dataCy}-mac` },
{ text: ADDRESS, dataCy: `${dataCy}-address` },
].filter(({ text } = {}) => Boolean(text))
return (
<Paper
variant="outlined"
className={classes.root}
data-cy={`${dataCy}-${NIC_ID}`}
sx={{
flexWrap: 'wrap',
...(isAlias && { boxShadow: 'none !important' }),
}}
>
<Box className={classes.main} {...(!isAlias && { pl: '1em' })}>
<div className={classes.title}>
<Typography component="span" data-cy={`${dataCy}-name`}>
{`${NIC_ID} | ${NETWORK}`}
</Typography>
<span className={classes.labels}>
<MultipleTags
clipboard
limitTags={isMobile ? 1 : 3}
tags={tags}
/>
</span>
</div>
</Box>
{!isMobile &&
!isPciDevice &&
actions.map((Action, idx) => (
<Action
key={`${Action.displayName ?? idx}-${NIC_ID}`}
{...extraActionProps}
nic={nic}
/>
))}
{!!ALIAS?.length && (
<Box flexBasis="100%">
{ALIAS?.map((alias) => (
<NicCard
key={alias.NIC_ID}
nic={alias}
actions={aliasActions}
extraActionProps={extraAliasActionProps}
/>
))}
</Box>
)}
{Array.isArray(SECURITY_GROUPS) && !!SECURITY_GROUPS?.length && (
<Paper
variant="outlined"
sx={{
display: 'flex',
flexBasis: '100%',
flexDirection: 'column',
gap: '0.5em',
p: '0.8em',
}}
>
<Typography variant="body1">
<Translate word={T.SecurityGroups} />
</Typography>
<Stack direction="column" divider={<Divider />} spacing={1}>
{SECURITY_GROUPS?.map((securityGroup, idx) => {
const key = `nic${NIC_ID}-${idx}-${securityGroup.NAME}`
return (
<SecurityGroupCard
key={key}
data-cy={key}
securityGroup={securityGroup}
/>
)
})}
</Stack>
</Paper>
)}
</Paper>
)
}
)
NicCard.propTypes = {
nic: PropTypes.object,
actions: PropTypes.array,
extraActionProps: PropTypes.object,
aliasActions: PropTypes.array,
extraAliasActionProps: PropTypes.object,
}
NicCard.displayName = 'NicCard'
NicCard.displayName = 'NicCard'
export default NicCard

View File

@ -110,7 +110,7 @@ ProvisionCard.propTypes = {
handleClick: PropTypes.func,
isProvider: PropTypes.bool,
image: PropTypes.string,
deleteAction: PropTypes.func,
deleteAction: PropTypes.object,
actions: PropTypes.arrayOf(
PropTypes.shape({
handleClick: PropTypes.func.isRequired,

View File

@ -20,7 +20,6 @@ import { useTheme, Typography, Paper, Stack } from '@mui/material'
import Timer from 'client/components/Timer'
import { StatusChip } from 'client/components/Status'
import { UpdateSchedButton, DeleteSchedButton } from 'client/components/Buttons'
import { rowStyles } from 'client/components/Tables/styles'
import {
@ -32,92 +31,77 @@ import { timeFromMilliseconds } from 'client/models/Helper'
import { sentenceCase } from 'client/utils'
import { T } from 'client/constants'
const ScheduleActionCard = memo(
({ vm, schedule, handleRemove, handleUpdate }) => {
const classes = rowStyles()
const { palette } = useTheme()
const ScheduleActionCard = memo(({ schedule, actions }) => {
const classes = rowStyles()
const { palette } = useTheme()
const { ID, ACTION, TIME, MESSAGE, DONE, WARNING } = schedule
const { ID, ACTION, TIME, MESSAGE, DONE, WARNING } = schedule
const titleAction = `#${ID} ${sentenceCase(ACTION)}`
const timeIsRelative = isRelative(TIME)
const titleAction = `#${ID} ${sentenceCase(ACTION)}`
const timeIsRelative = isRelative(TIME)
const time = timeIsRelative ? getPeriodicityByTimeInSeconds(TIME) : TIME
const formatTime =
!timeIsRelative && timeFromMilliseconds(+TIME).toFormat('ff')
const formatDoneTime = DONE && timeFromMilliseconds(+DONE).toFormat('ff')
const time = timeIsRelative ? getPeriodicityByTimeInSeconds(TIME) : TIME
const formatTime =
!timeIsRelative && timeFromMilliseconds(+TIME).toFormat('ff')
const formatDoneTime = DONE && timeFromMilliseconds(+DONE).toFormat('ff')
const { repeat, end } = getRepeatInformation(schedule)
const { repeat, end } = getRepeatInformation(schedule)
const noMore = !repeat && DONE
// const timeIsPast = new Date(+TIME * 1000) < new Date()
const noMore = !repeat && DONE
return (
<Paper variant="outlined" className={classes.root}>
<div className={classes.main}>
<div className={classes.title}>
<Typography component="span">{titleAction}</Typography>
{MESSAGE && (
<span className={classes.labels}>
<StatusChip text={MESSAGE} />
</span>
)}
</div>
<Stack
mt={0.5}
spacing={2}
alignItems="center"
flexWrap="wrap"
direction="row"
>
{repeat && <Typography variant="caption">{repeat}</Typography>}
{end && <Typography variant="caption">{end}</Typography>}
{DONE && (
<Typography variant="caption" title={formatDoneTime}>
<Timer initial={DONE} translateWord={T.DoneAgo} />
</Typography>
)}
{!noMore && (
<>
<Typography variant="caption">
{timeIsRelative ? (
<span>{Object.values(time).join(' ')}</span>
) : (
<span title={formatTime}>
<Timer initial={TIME} translateWord={T.FirstTime} />
</span>
)}
</Typography>
{WARNING && <WarningIcon color={palette.warning.main} />}
</>
)}
</Stack>
return (
<Paper variant="outlined" className={classes.root}>
<div className={classes.main}>
<div className={classes.title}>
<Typography component="span">{titleAction}</Typography>
{MESSAGE && (
<span className={classes.labels}>
<StatusChip text={MESSAGE} />
</span>
)}
</div>
{(handleUpdate || handleRemove) && (
<div className={classes.actions}>
{!noMore && handleUpdate && (
<UpdateSchedButton
vm={vm}
relative={timeIsRelative}
schedule={schedule}
onSubmit={handleUpdate}
/>
)}
{handleRemove && (
<DeleteSchedButton onSubmit={handleRemove} schedule={schedule} />
)}
</div>
)}
</Paper>
)
}
)
<Stack
mt={0.5}
spacing={2}
alignItems="center"
flexWrap="wrap"
direction="row"
>
{repeat && <Typography variant="caption">{repeat}</Typography>}
{end && <Typography variant="caption">{end}</Typography>}
{DONE && (
<Typography variant="caption" title={formatDoneTime}>
<Timer initial={DONE} translateWord={T.DoneAgo} />
</Typography>
)}
{!noMore && (
<>
<Typography variant="caption">
{timeIsRelative ? (
<span>{Object.values(time).join(' ')}</span>
) : (
<span title={formatTime}>
<Timer initial={TIME} translateWord={T.FirstTime} />
</span>
)}
</Typography>
{WARNING && <WarningIcon color={palette.warning.main} />}
</>
)}
</Stack>
</div>
{actions && (
<div className={classes.actions}>
{typeof actions === 'function' ? actions({ noMore }) : actions}
</div>
)}
</Paper>
)
})
ScheduleActionCard.propTypes = {
vm: PropTypes.object,
schedule: PropTypes.object.isRequired,
handleRemove: PropTypes.func,
handleUpdate: PropTypes.func,
actions: PropTypes.any,
}
ScheduleActionCard.displayName = 'ScheduleActionCard'

View File

@ -16,82 +16,45 @@
import { memo } from 'react'
import PropTypes from 'prop-types'
import { styled, useMediaQuery } from '@mui/material'
import Typography from '@mui/material/Typography'
import { useMediaQuery, Typography } from '@mui/material'
import MultipleTags from 'client/components/MultipleTags'
import { rowStyles } from 'client/components/Tables/styles'
import { SecurityGroup } from 'client/constants'
const DATACY_SECGROUP = 'securitygroup-'
const Row = styled('div')({
display: 'flex',
width: '100%',
gap: '0.5em',
alignItems: 'center',
flexWrap: 'nowrap',
})
const Labels = styled('span')({
display: 'inline-flex',
gap: '0.5em',
alignItems: 'center',
})
const SecGroup = memo(({ index, securityGroup }) => {
const SecurityGroupCard = memo(({ securityGroup, ...props }) => {
const classes = rowStyles()
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('md'))
/** @type {SecurityGroup} */
const { ID, NAME, PROTOCOL, RULE_TYPE, ICMP_TYPE, RANGE, NETWORK_ID } =
securityGroup
const tags = [
{
text: PROTOCOL,
dataCy: `${DATACY_SECGROUP}protocol`,
},
{
text: RULE_TYPE,
dataCy: `${DATACY_SECGROUP}ruletype`,
},
{
text: RANGE,
dataCy: `${DATACY_SECGROUP}range`,
},
{
text: NETWORK_ID,
dataCy: `${DATACY_SECGROUP}networkid`,
},
{
text: ICMP_TYPE,
dataCy: `${DATACY_SECGROUP}icmp_type`,
},
{ text: PROTOCOL, dataCy: 'protocol' },
{ text: RULE_TYPE, dataCy: 'ruletype' },
{ text: RANGE, dataCy: 'range' },
{ text: NETWORK_ID, dataCy: 'networkid' },
{ text: ICMP_TYPE, dataCy: 'icmp-type' },
].filter(({ text } = {}) => Boolean(text))
return (
<Row data-cy={`${DATACY_SECGROUP}${index}`}>
<Typography noWrap variant="body2" data-cy={`${DATACY_SECGROUP}name`}>
<div data-cy={props['data-cy']} className={classes.title}>
<Typography noWrap component="span" data-cy="name" variant="body2">
{`${ID} | ${NAME}`}
</Typography>
<Labels>
<span className={classes.labels}>
<MultipleTags limitTags={isMobile ? 2 : 5} tags={tags} />
</Labels>
</Row>
</span>
</div>
)
})
SecGroup.displayName = 'SecGroup'
SecGroup.propTypes = {
index: PropTypes.number,
securityGroup: PropTypes.shape({
ID: PropTypes.string,
SECURITY_GROUP_ID: PropTypes.string,
NAME: PropTypes.string,
PROTOCOL: PropTypes.string,
RULE_TYPE: PropTypes.string,
ICMP_TYPE: PropTypes.string,
RANGE: PropTypes.string,
NETWORK_ID: PropTypes.string,
}),
SecurityGroupCard.propTypes = {
securityGroup: PropTypes.object,
'data-cy': PropTypes.string,
}
export default SecGroup
SecurityGroupCard.displayName = 'SecurityGroupCard'
export default SecurityGroupCard

View File

@ -13,55 +13,58 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { memo } from 'react'
import PropTypes from 'prop-types'
import { Typography, Paper } from '@mui/material'
import * as Actions from 'client/components/Tabs/Vm/Snapshot/Actions'
import { rowStyles } from 'client/components/Tables/styles'
import * as Helper from 'client/models/Helper'
import { VM_ACTIONS } from 'client/constants'
import { Snapshot } from 'client/constants'
const SnapshotItem = ({ snapshot, actions = [] }) => {
const classes = rowStyles()
const SnapshotCard = memo(
({ snapshot, actions = [], extraActionProps = {} }) => {
const classes = rowStyles()
const { SNAPSHOT_ID, NAME, TIME } = snapshot
/** @type {Snapshot} */
const { SNAPSHOT_ID, NAME, TIME } = snapshot
const time = Helper.timeFromMilliseconds(+TIME)
const timeAgo = `created ${time.toRelative()}`
const time = Helper.timeFromMilliseconds(+TIME)
const timeAgo = `created ${time.toRelative()}`
return (
<Paper variant="outlined" className={classes.root}>
<div className={classes.main}>
<div className={classes.title}>
<Typography component="span">{NAME}</Typography>
return (
<Paper variant="outlined" className={classes.root}>
<div className={classes.main}>
<div className={classes.title}>
<Typography component="span">{NAME}</Typography>
</div>
<div className={classes.caption}>
<span title={time.toFormat('ff')}>
{`#${SNAPSHOT_ID} ${timeAgo}`}
</span>
</div>
</div>
<div className={classes.caption}>
<span title={time.toFormat('ff')}>
{`#${SNAPSHOT_ID} ${timeAgo}`}
</span>
</div>
</div>
{!!actions.length && (
<div className={classes.actions}>
{actions?.includes?.(VM_ACTIONS.SNAPSHOT_REVERT) && (
<Actions.RevertAction snapshot={snapshot} />
)}
{actions?.includes?.(VM_ACTIONS.SNAPSHOT_DELETE) && (
<Actions.DeleteAction snapshot={snapshot} />
)}
</div>
)}
</Paper>
)
}
{!!actions.length && (
<div className={classes.actions}>
{actions.map((Action, idx) => (
<Action
key={`${Action.displayName ?? idx}-${SNAPSHOT_ID}`}
{...extraActionProps}
snapshot={snapshot}
/>
))}
</div>
)}
</Paper>
)
}
)
SnapshotItem.propTypes = {
SnapshotCard.propTypes = {
snapshot: PropTypes.object.isRequired,
actions: PropTypes.arrayOf(PropTypes.string),
actions: PropTypes.array,
extraActionProps: PropTypes.object,
}
SnapshotItem.displayName = 'SnapshotItem'
SnapshotCard.displayName = 'SnapshotCard'
export default SnapshotItem
export default SnapshotCard

View File

@ -13,62 +13,86 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo } from 'react'
import { ReactElement, memo } from 'react'
import PropTypes from 'prop-types'
import { ViewGrid as VmIcon } from 'iconoir-react'
import SelectCard, { Action } from 'client/components/Cards/SelectCard'
import { StatusBadge } from 'client/components/Status'
import { getState } from 'client/models/VirtualMachine'
import { User, Group, Lock, HardDrive } from 'iconoir-react'
import { Stack, Typography } from '@mui/material'
import Timer from 'client/components/Timer'
import MultipleTags from 'client/components/MultipleTags'
import { StatusCircle } from 'client/components/Status'
import { rowStyles } from 'client/components/Tables/styles'
import { getState, getLastHistory } from 'client/models/VirtualMachine'
import { timeFromMilliseconds } from 'client/models/Helper'
import { VM } from 'client/constants'
const VirtualMachineCard = memo(
({ value, isSelected, handleClick, actions }) => {
const { ID, NAME } = value
const { color, name } = getState(value) ?? {}
/**
* @param {object} props - Props
* @param {VM} props.vm - Virtual machine resource
* @param {object} props.rootProps - Props to root component
* @returns {ReactElement} - Card
*/
({ vm, rootProps }) => {
const classes = rowStyles()
const { ID, NAME, UNAME, GNAME, IPS, STIME, ETIME, LOCK } = vm
const HOSTNAME = getLastHistory(vm)?.HOSTNAME ?? '--'
const time = timeFromMilliseconds(+ETIME || +STIME)
const { color: stateColor, name: stateName } = getState(vm)
return (
<SelectCard
action={actions?.map((action) => (
<Action key={action?.cy} {...action} />
))}
skeletonHeight={75}
dataCy={`vm-${ID}`}
handleClick={handleClick}
icon={
<StatusBadge title={name} stateColor={color}>
<VmIcon />
</StatusBadge>
}
isSelected={isSelected}
subheader={`#${ID}`}
title={NAME}
/>
<div {...rootProps} data-cy={`vm-${ID}`}>
<div>
<StatusCircle color={stateColor} tooltip={stateName} />
</div>
<div className={classes.main}>
<div className={classes.title}>
<Typography noWrap component="span">
{NAME}
</Typography>
<span className={classes.labels}>
{LOCK && <Lock data-cy="lock" />}
</span>
</div>
<div className={classes.caption}>
<span title={time.toFormat('ff')}>
{`#${ID} ${+ETIME ? 'done' : 'started'} `}
<Timer initial={time} />
</span>
<span title={`Owner: ${UNAME}`}>
<User />
<span data-cy="uname">{` ${UNAME}`}</span>
</span>
<span title={`Group: ${GNAME}`}>
<Group />
<span data-cy="gname">{` ${GNAME}`}</span>
</span>
<span title={`Hostname: ${HOSTNAME}`}>
<HardDrive />
<span data-cy="hostname">{` ${HOSTNAME}`}</span>
</span>
</div>
</div>
{!!IPS?.length && (
<div className={classes.secondary}>
<Stack flexWrap="wrap" justifyContent="end" alignItems="center">
<MultipleTags tags={IPS.split(',')} />
</Stack>
</div>
)}
</div>
)
},
(prev, next) =>
prev.isSelected === next.isSelected &&
prev.value.STATE === next.value.STATE &&
prev.value?.LCM_STATE === next.value?.LCM_STATE
}
)
VirtualMachineCard.propTypes = {
handleClick: PropTypes.func,
isSelected: PropTypes.bool,
value: PropTypes.object,
actions: PropTypes.arrayOf(
PropTypes.shape({
handleClick: PropTypes.func.isRequired,
icon: PropTypes.object.isRequired,
cy: PropTypes.string,
})
),
}
VirtualMachineCard.defaultProps = {
handleClick: undefined,
isSelected: false,
value: {},
actions: undefined,
vm: PropTypes.object,
rootProps: PropTypes.shape({
className: PropTypes.string,
}),
}
VirtualMachineCard.displayName = 'VirtualMachineCard'

View File

@ -0,0 +1,107 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, 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 { User, Group, Lock } from 'iconoir-react'
import { Typography } from '@mui/material'
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 { timeFromMilliseconds } from 'client/models/Helper'
import { isExternalURL } from 'client/utils'
import { VM, LOGO_IMAGES_URL } 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
* @returns {ReactElement} - Card
*/
({ template, rootProps }) => {
const classes = rowStyles()
const {
ID,
NAME,
UNAME,
GNAME,
REGTIME,
LOCK,
VROUTER,
LOGO = '',
} = template
const [logoSource] = useMemo(() => {
const external = isExternalURL(LOGO)
const cleanLogoAttribute = String(LOGO).split('/').at(-1)
const src = external ? LOGO : `${LOGO_IMAGES_URL}/${cleanLogoAttribute}`
return [src, external]
}, [LOGO])
const time = timeFromMilliseconds(+REGTIME)
return (
<div {...rootProps} data-cy={`template-${ID}`}>
<div className={classes.figure}>
<Image
alt="logo"
src={logoSource}
imgProps={{ className: classes.image }}
/>
</div>
<div className={classes.main}>
<div className={classes.title}>
<Typography component="span">{NAME}</Typography>
<span className={classes.labels}>
{LOCK && <Lock />}
{VROUTER && <StatusChip text={VROUTER} />}
</span>
</div>
<div className={classes.caption}>
<span title={time.toFormat('ff')} className="full-width">
{`#${ID} registered `}
<Timer initial={time} />
</span>
<span title={`Owner: ${UNAME}`}>
<User />
<span>{` ${UNAME}`}</span>
</span>
<span title={`Group: ${GNAME}`}>
<Group />
<span>{` ${GNAME}`}</span>
</span>
</div>
</div>
</div>
)
}
)
VmTemplateCard.propTypes = {
template: PropTypes.object,
rootProps: PropTypes.shape({
className: PropTypes.string,
}),
}
VmTemplateCard.displayName = 'VmTemplateCard'
export default VmTemplateCard

View File

@ -18,16 +18,22 @@ import ApplicationNetworkCard from 'client/components/Cards/ApplicationNetworkCa
import ApplicationTemplateCard from 'client/components/Cards/ApplicationTemplateCard'
import ClusterCard from 'client/components/Cards/ClusterCard'
import DatastoreCard from 'client/components/Cards/DatastoreCard'
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 NetworkCard from 'client/components/Cards/NetworkCard'
import NicCard from 'client/components/Cards/NicCard'
import PolicyCard from 'client/components/Cards/PolicyCard'
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 TierCard from 'client/components/Cards/TierCard'
import VirtualMachineCard from 'client/components/Cards/VirtualMachineCard'
import VmTemplateCard from 'client/components/Cards/VmTemplateCard'
import WavesCard from 'client/components/Cards/WavesCard'
export {
@ -36,15 +42,21 @@ export {
ApplicationTemplateCard,
ClusterCard,
DatastoreCard,
DiskCard,
DiskSnapshotCard,
EmptyCard,
HostCard,
NetworkCard,
NicCard,
PolicyCard,
ProvisionCard,
ProvisionTemplateCard,
ScheduleActionCard,
SecurityGroupCard,
SnapshotCard,
SelectCard,
TierCard,
VirtualMachineCard,
VmTemplateCard,
WavesCard,
}

View File

@ -90,7 +90,7 @@ const CircleChart = memo(
<Typography
variant="h4"
component="div"
style={{ cursor: 'pointer' }}
sx={{ cursor: 'pointer' }}
{...labelProps}
>
<NumberEasing value={label} />

View File

@ -28,6 +28,7 @@ const debugLogStyles = makeStyles((theme) => ({
display: 'flex',
flexFlow: 'column',
height: '100%',
overflow: 'auto',
},
containerScroll: {
width: '100%',

View File

@ -122,7 +122,11 @@ const DialogConfirmation = memo(
)}
</DialogTitle>
{children && (
<DialogContent dividers {...contentProps}>
<DialogContent
dividers
sx={{ display: 'flex', flexDirection: 'column' }}
{...contentProps}
>
{children}
</DialogContent>
)}

View File

@ -13,12 +13,11 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo, useEffect } from 'react'
import { memo } from 'react'
import { styled, Link, Typography } from '@mui/material'
import { useFetch } from 'client/hooks'
import { useSystem, useSystemApi } from 'client/features/One'
import { useGetOneVersionQuery } from 'client/features/OneApi/system'
import { StatusChip } from 'client/components/Status'
import { BY } from 'client/constants'
@ -44,13 +43,7 @@ const HeartIcon = styled('span')(({ theme }) => ({
}))
const Footer = memo(() => {
const { version } = useSystem()
const { getOneVersion } = useSystemApi()
const { fetchRequest } = useFetch(getOneVersion)
useEffect(() => {
!version && fetchRequest()
}, [])
const { data: version } = useGetOneVersionQuery()
return (
<FooterBox>

View File

@ -63,7 +63,7 @@ const ButtonComponent = forwardRef(
)
)
const TooltipComponent = ({ tooltip, tooltipProps, children }) => (
const TooltipComponent = ({ tooltip, tooltipprops, children }) => (
<ConditionalWrap
condition={tooltip && tooltip !== ''}
wrap={(wrapperChildren) => (
@ -71,7 +71,7 @@ const TooltipComponent = ({ tooltip, tooltipProps, children }) => (
arrow
placement="bottom"
title={<Typography variant="subtitle2">{tooltip}</Typography>}
{...tooltipProps}
{...tooltipprops}
>
<span>{wrapperChildren}</span>
</Tooltip>
@ -118,7 +118,7 @@ export const SubmitButtonPropTypes = {
endicon: PropTypes.node,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
tooltip: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
tooltipProps: PropTypes.object,
tooltipprops: PropTypes.object,
isSubmitting: PropTypes.bool,
disabled: PropTypes.bool,
className: PropTypes.string,

View File

@ -23,7 +23,7 @@ import {
NavArrowRight as NextIcon,
} from 'iconoir-react'
import { Translate, labelCanBeTranslated } from 'client/components/HOC'
import { Tr, Translate, labelCanBeTranslated } from 'client/components/HOC'
import { T } from 'client/constants'
const useStyles = makeStyles((theme) => ({
@ -64,11 +64,9 @@ const CustomMobileStepper = ({
</Typography>
{Boolean(errors[id]) && (
<Typography className={classes.error} variant="caption" color="error">
{labelCanBeTranslated(label) ? (
<Translate word={errors[id]?.message} />
) : (
errors[id]?.message
)}
{labelCanBeTranslated(label)
? Tr(errors[id]?.message)
: errors[id]?.message}
</Typography>
)}
</Box>

View File

@ -112,7 +112,7 @@ const CustomStepper = ({
StepIconComponent={StepIconStyled}
error={Boolean(errors[id]?.message)}
>
{labelCanBeTranslated(label) ? <Translate word={label} /> : label}
{labelCanBeTranslated(label) ? Tr(label) : label}
</StepLabel>
</StepButton>
</Step>

View File

@ -22,17 +22,15 @@ import {
} from 'react'
import PropTypes from 'prop-types'
import { sprintf } from 'sprintf-js'
import { BaseSchema } from 'yup'
import { useFormContext } from 'react-hook-form'
import { DevTool } from '@hookform/devtools'
import { useMediaQuery } from '@mui/material'
import { useGeneral } from 'client/features/General'
import CustomMobileStepper from 'client/components/FormStepper/MobileStepper'
import CustomStepper from 'client/components/FormStepper/Stepper'
import SkeletonStepsForm from 'client/components/FormStepper/Skeleton'
import { groupBy, Step, isDevelopment } from 'client/utils'
import { groupBy, Step } from 'client/utils'
import { T } from 'client/constants'
const FIRST_STEP = 0
@ -50,7 +48,6 @@ const FIRST_STEP = 0
const FormStepper = ({ steps = [], schema, onSubmit }) => {
const isMobile = useMediaQuery((theme) => theme.breakpoints.only('xs'))
const {
control,
watch,
reset,
formState: { errors },
@ -82,28 +79,17 @@ const FormStepper = ({ steps = [], schema, onSubmit }) => {
return { id, data: stepData, ...step }
}
const setErrors = ({ inner = [], ...rest } = {}) => {
const setErrors = ({ inner = [], message = { word: 'Error' } } = {}) => {
const errorsByPath = groupBy(inner, 'path') ?? {}
const totalErrors = Object.keys(errorsByPath).length
totalErrors > 0
? setError(stepId, {
type: 'manual',
message: [T.ErrorsOcurred, totalErrors],
})
: setError(stepId, rest)
const translationError =
totalErrors > 0 ? [T.ErrorsOcurred, totalErrors] : Object.values(message)
inner?.forEach(({ path, type, errors: message }) => {
if (isDevelopment()) {
// the package @hookform/devtools requires message as string
const [key, ...values] = [message].flat()
setError(`${stepId}.${path}`, {
type,
message: sprintf(key, ...values),
})
} else {
setError(`${stepId}.${path}`, { type, message })
}
setError(stepId, { type: 'manual', message: translationError })
inner?.forEach(({ path, type, errors: innerMessage }) => {
setError(`${stepId}.${path}`, { type, message: innerMessage })
})
}
@ -202,8 +188,6 @@ const FormStepper = ({ steps = [], schema, onSubmit }) => {
)}
{/* FORM CONTENT */}
{Content && <Content data={formData[stepId]} setFormData={setFormData} />}
{isDevelopment() && <DevTool control={control} placement="top-left" />}
</>
)
}

View File

@ -16,7 +16,8 @@
import { string, boolean, ObjectSchema } from 'yup'
import { makeStyles } from '@mui/styles'
import { useSystem, useDatastore } from 'client/features/One'
import { useGetOneConfigQuery } from 'client/features/OneApi/system'
import { useGetDatastoresQuery } from 'client/features/OneApi/datastore'
import {
ImagesTable,
VmsTable,
@ -134,8 +135,8 @@ const RES_TABLE = {
.default(() => undefined),
grid: { md: 12 },
fieldProps: (type) => {
const { config: oneConfig } = useSystem()
const datastores = useDatastore()
const { data: oneConfig = {} } = useGetOneConfigQuery()
const { data: datastores = [] } = useGetDatastoresQuery()
const classes = useTableStyles()
return {

View File

@ -16,7 +16,7 @@
import PropTypes from 'prop-types'
import { useFormContext } from 'react-hook-form'
import { useSystem } from 'client/features/One'
import { useGetOneConfigQuery } from 'client/features/OneApi/system'
import { MarketplacesTable } from 'client/components/Tables'
import { SCHEMA } from 'client/components/Forms/MarketplaceApp/CreateForm/Steps/MarketplacesTable/schema'
import { Step } from 'client/utils'
@ -27,7 +27,7 @@ export const STEP_ID = 'marketplace'
const Content = ({ data }) => {
const { NAME } = data?.[0] ?? {}
const { setValue } = useFormContext()
const { config: oneConfig } = useSystem()
const { data: oneConfig = {} } = useGetOneConfigQuery()
const handleSelectedRows = (rows) => {
const { original = {} } = rows?.[0] ?? {}

View File

@ -13,13 +13,12 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useEffect, useMemo, JSXElementConstructor } from 'react'
import { useMemo, JSXElementConstructor } from 'react'
import PropTypes from 'prop-types'
import { useForm, FormProvider } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import { useDatastoreApi } from 'client/features/One'
import FormStepper from 'client/components/FormStepper'
import Steps from 'client/components/Forms/MarketplaceApp/CreateForm/Steps'
@ -32,7 +31,6 @@ import Steps from 'client/components/Forms/MarketplaceApp/CreateForm/Steps'
* @returns {JSXElementConstructor} Form component
*/
const CreateForm = ({ initialValues, onSubmit }) => {
const { getDatastores } = useDatastoreApi()
const stepsForm = useMemo(() => Steps(initialValues, initialValues), [])
const { steps, defaultValues, resolver, transformBeforeSubmit } = stepsForm
@ -42,10 +40,6 @@ const CreateForm = ({ initialValues, onSubmit }) => {
resolver: yupResolver(resolver?.()),
})
useEffect(() => {
getDatastores()
}, [])
return (
<FormProvider {...methods}>
<FormStepper

View File

@ -13,33 +13,37 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import * as yup from 'yup'
import { INPUT_TYPES } from 'client/constants'
import { getValidationFromFields } from 'client/utils'
import { string, object, ObjectSchema } from 'yup'
import { INPUT_TYPES, T } from 'client/constants'
import { Field, getValidationFromFields } from 'client/utils'
const NAME = {
name: 'name',
label: 'Name',
label: T.Name,
type: INPUT_TYPES.TEXT,
validation: yup
.string()
.min(1, 'Name field is required')
.trim()
.required('Name field is required')
.default(''),
validation: string().min(1).trim().required().default(''),
}
const DESCRIPTION = {
name: 'description',
label: 'Description',
label: T.Description,
type: INPUT_TYPES.TEXT,
multiline: true,
validation: yup.string().trim().default(''),
validation: string().trim().default(''),
}
/**
* @param {object} config - Form configuration
* @param {boolean} [config.isUpdate] - Form is updating the provider
* @returns {Field[]} - List of fields
*/
export const FORM_FIELDS = ({ isUpdate }) =>
[!isUpdate && NAME, DESCRIPTION].filter(Boolean)
/**
* @param {object} config - Form configuration
* @param {boolean} [config.isUpdate] - Form is updating the provider
* @returns {ObjectSchema} - Schema
*/
export const STEP_FORM_SCHEMA = ({ isUpdate }) =>
yup.object(getValidationFromFields(FORM_FIELDS({ isUpdate })))
object(getValidationFromFields(FORM_FIELDS({ isUpdate })))

View File

@ -18,7 +18,7 @@ import { useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import { useFormContext } from 'react-hook-form'
import { useAuth } from 'client/features/Auth'
import { useGetProviderConfigQuery } from 'client/features/OneApi/provider'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { EmptyCard } from 'client/components/Cards'
@ -39,7 +39,7 @@ let fileCredentials = false
const Content = ({ isUpdate }) => {
const [fields, setFields] = useState([])
const { providerConfig } = useAuth()
const { data: providerConfig } = useGetProviderConfigQuery()
const { watch } = useFormContext()
useEffect(() => {

View File

@ -13,21 +13,26 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import * as yup from 'yup'
import { string, object, ObjectSchema } from 'yup'
import { INPUT_TYPES } from 'client/constants'
import { getValidationFromFields, prettyBytes } from 'client/utils'
import { Field, getValidationFromFields, prettyBytes } from 'client/utils'
const MAX_SIZE_JSON = 102_400
const JSON_FORMAT = 'application/json'
const CREDENTIAL_INPUT = 'credentials'
/**
* @param {object} config - Form configuration
* @param {object} config.connection - Provider connection
* @param {boolean} config.fileCredentials - Provider needs file with credentials
* @returns {Field[]} - List of fields
*/
export const FORM_FIELDS = ({ connection, fileCredentials }) =>
Object.entries(connection)?.map(([name, label]) => {
const isInputFile =
fileCredentials && String(name).toLowerCase() === CREDENTIAL_INPUT
let validation = yup.string().trim().required().default(undefined)
let validation = string().trim().required().default(undefined)
if (isInputFile) {
validation = validation.isBase64()
@ -62,5 +67,9 @@ export const FORM_FIELDS = ({ connection, fileCredentials }) =>
}
})
export const STEP_FORM_SCHEMA = (props) =>
yup.object(getValidationFromFields(FORM_FIELDS(props)))
/**
* @param {object} config - Form configuration
* @returns {ObjectSchema} - Schema
*/
export const STEP_FORM_SCHEMA = (config) =>
object(getValidationFromFields(FORM_FIELDS(config)))

View File

@ -27,8 +27,8 @@ import { NavArrowRight } from 'iconoir-react'
import { marked } from 'marked'
import { useListForm } from 'client/hooks'
import { useAuth } from 'client/features/Auth'
import { useProvisionTemplate } from 'client/features/One'
import { useGetProviderConfigQuery } from 'client/features/OneApi/provider'
import { useGetProvisionTemplatesQuery } from 'client/features/OneApi/provision'
import { ListCards } from 'client/components/List'
import { ProvisionTemplateCard } from 'client/components/Cards'
import { Step, sanitize, deepmerge } from 'client/utils'
@ -76,8 +76,8 @@ Description.propTypes = { description: PropTypes.string }
// ----------------------------------------------------------
const Content = ({ data, setFormData }) => {
const provisionTemplates = useProvisionTemplate()
const { providerConfig } = useAuth()
const { data: provisionTemplates = {} } = useGetProvisionTemplatesQuery()
const { data: providerConfig = {} } = useGetProviderConfigQuery()
const templateSelected = data?.[0]
const provisionTypes = useMemo(
@ -174,7 +174,7 @@ const Content = ({ data, setFormData }) => {
inputProps={{ 'data-cy': 'select-provision-type' }}
labelId="select-provision-type-label"
native
style={{ marginTop: '1em', minWidth: '8em' }}
sx={{ marginTop: '1em', minWidth: '8em' }}
onChange={handleChangeProvision}
value={provisionSelected}
variant="outlined"
@ -195,7 +195,7 @@ const Content = ({ data, setFormData }) => {
inputProps={{ 'data-cy': 'select-provider-type' }}
labelId="select-provider-type-label"
native
style={{ marginTop: '1em', minWidth: '8em' }}
sx={{ marginTop: '1em', minWidth: '8em' }}
onChange={handleChangeProvider}
value={providerSelected}
variant="outlined"
@ -212,7 +212,7 @@ const Content = ({ data, setFormData }) => {
{/* -- DESCRIPTION -- */}
{providerDescription && <Description description={providerDescription} />}
<Divider style={{ margin: '1rem 0' }} />
<Divider sx={{ margin: '1rem 0' }} />
{/* -- LIST -- */}
<ListCards

View File

@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import * as yup from 'yup'
import { array, object, ArraySchema } from 'yup'
export const STEP_FORM_SCHEMA = yup
.array(yup.object())
.min(1, 'Select provider template')
.max(1, 'Max. one template selected')
.required('Provider template field is required')
/** @type {ArraySchema} - Schema */
export const STEP_FORM_SCHEMA = array(object())
.min(1)
.max(1)
.required()
.default([])

View File

@ -21,9 +21,11 @@ import { Redirect } from 'react-router'
import { useForm, FormProvider } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import { useFetchAll } from 'client/hooks'
import { useAuth } from 'client/features/Auth'
import { useProviderApi } from 'client/features/One'
import {
useGetProviderConfigQuery,
useLazyGetProviderConnectionQuery,
useLazyGetProviderQuery,
} from 'client/features/OneApi/provider'
import FormStepper, { SkeletonStepsForm } from 'client/components/FormStepper'
import Steps from 'client/components/Forms/Provider/CreateForm/Steps'
import { PATH } from 'client/apps/provision/routes'
@ -55,29 +57,28 @@ const CreateForm = ({ provider, providerConfig, connection, onSubmit }) => {
}
const PreFetchingForm = ({ providerId, onSubmit }) => {
const { providerConfig } = useAuth()
const { getProvider, getProviderConnection } = useProviderApi()
const { data, fetchRequestAll, error } = useFetchAll()
const [provider, connection] = data ?? []
const { data: config, error: errorConfig } = useGetProviderConfigQuery()
const [getConnection, { data: connection, error: errorConnection }] =
useLazyGetProviderConnectionQuery()
const [getProvider, { data: provider, error: errorProvider }] =
useLazyGetProviderQuery()
useEffect(() => {
providerId &&
fetchRequestAll([
getProvider(providerId),
getProviderConnection(providerId),
])
providerId && getProvider(providerId)
providerId && getConnection(providerId)
}, [])
if (error) {
if (errorConfig || errorConnection || errorProvider) {
return <Redirect to={PATH.PROVIDERS.LIST} />
}
return providerId && !data ? (
return providerId && (!config || !connection || !provider) ? (
<SkeletonStepsForm />
) : (
<CreateForm
provider={provider}
providerConfig={providerConfig}
providerConfig={config}
connection={connection}
onSubmit={onSubmit}
/>

View File

@ -14,8 +14,6 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useCallback } from 'react'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { T } from 'client/constants'
@ -31,11 +29,8 @@ const BasicConfiguration = () => ({
label: T.ProvisionOverview,
resolver: () => STEP_FORM_SCHEMA,
optionsValidate: { abortEarly: false },
content: useCallback(
() => (
<FormWithSchema cy="form-provision" fields={FORM_FIELDS} id={STEP_ID} />
),
[]
content: () => (
<FormWithSchema cy="form-provision" fields={FORM_FIELDS} id={STEP_ID} />
),
})

View File

@ -13,30 +13,25 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import * as yup from 'yup'
import { INPUT_TYPES } from 'client/constants'
import { string, object } from 'yup'
import { T, INPUT_TYPES } from 'client/constants'
import { getValidationFromFields } from 'client/utils'
const NAME = {
name: 'name',
label: 'Name',
label: T.Name,
type: INPUT_TYPES.TEXT,
validation: yup
.string()
.min(1, 'Name field is required')
.trim()
.required('Name field is required')
.default(''),
validation: string().min(1).trim().required().default(''),
}
const DESCRIPTION = {
name: 'description',
label: 'Description',
label: T.Description,
type: INPUT_TYPES.TEXT,
multiline: true,
validation: yup.string().trim().default(''),
validation: string().trim().default(''),
}
export const FORM_FIELDS = [NAME, DESCRIPTION]
export const STEP_FORM_SCHEMA = yup.object(getValidationFromFields(FORM_FIELDS))
export const STEP_FORM_SCHEMA = object(getValidationFromFields(FORM_FIELDS))

View File

@ -14,14 +14,13 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useCallback, useEffect, useState } from 'react'
import { useEffect, useState } from 'react'
import { useFormContext } from 'react-hook-form'
import { LinearProgress } from '@mui/material'
import { useFetch } from 'client/hooks'
import { useGeneralApi } from 'client/features/General'
import { useProviderApi } from 'client/features/One'
import { useLazyGetProviderQuery } from 'client/features/OneApi/provider'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { EmptyCard } from 'client/components/Cards'
import { T } from 'client/constants'
@ -43,12 +42,11 @@ const Inputs = () => ({
label: T.ConfigureInputs,
resolver: () => STEP_FORM_SCHEMA(inputs),
optionsValidate: { abortEarly: false },
content: useCallback(() => {
content: () => {
const [fields, setFields] = useState(undefined)
const { changeLoading } = useGeneralApi()
const { getProvider } = useProviderApi()
const { data: fetchData, fetchRequest } = useFetch(getProvider)
const { watch, reset } = useFormContext()
const { changeLoading } = useGeneralApi()
const [getProvider, { data: fetchData }] = useLazyGetProviderQuery()
useEffect(() => {
const { [PROVIDER_ID]: providerSelected = [], [STEP_ID]: currentInputs } =
@ -56,7 +54,7 @@ const Inputs = () => ({
if (!currentInputs) {
changeLoading(true) // disable finish button until provider is fetched
fetchRequest(providerSelected[0]?.ID)
getProvider(providerSelected[0]?.ID)
} else {
setFields(FORM_FIELDS(inputs))
}
@ -92,7 +90,7 @@ const Inputs = () => ({
) : (
<FormWithSchema cy="form-provision" fields={fields} id={STEP_ID} />
)
}, []),
},
})
export default Inputs

View File

@ -14,12 +14,14 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useCallback, useMemo } from 'react'
import { useMemo } from 'react'
import { useWatch } from 'react-hook-form'
import { useListForm } from 'client/hooks'
import { useAuth } from 'client/features/Auth'
import { useProvider } from 'client/features/One'
import {
useGetProvidersQuery,
useGetProviderConfigQuery,
} from 'client/features/OneApi/provider'
import { ListCards } from 'client/components/List'
import { EmptyCard, ProvisionCard } from 'client/components/Cards'
import { T } from 'client/constants'
@ -34,9 +36,9 @@ const Provider = () => ({
id: STEP_ID,
label: T.Provider,
resolver: () => STEP_FORM_SCHEMA,
content: useCallback(({ data, setFormData }) => {
const providers = useProvider()
const { providerConfig } = useAuth()
content: ({ data, setFormData }) => {
const { data: providers } = useGetProvidersQuery()
const { data: providerConfig } = useGetProviderConfigQuery()
const provisionTemplateSelected = useWatch({ name: TEMPLATE_ID })?.[0] ?? {}
@ -87,7 +89,7 @@ const Provider = () => ({
}}
/>
)
}, []),
},
})
export default Provider

View File

@ -13,11 +13,10 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import * as yup from 'yup'
import { array, object } from 'yup'
export const STEP_FORM_SCHEMA = yup
.array(yup.object())
.min(1, 'Select provider')
.max(1, 'Max. one provider selected')
.required('Provider field is required')
export const STEP_FORM_SCHEMA = array(object())
.min(1)
.max(1)
.required()
.default([])

View File

@ -26,8 +26,11 @@ import { NavArrowRight } from 'iconoir-react'
import { marked } from 'marked'
import { useListForm } from 'client/hooks'
import { useAuth } from 'client/features/Auth'
import { useProvider, useProvisionTemplate } from 'client/features/One'
import {
useGetProvidersQuery,
useGetProviderConfigQuery,
} from 'client/features/OneApi/provider'
import { useGetProvisionTemplatesQuery } from 'client/features/OneApi/provision'
import { ListCards } from 'client/components/List'
import { ProvisionTemplateCard } from 'client/components/Cards'
import { Step, sanitize } from 'client/utils'
@ -75,9 +78,9 @@ Description.propTypes = { description: PropTypes.string }
// ----------------------------------------------------------
const Content = ({ data, setFormData }) => {
const providers = useProvider()
const provisionTemplates = useProvisionTemplate()
const { providerConfig } = useAuth()
const { data: provisionTemplates } = useGetProvisionTemplatesQuery()
const { data: providers } = useGetProvidersQuery()
const { data: providerConfig } = useGetProviderConfigQuery()
const templateSelected = data?.[0]
const provisionTypes = useMemo(
@ -177,7 +180,7 @@ const Content = ({ data, setFormData }) => {
inputProps={{ 'data-cy': 'select-provision-type' }}
labelId="select-provision-type-label"
native
style={{ marginTop: '1em', minWidth: '8em' }}
sx={{ marginTop: '1em', minWidth: '8em' }}
onChange={handleChangeProvision}
value={provisionSelected}
variant="outlined"
@ -198,7 +201,7 @@ const Content = ({ data, setFormData }) => {
inputProps={{ 'data-cy': 'select-provider-type' }}
labelId="select-provider-type-label"
native
style={{ marginTop: '1em', minWidth: '8em' }}
sx={{ marginTop: '1em', minWidth: '8em' }}
onChange={handleChangeProvider}
value={providerSelected}
variant="outlined"
@ -221,7 +224,7 @@ const Content = ({ data, setFormData }) => {
[providerDescription]
)}
<Divider style={{ margin: '1rem 0' }} />
<Divider sx={{ margin: '1rem 0' }} />
{/* -- LIST -- */}
<ListCards

View File

@ -13,11 +13,10 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import * as yup from 'yup'
import { array, object } from 'yup'
export const STEP_FORM_SCHEMA = yup
.array(yup.object())
.min(1, 'Select provision template')
.max(1, 'Max. one template selected')
.required('Provision template field is required')
export const STEP_FORM_SCHEMA = array(object())
.min(1)
.max(1)
.required()
.default([])

View File

@ -14,6 +14,7 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo } from 'react'
import PropTypes from 'prop-types'
import { useForm, FormProvider } from 'react-hook-form'
@ -23,7 +24,8 @@ import FormStepper from 'client/components/FormStepper'
import Steps from 'client/components/Forms/Provision/CreateForm/Steps'
const CreateForm = ({ onSubmit }) => {
const { steps, defaultValues, resolver, transformBeforeSubmit } = Steps()
const stepsForm = useMemo(() => Steps(), [])
const { steps, defaultValues, resolver, transformBeforeSubmit } = stepsForm
const methods = useForm({
mode: 'onSubmit',

View File

@ -44,7 +44,6 @@ const Content = ({ data, setFormData }) => {
onlyGlobalSelectedRows
initialState={{ selectedRowIds: { [ID]: true } }}
onSelectedRowsChange={handleSelectedRows}
searchProps={{ 'data-cy': 'search-images' }}
/>
)
}

View File

@ -24,7 +24,7 @@ const GROUP = {
name: 'group',
label: 'Select the new group',
type: INPUT_TYPES.TABLE,
Table: GroupsTable,
Table: () => GroupsTable,
validation: string()
.trim()
.required('You must select a group')

View File

@ -24,7 +24,7 @@ const USER = {
name: 'user',
label: 'Select the new owner',
type: INPUT_TYPES.TABLE,
Table: UsersTable,
Table: () => UsersTable,
validation: string()
.trim()
.required('You must select an user')

View File

@ -21,15 +21,15 @@ import {
INPUT_TYPES,
VM_ACTIONS_IN_CHARTER,
VM_ACTIONS_WITH_SCHEDULE,
END_TYPE_VALUES,
REPEAT_VALUES,
ARGS_TYPES,
PERIOD_TYPES,
} from 'client/constants'
import { Field, sentenceCase, arrayToOptions } from 'client/utils'
import {
isRelative,
END_TYPE_VALUES,
REPEAT_VALUES,
ARGS_TYPES,
getRequiredArgsByAction,
PERIOD_TYPES,
getPeriodicityByTimeInSeconds,
} from 'client/models/Scheduler'
import {

View File

@ -15,12 +15,13 @@
* ------------------------------------------------------------------------- */
import { string, object, ObjectSchema } from 'yup'
import { ARGS_TYPES, getRequiredArgsByAction } from 'client/models/Scheduler'
import { getRequiredArgsByAction } from 'client/models/Scheduler'
import { Field, getObjectSchemaFromFields } from 'client/utils'
import {
PUNCTUAL_FIELDS,
RELATIVE_FIELDS,
} from 'client/components/Forms/Vm/CreateSchedActionForm/fields'
import { ARGS_TYPES } from 'client/constants'
const { ACTION_FIELD } = PUNCTUAL_FIELDS

View File

@ -35,7 +35,7 @@ const DATASTORE = {
name: 'datastore',
label: 'Select the new datastore',
type: INPUT_TYPES.TABLE,
Table: DatastoresTable,
Table: () => DatastoresTable,
validation: string()
.trim()
.notRequired()

View File

@ -15,7 +15,7 @@
* ------------------------------------------------------------------------- */
import { string, boolean } from 'yup'
import { useHost } from 'client/features/One'
import { useGetHostsQuery } from 'client/features/OneApi/host'
import { getKvmMachines, getKvmCpuModels } from 'client/models/Host'
import { Field, arrayToOptions } from 'client/utils'
import {
@ -64,7 +64,7 @@ export const MACHINE_TYPES = {
notOnHypervisors: [vcenter, firecracker, lxc],
type: INPUT_TYPES.SELECT,
values: () => {
const hosts = useHost()
const { data: hosts = [] } = useGetHostsQuery()
const kvmMachines = getKvmMachines(hosts)
return arrayToOptions(kvmMachines)
@ -82,7 +82,7 @@ export const CPU_MODEL = {
notOnHypervisors: [vcenter, firecracker, lxc],
type: INPUT_TYPES.SELECT,
values: () => {
const hosts = useHost()
const { data: hosts = [] } = useGetHostsQuery()
const kvmCpuModels = getKvmCpuModels(hosts)
return arrayToOptions(kvmCpuModels)

View File

@ -15,7 +15,7 @@
* ------------------------------------------------------------------------- */
import { boolean, string } from 'yup'
import { useImage } from 'client/features/One'
import { useGetImagesQuery } from 'client/features/OneApi/image'
import { getType } from 'client/models/Image'
import { Field, clearNames } from 'client/utils'
import { T, INPUT_TYPES, HYPERVISORS, IMAGE_TYPES_STR } from 'client/constants'
@ -47,7 +47,7 @@ export const KERNEL_DS = {
dependOf: KERNEL_PATH_ENABLED.name,
htmlType: (enabled) => enabled && INPUT_TYPES.HIDDEN,
values: () => {
const images = useImage()
const { data: images = [] } = useGetImagesQuery()
return images
?.filter((image) => getType(image) === IMAGE_TYPES_STR.KERNEL)

View File

@ -15,7 +15,7 @@
* ------------------------------------------------------------------------- */
import { string, boolean } from 'yup'
import { useImage } from 'client/features/One'
import { useGetImagesQuery } from 'client/features/OneApi/image'
import { getType } from 'client/models/Image'
import { Field, clearNames } from 'client/utils'
import { T, INPUT_TYPES, HYPERVISORS, IMAGE_TYPES_STR } from 'client/constants'
@ -47,7 +47,7 @@ export const RAMDISK_DS = {
dependOf: RAMDISK_PATH_ENABLED.name,
htmlType: (enabled) => enabled && INPUT_TYPES.HIDDEN,
values: () => {
const images = useImage()
const { data: images = [] } = useGetImagesQuery()
return images
?.filter((image) => getType(image) === IMAGE_TYPES_STR.RAMDISK)

View File

@ -15,7 +15,7 @@
* ------------------------------------------------------------------------- */
import { object, string, array, ObjectSchema } from 'yup'
import { useHost } from 'client/features/One'
import { useGetHostsQuery } from 'client/features/OneApi/host'
import { getPciDevices } from 'client/models/Host'
import {
Field,
@ -46,7 +46,7 @@ const NAME_FIELD = {
notOnHypervisors: [vcenter, lxc, firecracker],
type: INPUT_TYPES.SELECT,
values: () => {
const hosts = useHost()
const { data: hosts = [] } = useGetHostsQuery()
const pciDevices = hosts.map(getPciDevices).flat()
return arrayToOptions(pciDevices, {

View File

@ -23,7 +23,7 @@ import { DeleteCircledOutline, AddCircledOutline } from 'iconoir-react'
import { useFieldArray, useForm, FormProvider } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import { useHost } from 'client/features/One'
import { useGetHostsQuery } from 'client/features/OneApi/host'
import { FormWithSchema, Legend } from 'client/components/Forms'
import { Translate } from 'client/components/HOC'
import { getPciDevices } from 'client/models/Host'
@ -40,7 +40,7 @@ export const SECTION_ID = 'PCI'
* @returns {JSXElementConstructor} - Inputs section
*/
const PciDevicesSection = ({ fields }) => {
const hosts = useHost()
const { data: hosts = [] } = useGetHostsQuery()
const pciDevicesAvailable = useMemo(
() => hosts.map(getPciDevices).flat(),
[hosts.length]

View File

@ -15,7 +15,7 @@
* ------------------------------------------------------------------------- */
import { string, number } from 'yup'
import { useHost } from 'client/features/One'
import { useGetHostsQuery } from 'client/features/OneApi/host'
import { getHugepageSizes } from 'client/models/Host'
import {
T,
@ -144,7 +144,7 @@ const HUGEPAGES = {
notOnHypervisors: [vcenter, firecracker],
type: INPUT_TYPES.SELECT,
values: () => {
const hosts = useHost()
const { data: hosts = [] } = useGetHostsQuery()
const sizes = hosts
.reduce((res, host) => res.concat(getHugepageSizes(host)), [])
.flat()

View File

@ -18,7 +18,12 @@ import { Calendar as ActionIcon } from 'iconoir-react'
import { useFieldArray } from 'react-hook-form'
import { ScheduleActionCard } from 'client/components/Cards'
import { CreateSchedButton, CharterButton } from 'client/components/Buttons'
import {
CreateSchedButton,
CharterButton,
UpdateSchedButton,
DeleteSchedButton,
} from 'client/components/Buttons/ScheduleAction'
import {
STEP_ID as EXTRA_ID,
@ -54,11 +59,11 @@ const ScheduleAction = () => {
append(mappedActions)
}
const handleUpdateAction = (action, index) => {
const handleUpdate = (action, index) => {
update(index, mapNameFunction(action, index))
}
const handleRemoveAction = (index) => {
const handleRemove = (index) => {
remove(index)
}
@ -78,14 +83,26 @@ const ScheduleAction = () => {
>
{scheduleActions?.map((schedule, index) => {
const { ID, NAME } = schedule
const fakeValues = { ...schedule, ID: index }
return (
<ScheduleActionCard
key={ID ?? NAME}
relative
schedule={{ ...schedule, ID: index }}
handleUpdate={(newAction) => handleUpdateAction(newAction, index)}
handleRemove={() => handleRemoveAction(index)}
schedule={fakeValues}
actions={
<>
<UpdateSchedButton
relative
vm={{}}
schedule={fakeValues}
onSubmit={(newAction) => handleUpdate(newAction, index)}
/>
<DeleteSchedButton
schedule={fakeValues}
onSubmit={() => handleRemove(index)}
/>
</>
}
/>
)
})}
@ -97,7 +114,7 @@ const ScheduleAction = () => {
/** @type {TabType} */
const TAB = {
id: 'sched_action',
name: T.ScheduledAction,
name: T.ScheduleAction,
icon: ActionIcon,
Content: ScheduleAction,
getError: (error) => !!error?.[TAB_ID],

View File

@ -15,7 +15,7 @@
* ------------------------------------------------------------------------- */
import { object, string, array, ObjectSchema } from 'yup'
import { useDatastore } from 'client/features/One'
import { useGetDatastoresQuery } from 'client/features/OneApi/datastore'
import { getDeployMode } from 'client/models/Datastore'
import { T, INPUT_TYPES } from 'client/constants'
import { Field, arrayToOptions, getValidationFromFields } from 'client/utils'
@ -28,8 +28,8 @@ const TM_MAD_SYSTEM = {
tooltip: T.DeployModeConcept,
type: INPUT_TYPES.SELECT,
values: () => {
const datastores = useDatastore()
const modes = datastores?.map(getDeployMode)?.flat()
const { data: datastores = [] } = useGetDatastoresQuery()
const modes = datastores.map(getDeployMode)?.flat()
return arrayToOptions([...new Set(modes)], { addEmpty: 'Default' })
},

View File

@ -15,7 +15,8 @@
* ------------------------------------------------------------------------- */
import { string } from 'yup'
import { useGroup, useUser } from 'client/features/One'
import { useGetUsersQuery } from 'client/features/OneApi/user'
import { useGetGroupsQuery } from 'client/features/OneApi/group'
import { T, INPUT_TYPES } from 'client/constants'
import { Field } from 'client/utils'
@ -25,7 +26,7 @@ export const UID_FIELD = {
label: T.InstantiateAsUser,
type: INPUT_TYPES.AUTOCOMPLETE,
values: () => {
const users = useUser()
const { data: users = [] } = useGetUsersQuery()
return users
.map(({ ID: value, NAME: text }) => ({ text, value }))
@ -41,7 +42,7 @@ export const GID_FIELD = {
label: T.InstantiateAsGroup,
type: INPUT_TYPES.AUTOCOMPLETE,
values: () => {
const groups = useGroup()
const { data: groups = [] } = useGetGroupsQuery()
return groups
.map(({ ID: value, NAME: text }) => ({ text, value }))

View File

@ -15,7 +15,7 @@
* ------------------------------------------------------------------------- */
import { string } from 'yup'
import { useVmGroup } from 'client/features/One'
import { useGetVMGroupsQuery } from 'client/features/OneApi/vmGroup'
import { T, INPUT_TYPES } from 'client/constants'
import { Field } from 'client/utils'
@ -25,7 +25,7 @@ export const VM_GROUP_FIELD = {
label: T.AssociateToVMGroup,
type: INPUT_TYPES.AUTOCOMPLETE,
values: () => {
const vmGroups = useVmGroup()
const { data: vmGroups = [] } = useGetVMGroupsQuery()
return vmGroups
?.map(({ ID, NAME }) => ({ text: `#${ID} ${NAME}`, value: String(ID) }))
@ -51,7 +51,7 @@ export const ROLE_FIELD = {
htmlType: (vmGroup) =>
vmGroup && vmGroup !== '' ? undefined : INPUT_TYPES.HIDDEN,
values: (vmGroupSelected) => {
const vmGroups = useVmGroup()
const { data: vmGroups = [] } = useGetVMGroupsQuery()
const roles = vmGroups
?.filter(({ ID }) => ID === vmGroupSelected)

View File

@ -20,16 +20,13 @@ import PropTypes from 'prop-types'
import { useForm, FormProvider } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import {
useUserApi,
useVmGroupApi,
useVmTemplateApi,
useHostApi,
useImageApi,
useDatastoreApi,
} from 'client/features/One'
import { useGetVMGroupsQuery } from 'client/features/OneApi/vmGroup'
import { useGetHostsQuery } from 'client/features/OneApi/host'
import { useGetImagesQuery } from 'client/features/OneApi/image'
import { useGetUsersQuery } from 'client/features/OneApi/user'
import { useGetDatastoresQuery } from 'client/features/OneApi/datastore'
import { useLazyGetTemplateQuery } from 'client/features/OneApi/vmTemplate'
import { useFetch } from 'client/hooks'
import FormStepper, { SkeletonStepsForm } from 'client/components/FormStepper'
import Steps from 'client/components/Forms/VmTemplate/CreateForm/Steps'
@ -55,24 +52,15 @@ const CreateForm = ({ template, onSubmit }) => {
}
const PreFetchingForm = ({ templateId, onSubmit }) => {
const { getUsers } = useUserApi()
const { getVmGroups } = useVmGroupApi()
const { getHosts } = useHostApi()
const { getImages } = useImageApi()
const { getDatastores } = useDatastoreApi()
const { getVmTemplate } = useVmTemplateApi()
const { fetchRequest, data } = useFetch(() =>
getVmTemplate(templateId, { extended: true })
)
useGetVMGroupsQuery()
useGetHostsQuery()
useGetImagesQuery()
useGetUsersQuery()
useGetDatastoresQuery()
const [getTemplate, { data }] = useLazyGetTemplateQuery()
useEffect(() => {
templateId && fetchRequest()
getHosts()
getImages()
getDatastores()
getUsers()
getVmGroups()
templateId && getTemplate({ id: templateId, extended: true })
}, [])
return templateId && !data ? (

View File

@ -16,7 +16,8 @@
/* eslint-disable jsdoc/require-jsdoc */
import { string } from 'yup'
import { useGroup, useUser } from 'client/features/One'
import { useGetUsersQuery } from 'client/features/OneApi/user'
import { useGetGroupsQuery } from 'client/features/OneApi/group'
import { INPUT_TYPES } from 'client/constants'
export const UID_FIELD = {
@ -24,7 +25,7 @@ export const UID_FIELD = {
label: 'Instantiate as different User',
type: INPUT_TYPES.AUTOCOMPLETE,
values: () => {
const users = useUser()
const { data: users = [] } = useGetUsersQuery()
return users
.map(({ ID: value, NAME: text }) => ({ text, value }))
@ -39,7 +40,7 @@ export const GID_FIELD = {
label: 'Instantiate as different Group',
type: INPUT_TYPES.AUTOCOMPLETE,
values: () => {
const groups = useGroup()
const { data: groups = [] } = useGetGroupsQuery()
return groups
.map(({ ID: value, NAME: text }) => ({ text, value }))

View File

@ -16,7 +16,7 @@
/* eslint-disable jsdoc/require-jsdoc */
import { string } from 'yup'
import { useVmGroup } from 'client/features/One'
import { useGetVMGroupsQuery } from 'client/features/OneApi/vmGroup'
import { INPUT_TYPES } from 'client/constants'
export const VM_GROUP_FIELD = {
@ -24,7 +24,7 @@ export const VM_GROUP_FIELD = {
label: 'Associate VM to a VM Group',
type: INPUT_TYPES.AUTOCOMPLETE,
values: () => {
const vmGroups = useVmGroup()
const { data: vmGroups = [] } = useGetVMGroupsQuery()
return vmGroups
?.map(({ ID, NAME }) => ({ text: `#${ID} ${NAME}`, value: String(ID) }))
@ -49,7 +49,7 @@ export const ROLE_FIELD = {
htmlType: (vmGroup) =>
vmGroup && vmGroup !== '' ? undefined : INPUT_TYPES.HIDDEN,
values: (vmGroupSelected) => {
const vmGroups = useVmGroup()
const { data: vmGroups = [] } = useGetVMGroupsQuery()
const roles = vmGroups
?.filter(({ ID }) => ID === vmGroupSelected)

View File

@ -18,7 +18,7 @@ import PropTypes from 'prop-types'
import makeStyles from '@mui/styles/makeStyles'
import { useListForm } from 'client/hooks'
import { useVmTemplateApi } from 'client/features/One'
import { useLazyGetTemplateQuery } from 'client/features/OneApi/vmTemplate'
import { VmTemplatesTable } from 'client/components/Tables'
import { SCHEMA } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/VmTemplatesTable/schema'
@ -39,7 +39,7 @@ const useStyles = makeStyles({
const Content = ({ data, setFormData }) => {
const classes = useStyles()
const selectedTemplate = data?.[0]
const { getVmTemplate } = useVmTemplateApi()
const [getVmTemplate] = useLazyGetTemplateQuery()
const { handleSelect, handleClear } = useListForm({
key: STEP_ID,
@ -48,12 +48,12 @@ const Content = ({ data, setFormData }) => {
const handleSelectedRows = async (rows) => {
const { original: templateSelected } = rows?.[0] ?? {}
const { ID } = templateSelected ?? {}
const { ID: id } = templateSelected ?? {}
if (!ID) return handleClear()
if (!id) return handleClear()
const extendedTemplate = ID
? await getVmTemplate(ID, { extended: true })
const extendedTemplate = id
? await getVmTemplate({ id, extended: true }).unwrap()
: {}
setFormData((prev) => ({

View File

@ -20,12 +20,9 @@ import PropTypes from 'prop-types'
import { useForm, FormProvider } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import { useFetch } from 'client/hooks'
import {
useUserApi,
useVmGroupApi,
useVmTemplateApi,
} from 'client/features/One'
import { useGetUsersQuery } from 'client/features/OneApi/user'
import { useGetGroupsQuery } from 'client/features/OneApi/group'
import { useLazyGetTemplateQuery } from 'client/features/OneApi/vmTemplate'
import FormStepper, { SkeletonStepsForm } from 'client/components/FormStepper'
import Steps from 'client/components/Forms/VmTemplate/InstantiateForm/Steps'
@ -53,17 +50,12 @@ const InstantiateForm = ({ template, onSubmit }) => {
}
const PreFetchingForm = ({ templateId, onSubmit }) => {
const { getUsers } = useUserApi()
const { getVmGroups } = useVmGroupApi()
const { getVmTemplate } = useVmTemplateApi()
const { fetchRequest, data } = useFetch(() =>
getVmTemplate(templateId, { extended: true })
)
useGetUsersQuery()
useGetGroupsQuery()
const [getTemplate, { data }] = useLazyGetTemplateQuery()
useEffect(() => {
templateId && fetchRequest()
getUsers()
getVmGroups()
templateId && getTemplate({ id: templateId, extended: true })
}, [])
return templateId && !data ? (

View File

@ -109,8 +109,7 @@ const HeaderPopover = memo(
<Fade {...TransitionProps} timeout={300}>
<Paper
variant="outlined"
style={mobileStyles}
sx={{ p: headerTitle ? 2 : 0 }}
sx={{ p: headerTitle ? 2 : 0, ...mobileStyles }}
>
{(headerTitle || isMobile) && (
<Box

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useMemo, memo, JSXElementConstructor } from 'react'
import { useMemo, memo, ReactElement } from 'react'
import PropTypes from 'prop-types'
import { Button } from '@mui/material'
@ -70,7 +70,7 @@ ButtonView.displayName = 'ButtonView'
*
* These views are defined in yaml config.
*
* @returns {JSXElementConstructor} Returns interface views list
* @returns {ReactElement} Returns interface views list
*/
const View = () => {
const { view: currentView, views = {} } = useAuth()

View File

@ -13,13 +13,12 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useEffect, JSXElementConstructor } from 'react'
import { ReactElement } from 'react'
import { MenuList, MenuItem, LinearProgress } from '@mui/material'
import { Language as ZoneIcon } from 'iconoir-react'
import { useFetch } from 'client/hooks'
import { useZone, useZoneApi } from 'client/features/One'
import { useGetZonesQuery } from 'client/features/OneApi/zone'
import HeaderPopover from 'client/components/Header/Popover'
import { Translate } from 'client/components/HOC'
import { T } from 'client/constants'
@ -27,12 +26,10 @@ import { T } from 'client/constants'
/**
* Menu to select the OpenNebula Zone.
*
* @returns {JSXElementConstructor} Returns Zone list
* @returns {ReactElement} Returns Zone list
*/
const Zone = () => {
const zones = useZone()
const { getZones } = useZoneApi()
const { fetchRequest, loading } = useFetch(getZones)
const { data: zones = [], isLoading } = useGetZonesQuery()
return (
<HeaderPopover
@ -42,30 +39,24 @@ const Zone = () => {
buttonProps={{ 'data-cy': 'header-zone-button' }}
headerTitle={<Translate word={T.Zones} />}
>
{({ handleClose }) => {
useEffect(() => {
fetchRequest()
}, [])
return (
<>
{loading && <LinearProgress color="secondary" />}
<MenuList>
{zones?.length ? (
zones?.map(({ ID, NAME }) => (
<MenuItem key={`zone-${ID}`} onClick={handleClose}>
{NAME}
</MenuItem>
))
) : (
<MenuItem disabled>
<Translate word={T.NotFound} />
{({ handleClose }) => (
<>
{isLoading && <LinearProgress color="secondary" />}
<MenuList>
{zones?.length ? (
zones?.map(({ ID, NAME }) => (
<MenuItem key={`zone-${ID}`} onClick={handleClose}>
{NAME}
</MenuItem>
)}
</MenuList>
</>
)
}}
))
) : (
<MenuItem disabled>
<Translate word={T.NotFound} />
</MenuItem>
)}
</MenuList>
</>
)}
</HeaderPopover>
)
}

View File

@ -64,7 +64,7 @@ const ListInfiniteScroll = ({ list, renderResult }) => {
<LinearProgress
ref={loaderRef}
color="secondary"
style={{ width: '100%', marginTop: 10 }}
sx={{ width: '100%', marginTop: 10 }}
/>
)}
</div>

View File

@ -13,46 +13,55 @@
* 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 { useMemo, ReactElement } from 'react'
import { useAuth } from 'client/features/Auth'
import { useFetch } from 'client/hooks'
import { useCluster, useClusterApi } from 'client/features/One'
import { useGetClustersQuery } from 'client/features/OneApi/cluster'
import { SkeletonTable, EnhancedTable } from 'client/components/Tables'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import ClusterColumns from 'client/components/Tables/Clusters/columns'
import ClusterRow from 'client/components/Tables/Clusters/row'
import { RESOURCE_NAMES } from 'client/constants'
const DEFAULT_DATA_CY = 'clusters'
/**
* @param {object} props - Props
* @returns {ReactElement} Clusters table
*/
const ClustersTable = (props) => {
const columns = useMemo(() => ClusterColumns, [])
const { rootProps = {}, searchProps = {}, ...rest } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const clusters = useCluster()
const { getClusters } = useClusterApi()
const { filterPool } = useAuth()
const { view, getResourceView } = useAuth()
const { data = [], isFetching, refetch } = useGetClustersQuery()
const { status, fetchRequest, loading, reloading, STATUS } =
useFetch(getClusters)
const { INIT, PENDING } = STATUS
useEffect(() => {
fetchRequest()
}, [filterPool])
if (clusters?.length === 0 && [INIT, PENDING].includes(status)) {
return <SkeletonTable />
}
const columns = useMemo(
() =>
createColumns({
filters: getResourceView(RESOURCE_NAMES.CLUSTER)?.filters,
columns: ClusterColumns,
}),
[view]
)
return (
<EnhancedTable
columns={columns}
data={clusters}
isLoading={loading || reloading}
data={data}
rootProps={rootProps}
searchProps={searchProps}
refetch={refetch}
isLoading={isFetching}
getRowId={(row) => String(row.ID)}
RowComponent={ClusterRow}
{...props}
{...rest}
/>
)
}
ClustersTable.propTypes = { ...EnhancedTable.propTypes }
ClustersTable.displayName = 'ClustersTable'
export default ClustersTable

View File

@ -13,55 +13,60 @@
* 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 { useMemo, ReactElement } from 'react'
import { useAuth } from 'client/features/Auth'
import { useFetch } from 'client/hooks'
import { useDatastore, useDatastoreApi } from 'client/features/One'
import { useGetDatastoresQuery } from 'client/features/OneApi/datastore'
import { SkeletonTable, EnhancedTable } from 'client/components/Tables'
import { createColumns } from 'client/components/Tables/Enhanced/Utils'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import DatastoreColumns from 'client/components/Tables/Datastores/columns'
import DatastoreRow from 'client/components/Tables/Datastores/row'
import { RESOURCE_NAMES } from 'client/constants'
const DEFAULT_DATA_CY = 'datastores'
/**
* @param {object} props - Props
* @returns {ReactElement} Datastores table
*/
const DatastoresTable = (props) => {
const { view, getResourceView, filterPool } = useAuth()
const {
rootProps = {},
searchProps = {},
useQuery = useGetDatastoresQuery,
...rest
} = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useAuth()
const { data = [], isFetching, refetch } = useQuery()
const columns = useMemo(
() =>
createColumns({
filters: getResourceView('DATASTORE')?.filters,
filters: getResourceView(RESOURCE_NAMES.DATASTORE)?.filters,
columns: DatastoreColumns,
}),
[view]
)
const datastores = useDatastore()
const { getDatastores } = useDatastoreApi()
const { status, fetchRequest, loading, reloading, STATUS } =
useFetch(getDatastores)
const { INIT, PENDING } = STATUS
useEffect(() => {
fetchRequest()
}, [filterPool])
if (datastores?.length === 0 && [INIT, PENDING].includes(status)) {
return <SkeletonTable />
}
return (
<EnhancedTable
columns={columns}
data={datastores}
isLoading={loading || reloading}
data={data}
rootProps={rootProps}
searchProps={searchProps}
refetch={refetch}
isLoading={isFetching}
getRowId={(row) => String(row.ID)}
RowComponent={DatastoreRow}
{...props}
{...rest}
/>
)
}
DatastoresTable.propTypes = { ...EnhancedTable.propTypes }
DatastoresTable.displayName = 'DatastoresTable'
export default DatastoresTable

View File

@ -13,71 +13,22 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { memo } from 'react'
import PropTypes from 'prop-types'
import api from 'client/features/OneApi/datastore'
import { DatastoreCard } from 'client/components/Cards'
import { User, Group, Lock, Cloud, Server } from 'iconoir-react'
import { Typography } from '@mui/material'
const Row = memo(
({ original, ...props }) => {
const state = api.endpoints.getDatastores.useQueryState(undefined, {
selectFromResult: ({ data = [] }) =>
data.find((datastore) => +datastore.ID === +original.ID),
})
import {
StatusCircle,
LinearProgressWithLabel,
StatusChip,
} from 'client/components/Status'
import { rowStyles } from 'client/components/Tables/styles'
import * as DatastoreModel from 'client/models/Datastore'
const Row = ({ original, value, ...props }) => {
const classes = rowStyles()
const { ID, NAME, UNAME, GNAME, TYPE, CLUSTERS, LOCK, PROVISION_ID } = value
const { percentOfUsed, percentLabel } = DatastoreModel.getCapacityInfo(value)
const { color: stateColor, name: stateName } =
DatastoreModel.getState(original)
return (
<div {...props} data-cy={`datastore-${ID}`}>
<div>
<StatusCircle color={stateColor} tooltip={stateName} />
</div>
<div className={classes.main}>
<div className={classes.title}>
<Typography component="span">{NAME}</Typography>
<span className={classes.labels}>
{LOCK && <Lock />}
<StatusChip text={TYPE} />
</span>
</div>
<div className={classes.caption}>
<span>{`#${ID}`}</span>
<span title={`Owner: ${UNAME}`}>
<User />
<span>{` ${UNAME}`}</span>
</span>
<span title={`Group: ${GNAME}`}>
<Group />
<span>{` ${GNAME}`}</span>
</span>
{PROVISION_ID && (
<span title={`Provision ID: #${PROVISION_ID}`}>
<Cloud />
<span>{` ${PROVISION_ID}`}</span>
</span>
)}
<span title={`Cluster IDs: ${CLUSTERS.join(',')}`}>
<Server />
<span>{` ${CLUSTERS.join(',')}`}</span>
</span>
</div>
</div>
<div className={classes.secondary}>
<LinearProgressWithLabel value={percentOfUsed} label={percentLabel} />
</div>
</div>
)
}
return <DatastoreCard datastore={state ?? original} rootProps={props} />
},
(prev, next) => prev.className === next.className
)
Row.propTypes = {
original: PropTypes.object,
@ -86,4 +37,6 @@ Row.propTypes = {
handleClick: PropTypes.func,
}
Row.displayName = 'DatastoreRow'
export default Row

View File

@ -13,15 +13,16 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo, useEffect, useState, useCallback } from 'react'
import { useMemo, useEffect, useState, useCallback, ReactElement } from 'react'
import PropTypes from 'prop-types'
import { useFetch } from 'client/hooks'
import { useMarketplaceAppApi } from 'client/features/One'
import { useLazyGetDockerHubTagsQuery } from 'client/features/OneApi/marketplaceApp'
import EnhancedTable from 'client/components/Tables/Enhanced'
import DockerHubTagsRow from 'client/components/Tables/DockerHubTags/row'
import { MarketplaceApp } from 'client/constants'
const DEFAULT_DATA_CY = 'docker-tags'
const getNextPageFromUrl = (url) => {
try {
@ -33,49 +34,49 @@ const getNextPageFromUrl = (url) => {
}
}
/**
* @param {object} props - Props
* @param {MarketplaceApp} props.app - Marketplace App
* @returns {ReactElement} Datastores table
*/
const DockerHubTagsTable = ({ app, ...props } = {}) => {
const { getDockerHubTags } = useMarketplaceAppApi()
const appId = app?.ID
const { rootProps = {}, searchProps = {}, ...rest } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const [tags, setTags] = useState(() => [])
const {
data: { next, results = [] } = {},
fetchRequest,
loading,
status,
STATUS: { INIT, FETCHED },
} = useFetch(getDockerHubTags)
const [fetchRequest, { data: { next } = {}, isSuccess, isFetching }] =
useLazyGetDockerHubTagsQuery()
useEffect(() => {
const appId = app?.ID
const requests = app?.ID && {
[INIT]: () => fetchRequest({ id: appId }),
[FETCHED]: () => {
if (!next) return
;(async () => {
if (!appId || (isSuccess && !next)) return
const page = getNextPageFromUrl(next)
fetchRequest({ id: appId, page })
},
}
requests[status]?.()
}, [app?.ID, status])
useEffect(() => {
status === FETCHED && setTags((prev) => prev.concat(results))
}, [status])
const page = next ? getNextPageFromUrl(next) : undefined
const { results = [] } = await fetchRequest({ id: appId, page }).unwrap()
setTags((prev) => prev.concat(results))
})()
}, [appId, next])
const memoData = useMemo(() => tags, [tags?.length])
const memoColumns = useMemo(() => [{ accessor: 'name' }], [])
if (!appId) {
return <>{'App ID is required'}</>
}
return (
<EnhancedTable
columns={memoColumns}
data={memoData}
isLoading={loading}
rootProps={rootProps}
searchProps={searchProps}
refetch={fetchRequest}
isLoading={isFetching}
getRowId={useCallback((row) => String(row.name), [])}
RowComponent={DockerHubTagsRow}
{...props}
{...rest}
/>
)
}

View File

@ -27,7 +27,7 @@ import {
import { Cancel } from 'iconoir-react'
import { UseFiltersInstanceProps } from 'react-table'
import { Tr, Translate } from 'client/components/HOC'
import { Translate } from 'client/components/HOC'
import { T } from 'client/constants'
/**
@ -96,10 +96,9 @@ const CategoryFilter = ({
<ListSubheader
disableSticky
disableGutters
title={Tr(title)}
style={{ display: 'flex', alignItems: 'center' }}
sx={{ display: 'flex', alignItems: 'center' }}
>
{Tr(title)}
<Translate word={title} />
{isFiltered && (
<Tooltip title={<Translate word={T.Clear} />}>
<IconButton disableRipple size="small" onClick={handleClear}>

View File

@ -13,10 +13,13 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo, JSXElementConstructor } from 'react'
// eslint-disable-next-line no-unused-vars
import { memo, ReactElement } from 'react'
import PropTypes from 'prop-types'
// eslint-disable-next-line no-unused-vars
import { Row } from 'react-table'
import QueryButton from 'client/components/Buttons/QueryButton'
import { Action } from 'client/components/Cards/SelectCard'
import { ButtonToTriggerForm } from 'client/components/Forms'
import { Tr } from 'client/components/HOC'
@ -29,7 +32,7 @@ import { CreateStepsCallback, CreateFormCallback } from 'client/utils'
* @typedef {object} Option
* @property {string} name - Label of option
* @property {DialogProps} [dialogProps] - Dialog properties
* @property {JSXElementConstructor} [icon] - Icon
* @property {ReactElement} [icon] - Icon
* @property {boolean} [isConfirmDialog] - If `true`, the form will be a dialog with confirmation buttons
* @property {boolean|function(Row[]):boolean} [disabled] - If `true`, option will be disabled
* @property {function(object, Row[])} onSubmit - Function to handle after finish the form
@ -48,18 +51,12 @@ import { CreateStepsCallback, CreateFormCallback } from 'client/utils'
* @property {function(Row[])} [action] - Singular action without form
* @property {boolean|{min: number, max: number}} [selected] - Condition for selected rows
* @property {boolean|function(Row[]):boolean} [disabled] - If `true`, action will be disabled
* @property {function(Row[]):object} [useQuery] - Function to get rtk query result
*/
/**
* Render global action.
*
* @param {object} props - Props
* @param {GlobalAction[]} props.item - Item action
* @param {Row[]} props.selectedRows - Selected rows
* @returns {JSXElementConstructor} Component JSX
*/
const ActionItem = memo(
({ item, selectedRows }) => {
/** @type {GlobalAction} */
const {
accessor,
dataCy,
@ -71,6 +68,7 @@ const ActionItem = memo(
options,
action,
disabled,
useQuery,
} = item
const buttonProps = {
@ -87,6 +85,8 @@ const ActionItem = memo(
return action ? (
<Action {...buttonProps} handleClick={() => action?.(selectedRows)} />
) : useQuery ? (
<QueryButton {...buttonProps} useQuery={() => useQuery?.(selectedRows)} />
) : (
<ButtonToTriggerForm
buttonProps={buttonProps}
@ -141,48 +141,11 @@ const ActionItem = memo(
}
)
export const ActionPropTypes = PropTypes.shape({
accessor: PropTypes.string,
dataCy: PropTypes.string,
variant: PropTypes.string,
color: PropTypes.string,
size: PropTypes.string,
label: PropTypes.string,
tooltip: PropTypes.string,
icon: PropTypes.any,
disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
selected: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.shape({
min: PropTypes.number,
max: PropTypes.number,
}),
]),
action: PropTypes.func,
isConfirmDialog: PropTypes.bool,
options: PropTypes.arrayOf(
PropTypes.shape({
accessor: PropTypes.string,
name: PropTypes.string,
icon: PropTypes.any,
disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
form: PropTypes.func,
onSubmit: PropTypes.func,
dialogProps: PropTypes.shape({
...DialogPropTypes,
description: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
subheader: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
}),
})
),
})
ActionItem.propTypes = {
item: ActionPropTypes,
item: PropTypes.object,
selectedRows: PropTypes.array,
}
ActionItem.displayName = 'ActionItem'
export default ActionItem
export { ActionItem as Action }

View File

@ -16,7 +16,7 @@
import { JSXElementConstructor, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Stack, Checkbox } from '@mui/material'
import { Checkbox } from '@mui/material'
import {
UseTableInstanceProps,
UseRowSelectState,
@ -24,8 +24,8 @@ import {
UseRowSelectInstanceProps,
} from 'react-table'
import Action, {
ActionPropTypes,
import {
Action,
GlobalAction,
} from 'client/components/Tables/Enhanced/Utils/GlobalActions/Action'
import { Tr } from 'client/components/HOC'
@ -37,9 +37,14 @@ import { T } from 'client/constants'
* @param {object} props - Props
* @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
*/
const GlobalActions = ({ globalActions = [], useTableProps }) => {
const GlobalActions = ({
disableRowSelect = false,
globalActions = [],
useTableProps,
}) => {
/** @type {UseRowSelectInstanceProps} */
const { getToggleAllPageRowsSelectedProps, getToggleAllRowsSelectedProps } =
useTableProps
@ -69,18 +74,20 @@ const GlobalActions = ({ globalActions = [], useTableProps }) => {
)
return (
<Stack direction="row" flexWrap="wrap" alignItems="center" gap={1.5}>
<Checkbox
{...getToggleAllPageRowsSelectedProps()}
title={Tr(T.ToggleAllCurrentPageRowsSelected)}
indeterminate={getToggleAllRowsSelectedProps().indeterminate}
color="secondary"
/>
<>
{!disableRowSelect && (
<Checkbox
{...getToggleAllPageRowsSelectedProps()}
title={Tr(T.ToggleAllCurrentPageRowsSelected)}
indeterminate={getToggleAllRowsSelectedProps().indeterminate}
color="secondary"
/>
)}
{actionsNoSelected?.map((item) => (
<Action key={item.accessor} item={item} />
))}
{numberOfRowSelected > 0 &&
{!disableRowSelect &&
numberOfRowSelected > 0 &&
actionsSelected?.map((item, idx) => {
const { min = 1, max = Number.MAX_SAFE_INTEGER } =
item?.selected ?? {}
@ -92,15 +99,14 @@ const GlobalActions = ({ globalActions = [], useTableProps }) => {
return <Action key={key} item={item} selectedRows={selectedRows} />
})}
</Stack>
</>
)
}
GlobalActions.propTypes = {
globalActions: PropTypes.arrayOf(ActionPropTypes),
globalActions: PropTypes.array,
useTableProps: PropTypes.object,
disableRowSelect: PropTypes.bool,
}
export { Action, ActionPropTypes }
export default GlobalActions

View File

@ -79,9 +79,8 @@ const GlobalFilter = ({ useTableProps, className, searchProps }) => {
const handleChange = useCallback(
// Set undefined to remove the filter entirely
debounce((newFilter) => {
setGlobalFilter(newFilter || undefined)
}, 200)
debounce((newFilter) => setGlobalFilter(newFilter || undefined), 200),
[setGlobalFilter]
)
return (

View File

@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useEffect, useMemo, JSXElementConstructor } from 'react'
import PropTypes from 'prop-types'
@ -44,8 +43,6 @@ const GlobalSort = ({ useTableProps }) => {
/** @type {UseSortByState} */
const { sortBy } = state
useEffect(() => () => setSortBy([]), [])
const headersNotSorted = useMemo(
() =>
headers.filter(
@ -66,7 +63,9 @@ const GlobalSort = ({ useTableProps }) => {
setSortBy(sortBy.map((sort) => (sort.id === id ? { ...sort, desc } : sort)))
}
return (
useEffect(() => () => setSortBy([]), [])
return !headersNotSorted.length && !sortBy.length ? null : (
<Stack direction="row" gap="0.5em" flexWrap="wrap">
{useMemo(
() => (

View File

@ -22,7 +22,7 @@ import { List, ListSubheader, IconButton } from '@mui/material'
import { TreeView, TreeItem } from '@mui/lab'
import { UseFiltersInstanceProps } from 'react-table'
import { Tr } from 'client/components/HOC'
import { Translate } from 'client/components/HOC'
const buildTree = (data = [], separator = '/') => {
const mapper = {}
@ -101,10 +101,9 @@ const LabelFilter = ({ title, column }) => {
<ListSubheader
disableSticky
disableGutters
title={Tr(title)}
style={{ display: 'flex', alignItems: 'center' }}
sx={{ display: 'flex', alignItems: 'center' }}
>
{Tr(title)}
<Translate word={title} />
{isFiltered && (
<IconButton
disableRipple

View File

@ -14,20 +14,16 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import CategoryFilter from 'client/components/Tables/Enhanced/Utils/CategoryFilter'
import GlobalActions, {
Action,
ActionPropTypes,
} from 'client/components/Tables/Enhanced/Utils/GlobalActions'
import GlobalActions from 'client/components/Tables/Enhanced/Utils/GlobalActions'
import GlobalFilter from 'client/components/Tables/Enhanced/Utils/GlobalFilter'
import GlobalSelectedRows from 'client/components/Tables/Enhanced/Utils/GlobalSelectedRows'
import GlobalSort from 'client/components/Tables/Enhanced/Utils/GlobalSort'
import LabelFilter from 'client/components/Tables/Enhanced/Utils/LabelFilter'
export * from 'client/components/Tables/Enhanced/Utils/GlobalActions/Action'
export * from 'client/components/Tables/Enhanced/Utils/utils'
export {
Action,
ActionPropTypes,
CategoryFilter,
GlobalActions,
GlobalFilter,

View File

@ -19,7 +19,7 @@ import PropTypes from 'prop-types'
import clsx from 'clsx'
import { InfoEmpty } from 'iconoir-react'
import { Box, LinearProgress } from '@mui/material'
import { Box } from '@mui/material'
import {
useGlobalFilter,
useFilters,
@ -35,14 +35,12 @@ import {
import Toolbar from 'client/components/Tables/Enhanced/toolbar'
import Pagination from 'client/components/Tables/Enhanced/pagination'
import Filters from 'client/components/Tables/Enhanced/filters'
import { ActionPropTypes } from 'client/components/Tables/Enhanced/Utils'
import EnhancedTableStyles from 'client/components/Tables/Enhanced/styles'
import { Translate } from 'client/components/HOC'
import { T } from 'client/constants'
const EnhancedTable = ({
canFetchMore,
columns,
globalActions,
globalFilter,
@ -50,9 +48,12 @@ const EnhancedTable = ({
fetchMore,
getRowId,
initialState,
refetch,
isLoading,
onlyGlobalSearch,
onlyGlobalSelectedRows,
disableRowSelect,
disableGlobalSort,
onSelectedRowsChange,
pageSize = 10,
RowComponent,
@ -64,7 +65,7 @@ const EnhancedTable = ({
}) => {
const styles = EnhancedTableStyles()
const isFetching = isLoading && data === undefined
const isUninitialized = isLoading && data === undefined
const defaultColumn = useMemo(
() => ({
@ -140,7 +141,7 @@ const EnhancedTable = ({
const canNextPage =
pageCount === -1 ? page.length >= pageSize : newPage < pageCount - 1
newPage > pageIndex && canFetchMore && !canNextPage && fetchMore?.()
newPage > pageIndex && !canNextPage && fetchMore?.()
}
return (
@ -151,13 +152,15 @@ const EnhancedTable = ({
>
<div className={styles.toolbar}>
{/* TOOLBAR */}
{!isFetching && (
<Toolbar
globalActions={globalActions}
onlyGlobalSelectedRows={onlyGlobalSelectedRows}
useTableProps={useTableProps}
/>
)}
<Toolbar
refetch={refetch}
isLoading={isLoading}
globalActions={globalActions}
disableRowSelect={disableRowSelect}
disableGlobalSort={disableGlobalSort}
onlyGlobalSelectedRows={onlyGlobalSelectedRows}
useTableProps={useTableProps}
/>
{/* PAGINATION */}
<div className={styles.pagination}>
@ -170,17 +173,9 @@ const EnhancedTable = ({
</div>
</div>
{isLoading && (
<LinearProgress
size="1em"
color="secondary"
className={styles.loading}
/>
)}
<div className={styles.table}>
{/* FILTERS */}
{!isFetching && (
{!isUninitialized && (
<Filters
onlyGlobalSearch={onlyGlobalSearch}
useTableProps={useTableProps}
@ -190,7 +185,7 @@ const EnhancedTable = ({
<div className={clsx(styles.body, classes.body)}>
{/* NO DATA MESSAGE */}
{!isFetching && page?.length === 0 && (
{!isUninitialized && page?.length === 0 && (
<span className={styles.noDataMessage}>
<InfoEmpty />
<Translate word={T.NoDataAvailable} />
@ -219,8 +214,10 @@ const EnhancedTable = ({
value={values}
className={isSelected ? 'selected' : ''}
onClick={() => {
singleSelect && toggleAllRowsSelected(false)
toggleRowSelected(!isSelected)
if (!disableRowSelect) {
singleSelect && toggleAllRowsSelected?.(false)
toggleRowSelected?.(!isSelected)
}
}}
/>
)
@ -231,9 +228,9 @@ const EnhancedTable = ({
)
}
export const EnhancedTableProps = {
EnhancedTable.propTypes = {
canFetchMore: PropTypes.bool,
globalActions: PropTypes.arrayOf(ActionPropTypes),
globalActions: PropTypes.array,
columns: PropTypes.array,
data: PropTypes.array,
globalFilter: PropTypes.func,
@ -250,20 +247,19 @@ export const EnhancedTableProps = {
searchProps: PropTypes.shape({
'data-cy': PropTypes.string,
}),
refetch: PropTypes.func,
isLoading: PropTypes.bool,
disableGlobalSort: PropTypes.bool,
disableRowSelect: PropTypes.bool,
onlyGlobalSearch: PropTypes.bool,
onlyGlobalSelectedRows: PropTypes.bool,
onSelectedRowsChange: PropTypes.func,
pageSize: PropTypes.number,
RowComponent: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]),
RowComponent: PropTypes.any,
showPageCount: PropTypes.bool,
singleSelect: PropTypes.bool,
}
EnhancedTable.propTypes = EnhancedTableProps
export * from 'client/components/Tables/Enhanced/Utils'
export default EnhancedTable

View File

@ -18,66 +18,97 @@ 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,
ActionPropTypes,
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 {object} props.onlyGlobalSelectedRows - Show only the selected rows
* @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, useTableProps }) => {
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'))
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 ?? {}
if (onlyGlobalSelectedRows) {
return <GlobalSelectedRows useTableProps={useTableProps} />
}
const enableGlobalSort = !isSmallDevice && !disableGlobalSort
const enableGlobalSelect =
!onlyGlobalSelectedRows && !!Object.keys(selectedRowIds).length
return isMobile ? null : (
return (
<>
<Stack alignItems="start" gap="1em">
<GlobalActions
globalActions={globalActions}
useTableProps={useTableProps}
/>
</Stack>
<Stack
className="summary"
direction="row"
flexWrap="wrap"
alignItems="center"
gap={'1em'}
width={1}
>
{!isSmallDevice && (
<div>
<GlobalSort useTableProps={useTableProps} />
</div>
<Stack direction="row" flexWrap="wrap" alignItems="center" gap="1em">
{refetch && (
<SubmitButton
data-cy="refresh"
icon={<RefreshDouble />}
title={T.Tooltip}
isSubmitting={isLoading}
onClick={refetch}
/>
)}
{!!Object.keys(selectedRowIds).length && (
<GlobalSelectedRows withAlert useTableProps={useTableProps} />
{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.arrayOf(ActionPropTypes),
globalActions: PropTypes.array,
onlyGlobalSelectedRows: PropTypes.bool,
disableRowSelect: PropTypes.bool,
disableGlobalSort: PropTypes.bool,
useTableProps: PropTypes.object,
refetch: PropTypes.func,
isLoading: PropTypes.bool,
}
export default Toolbar

View File

@ -13,55 +13,55 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo, useEffect } from 'react'
import { useMemo, ReactElement } from 'react'
import { useAuth } from 'client/features/Auth'
import { useFetch } from 'client/hooks'
import { useGroup, useGroupApi } from 'client/features/One'
import { useGetGroupsQuery } from 'client/features/OneApi/group'
import { SkeletonTable, EnhancedTable } from 'client/components/Tables'
import { createColumns } from 'client/components/Tables/Enhanced/Utils'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import GroupColumns from 'client/components/Tables/Groups/columns'
import GroupRow from 'client/components/Tables/Groups/row'
import { RESOURCE_NAMES } from 'client/constants'
const DEFAULT_DATA_CY = 'groups'
/**
* @param {object} props - Props
* @returns {ReactElement} Groups table
*/
const GroupsTable = (props) => {
const { view, getResourceView, filterPool } = useAuth()
const { rootProps = {}, searchProps = {}, ...rest } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useAuth()
const { data = [], isFetching, refetch } = useGetGroupsQuery()
const columns = useMemo(
() =>
createColumns({
filters: getResourceView('GROUP')?.filters,
filters: getResourceView(RESOURCE_NAMES.GROUP)?.filters,
columns: GroupColumns,
}),
[view]
)
const groups = useGroup()
const { getGroups } = useGroupApi()
const { status, fetchRequest, loading, reloading, STATUS } =
useFetch(getGroups)
const { INIT, PENDING } = STATUS
useEffect(() => {
fetchRequest()
}, [filterPool])
if (groups?.length === 0 && [INIT, PENDING].includes(status)) {
return <SkeletonTable />
}
return (
<EnhancedTable
columns={columns}
data={groups}
isLoading={loading || reloading}
data={data}
rootProps={rootProps}
searchProps={searchProps}
refetch={refetch}
isLoading={isFetching}
getRowId={(row) => String(row.ID)}
RowComponent={GroupRow}
{...props}
{...rest}
/>
)
}
GroupsTable.propTypes = { ...EnhancedTable.propTypes }
GroupsTable.displayName = 'GroupsTable'
export default GroupsTable

View File

@ -13,62 +13,60 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo, useEffect } from 'react'
import { useMemo, ReactElement } from 'react'
import { useAuth } from 'client/features/Auth'
import { useFetch } from 'client/hooks'
import { useHost, useHostApi } from 'client/features/One'
import { useGetHostsQuery } from 'client/features/OneApi/host'
import {
SkeletonTable,
EnhancedTable,
EnhancedTableProps,
} from 'client/components/Tables'
import { createColumns } from 'client/components/Tables/Enhanced/Utils'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import HostColumns from 'client/components/Tables/Hosts/columns'
import HostRow from 'client/components/Tables/Hosts/row'
import { RESOURCE_NAMES } from 'client/constants'
const DEFAULT_DATA_CY = 'hosts'
/**
* @param {object} props - Props
* @returns {ReactElement} Hosts table
*/
const HostsTable = (props) => {
const { view, getResourceView, filterPool } = useAuth()
const {
rootProps = {},
searchProps = {},
useQuery = useGetHostsQuery,
...rest
} = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useAuth()
const { data = [], isFetching, refetch } = useQuery()
const columns = useMemo(
() =>
createColumns({
filters: getResourceView('HOST')?.filters,
filters: getResourceView(RESOURCE_NAMES.HOST)?.filters,
columns: HostColumns,
}),
[view]
)
const hosts = useHost()
const { getHosts } = useHostApi()
const { status, fetchRequest, loading, reloading, STATUS } =
useFetch(getHosts)
const { INIT, PENDING } = STATUS
useEffect(() => {
fetchRequest()
}, [filterPool])
if (hosts?.length === 0 && [INIT, PENDING].includes(status)) {
return <SkeletonTable />
}
return (
<EnhancedTable
columns={columns}
data={hosts}
isLoading={loading || reloading}
data={data}
rootProps={rootProps}
searchProps={searchProps}
refetch={refetch}
isLoading={isFetching}
getRowId={(row) => String(row.ID)}
RowComponent={HostRow}
{...props}
{...rest}
/>
)
}
HostsTable.propTypes = EnhancedTableProps
HostsTable.propTypes = { ...EnhancedTable.propTypes }
HostsTable.displayName = 'HostsTable'
export default HostsTable

View File

@ -13,86 +13,22 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { memo } from 'react'
import PropTypes from 'prop-types'
import hostApi from 'client/features/OneApi/host'
import { HostCard } from 'client/components/Cards'
import { Server, ModernTv } from 'iconoir-react'
import { Typography } from '@mui/material'
const Row = memo(
({ original, ...props }) => {
const state = hostApi.endpoints.getHosts.useQueryState(undefined, {
selectFromResult: ({ data = [] }) =>
data.find((host) => +host.ID === +original.ID),
})
import {
StatusCircle,
LinearProgressWithLabel,
StatusChip,
} from 'client/components/Status'
import { rowStyles } from 'client/components/Tables/styles'
import { Tr } from 'client/components/HOC'
import * as HostModel from 'client/models/Host'
import { T } from 'client/constants'
const Row = ({ original, value, ...props }) => {
const classes = rowStyles()
const {
ID,
NAME,
IM_MAD,
VM_MAD,
RUNNING_VMS,
TOTAL_VMS,
CLUSTER,
TEMPLATE,
} = value
const { percentCpuUsed, percentCpuLabel, percentMemUsed, percentMemLabel } =
HostModel.getAllocatedInfo(value)
const { color: stateColor, name: stateName } = HostModel.getState(original)
const labels = [...new Set([IM_MAD, VM_MAD])]
return (
<div {...props} data-cy={`host-${ID}`}>
<div>
<StatusCircle color={stateColor} tooltip={stateName} />
</div>
<div className={classes.main}>
<div className={classes.title}>
<Typography noWrap component="span">
{TEMPLATE?.NAME ?? NAME}
</Typography>
<span className={classes.labels}>
{labels.map((label) => (
<StatusChip key={label} text={label} />
))}
</span>
</div>
<div className={classes.caption}>
<span>{`#${ID}`}</span>
<span title={`Cluster: ${CLUSTER}`}>
<Server />
<span>{` ${CLUSTER}`}</span>
</span>
<span title={`Running VMs: ${RUNNING_VMS} / ${TOTAL_VMS}`}>
<ModernTv />
<span>{` ${RUNNING_VMS} / ${TOTAL_VMS}`}</span>
</span>
</div>
</div>
<div className={classes.secondary}>
<LinearProgressWithLabel
value={percentCpuUsed}
label={percentCpuLabel}
title={`${Tr(T.AllocatedCpu)}`}
/>
<LinearProgressWithLabel
value={percentMemUsed}
label={percentMemLabel}
title={`${Tr(T.AllocatedMemory)}`}
/>
</div>
</div>
)
}
return <HostCard host={state ?? original} rootProps={props} />
},
(prev, next) => prev.className === next.className
)
Row.propTypes = {
original: PropTypes.object,
@ -101,4 +37,6 @@ Row.propTypes = {
handleClick: PropTypes.func,
}
Row.displayName = 'HostRow'
export default Row

View File

@ -1,108 +0,0 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, 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 } from 'react'
import PropTypes from 'prop-types'
import { LinearProgress } from '@mui/material'
import Tabs from 'client/components/Tabs'
import { StatusBadge } from 'client/components/Status'
import { useFetch, useSocket } from 'client/hooks'
import { useImageApi } from 'client/features/One'
import { prettyBytes } from 'client/utils'
import * as ImageModel from 'client/models/Image'
import * as Helper from 'client/models/Helper'
const ImageDetail = ({ id }) => {
const { getImage } = useImageApi()
const { getHooksSocket } = useSocket()
const socket = getHooksSocket({ resource: 'image', id })
const { data, fetchRequest, loading, error } = useFetch(getImage, socket)
const isLoading = (!data && !error) || loading
useEffect(() => {
fetchRequest(id)
}, [id])
if (isLoading) {
return <LinearProgress color="secondary" style={{ width: '100%' }} />
}
if (error) {
return <div>{error}</div>
}
const {
ID,
NAME,
UNAME,
GNAME,
REGTIME,
SIZE,
PERSISTENT,
LOCK,
DATASTORE,
VMS,
RUNNING_VMS,
} = data
const { name: stateName, color: stateColor } = ImageModel.getState(data)
const type = ImageModel.getType(data)
const size = +SIZE ? prettyBytes(+SIZE, 'MB') : '-'
const usedByVms = [VMS?.ID ?? []].flat().length || 0
const tabs = [
{
name: 'info',
renderContent: (
<div>
<div>
<StatusBadge
title={stateName}
stateColor={stateColor}
customTransform="translate(150%, 50%)"
/>
<span style={{ marginLeft: 20 }}>{`#${ID} - ${NAME}`}</span>
</div>
<div>
<p>Owner: {UNAME}</p>
<p>Group: {GNAME}</p>
<p>Datastore: {DATASTORE}</p>
<p>Persistent: {type}</p>
<p>Size: {size}</p>
<p>Register time: {Helper.timeToString(REGTIME)}</p>
<p>Locked: {Helper.levelLockToString(LOCK?.LOCKED)}</p>
<p>Persistent: {Helper.booleanToString(PERSISTENT)}</p>
<p>Running VMS: {` ${RUNNING_VMS} / ${usedByVms}`}</p>
</div>
</div>
),
},
]
return <Tabs tabs={tabs} />
}
ImageDetail.propTypes = {
id: PropTypes.string.isRequired,
}
export default ImageDetail

View File

@ -13,53 +13,55 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo, useEffect } from 'react'
import { useMemo, ReactElement } from 'react'
import { useAuth } from 'client/features/Auth'
import { useFetch } from 'client/hooks'
import { useImage, useImageApi } from 'client/features/One'
import { useGetImagesQuery } from 'client/features/OneApi/image'
import {
SkeletonTable,
EnhancedTable,
EnhancedTableProps,
} from 'client/components/Tables'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import ImageColumns from 'client/components/Tables/Images/columns'
import ImageRow from 'client/components/Tables/Images/row'
import { RESOURCE_NAMES } from 'client/constants'
const DEFAULT_DATA_CY = 'images'
/**
* @param {object} props - Props
* @returns {ReactElement} Images table
*/
const ImagesTable = (props) => {
const columns = useMemo(() => ImageColumns, [])
const { rootProps = {}, searchProps = {}, ...rest } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const images = useImage()
const { getImages } = useImageApi()
const { filterPool } = useAuth()
const { view, getResourceView } = useAuth()
const { data = [], isFetching, refetch } = useGetImagesQuery()
const { status, fetchRequest, loading, reloading, STATUS } =
useFetch(getImages)
const { INIT, PENDING } = STATUS
useEffect(() => {
fetchRequest()
}, [filterPool])
if (images?.length === 0 && [INIT, PENDING].includes(status)) {
return <SkeletonTable />
}
const columns = useMemo(
() =>
createColumns({
filters: getResourceView(RESOURCE_NAMES.IMAGE)?.filters,
columns: ImageColumns,
}),
[view]
)
return (
<EnhancedTable
columns={columns}
data={images}
isLoading={loading || reloading}
data={data}
rootProps={rootProps}
searchProps={searchProps}
refetch={refetch}
isLoading={isFetching}
getRowId={(row) => String(row.ID)}
RowComponent={ImageRow}
{...props}
{...rest}
/>
)
}
ImagesTable.propTypes = EnhancedTableProps
ImagesTable.propTypes = { ...EnhancedTable.propTypes }
ImagesTable.displayName = 'ImagesTable'
export default ImagesTable

View File

@ -16,18 +16,17 @@
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo } from 'react'
import { useHistory } from 'react-router-dom'
import { RefreshDouble, AddSquare, CloudDownload } from 'iconoir-react'
import { AddSquare, CloudDownload } from 'iconoir-react'
import { useAuth } from 'client/features/Auth'
import { useGeneralApi } from 'client/features/General'
import { useMarketplaceAppApi } from 'client/features/One'
import { Translate } from 'client/components/HOC'
import { useExportAppMutation } from 'client/features/OneApi/marketplaceApp'
import { ExportForm } from 'client/components/Forms/MarketplaceApp'
import { createActions } from 'client/components/Tables/Enhanced/Utils'
import { PATH } from 'client/apps/sunstone/routesOne'
import { T, MARKETPLACE_APP_ACTIONS } from 'client/constants'
import { T, RESOURCE_NAMES, MARKETPLACE_APP_ACTIONS } from 'client/constants'
const MessageToConfirmAction = (rows) => {
const names = rows?.map?.(({ original }) => original?.NAME)
@ -51,21 +50,13 @@ const Actions = () => {
const history = useHistory()
const { view, getResourceView } = useAuth()
const { enqueueSuccess } = useGeneralApi()
const { getMarketplaceApps, exportApp } = useMarketplaceAppApi()
const [exportApp] = useExportAppMutation()
const marketplaceAppActions = useMemo(
() =>
createActions({
filters: getResourceView('MARKETPLACE-APP')?.actions,
filters: getResourceView(RESOURCE_NAMES.APP)?.actions,
actions: [
{
accessor: MARKETPLACE_APP_ACTIONS.REFRESH,
tooltip: T.Refresh,
icon: RefreshDouble,
action: async () => {
await getMarketplaceApps()
},
},
{
accessor: MARKETPLACE_APP_ACTIONS.CREATE_DIALOG,
tooltip: T.CreateMarketApp,
@ -88,9 +79,9 @@ const Actions = () => {
return ExportForm(app, app)
},
onSubmit: async (formData, rows) => {
const appId = rows?.[0]?.original?.ID
const response = await exportApp(appId, formData)
enqueueSuccess(response)
const id = rows?.[0]?.original?.ID
const res = await exportApp({ id, ...formData }).unwrap()
enqueueSuccess(res)
},
},
],

View File

@ -13,55 +13,55 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo, useEffect } from 'react'
import { useMemo, ReactElement } from 'react'
import { useAuth } from 'client/features/Auth'
import { useFetch } from 'client/hooks'
import { useMarketplaceApp, useMarketplaceAppApi } from 'client/features/One'
import { useGetMarketplaceAppsQuery } from 'client/features/OneApi/marketplaceApp'
import { SkeletonTable, EnhancedTable } from 'client/components/Tables'
import { createColumns } from 'client/components/Tables/Enhanced/Utils'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import MarketplaceAppColumns from 'client/components/Tables/MarketplaceApps/columns'
import MarketplaceAppRow from 'client/components/Tables/MarketplaceApps/row'
import { RESOURCE_NAMES } from 'client/constants'
const DEFAULT_DATA_CY = 'apps'
/**
* @param {object} props - Props
* @returns {ReactElement} Marketplace Apps table
*/
const MarketplaceAppsTable = (props) => {
const { view, getResourceView, filterPool } = useAuth()
const { rootProps = {}, searchProps = {}, ...rest } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useAuth()
const { data = [], isFetching, refetch } = useGetMarketplaceAppsQuery()
const columns = useMemo(
() =>
createColumns({
filters: getResourceView('MARKETPLACE-APP')?.filters,
filters: getResourceView(RESOURCE_NAMES.APP)?.filters,
columns: MarketplaceAppColumns,
}),
[view]
)
const marketplaceApps = useMarketplaceApp()
const { getMarketplaceApps } = useMarketplaceAppApi()
const { status, fetchRequest, loading, reloading, STATUS } =
useFetch(getMarketplaceApps)
const { INIT, PENDING } = STATUS
useEffect(() => {
fetchRequest()
}, [filterPool])
if (marketplaceApps?.length === 0 && [INIT, PENDING].includes(status)) {
return <SkeletonTable />
}
return (
<EnhancedTable
columns={columns}
data={marketplaceApps}
isLoading={loading || reloading}
data={data}
rootProps={rootProps}
searchProps={searchProps}
refetch={refetch}
isLoading={isFetching}
getRowId={(row) => String(row.ID)}
RowComponent={MarketplaceAppRow}
{...props}
{...rest}
/>
)
}
MarketplaceAppsTable.propTypes = { ...EnhancedTable.propTypes }
MarketplaceAppsTable.displayName = 'MarketplaceAppsTable'
export default MarketplaceAppsTable

View File

@ -13,58 +13,52 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo, useEffect } from 'react'
import { useMemo, ReactElement } from 'react'
import PropTypes from 'prop-types'
import { useAuth } from 'client/features/Auth'
import { useFetch } from 'client/hooks'
import { useMarketplace, useMarketplaceApi } from 'client/features/One'
import { useGetMarketplacesQuery } from 'client/features/OneApi/marketplace'
import { SkeletonTable, EnhancedTable } from 'client/components/Tables'
import { createColumns } from 'client/components/Tables/Enhanced/Utils'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import MarketplaceColumns from 'client/components/Tables/Marketplaces/columns'
import MarketplaceRow from 'client/components/Tables/Marketplaces/row'
import { RESOURCE_NAMES } from 'client/constants'
const DEFAULT_DATA_CY = 'marketplaces'
/**
* @param {object} props - Props
* @param {function():Array} [props.filter] - Function to filter the data
* @returns {ReactElement} Marketplaces table
*/
const MarketplacesTable = ({ filter, ...props }) => {
const { view, getResourceView, filterPool } = useAuth()
const { rootProps = {}, searchProps = {}, ...rest } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useAuth()
const { data = [], isFetching, refetch } = useGetMarketplacesQuery()
const columns = useMemo(
() =>
createColumns({
filters: getResourceView('MARKETPLACE')?.filters,
filters: getResourceView(RESOURCE_NAMES.MARKETPLACE)?.filters,
columns: MarketplaceColumns,
}),
[view]
)
const marketplaces = useMarketplace()
const { getMarketplaces } = useMarketplaceApi()
const { status, fetchRequest, loading, reloading, STATUS } =
useFetch(getMarketplaces)
const { INIT, PENDING } = STATUS
useEffect(() => {
fetchRequest()
}, [filterPool])
if (marketplaces?.length === 0 && [INIT, PENDING].includes(status)) {
return <SkeletonTable />
}
return (
<EnhancedTable
columns={columns}
data={
typeof filter === 'function'
? marketplaces?.filter(filter)
: marketplaces
}
isLoading={loading || reloading}
data={typeof filter === 'function' ? data?.filter(filter) : data}
rootProps={rootProps}
searchProps={searchProps}
refetch={refetch}
isLoading={isFetching}
getRowId={(row) => String(row.ID)}
RowComponent={MarketplaceRow}
{...props}
{...rest}
/>
)
}
@ -73,7 +67,6 @@ MarketplacesTable.propTypes = {
filter: PropTypes.func,
...EnhancedTable.propTypes,
}
MarketplacesTable.displayName = 'MarketplacesTable'
export default MarketplacesTable

View File

@ -39,7 +39,7 @@ const SkeletonTable = memo(() => {
const rowClasses = rowStyles()
const SkeletonRow = () => (
<Card style={{ padding: '1em' }}>
<Card sx={{ p: '1em' }}>
<div className={rowClasses.main}>
<div className={rowClasses.title}>
<Skeleton width={'40%'} height={30} />
@ -67,18 +67,10 @@ const SkeletonTable = memo(() => {
</div>
<div className={classes.table}>
{isMobile ? (
<Skeleton
variant="rectangular"
height={40}
style={{ marginBottom: '1em' }}
/>
<Skeleton variant="rectangular" height={40} sx={{ mb: '1em' }} />
) : (
<Card variant="outlined" style={{ padding: '1em' }}>
<Skeleton
variant="rectangular"
height={40}
style={{ marginBottom: '1em' }}
/>
<Card variant="outlined" sx={{ p: '1em' }}>
<Skeleton variant="rectangular" height={40} sx={{ mb: '1em' }} />
<div>
<SkeletonCategory />
<SkeletonCategory numberOfItems={3} />

View File

@ -13,55 +13,55 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo, useEffect } from 'react'
import { useMemo, ReactElement } from 'react'
import { useAuth } from 'client/features/Auth'
import { useFetch } from 'client/hooks'
import { useUser, useUserApi } from 'client/features/One'
import { useGetUsersQuery } from 'client/features/OneApi/user'
import { SkeletonTable, EnhancedTable } from 'client/components/Tables'
import { createColumns } from 'client/components/Tables/Enhanced/Utils'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import UserColumns from 'client/components/Tables/Users/columns'
import UserRow from 'client/components/Tables/Users/row'
import { RESOURCE_NAMES } from 'client/constants'
const DEFAULT_DATA_CY = 'users'
/**
* @param {object} props - Props
* @returns {ReactElement} Users table
*/
const UsersTable = (props) => {
const { view, getResourceView, filterPool } = useAuth()
const { rootProps = {}, searchProps = {}, ...rest } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useAuth()
const { data = [], isFetching, refetch } = useGetUsersQuery()
const columns = useMemo(
() =>
createColumns({
filters: getResourceView('USER')?.filters,
filters: getResourceView(RESOURCE_NAMES.USER)?.filters,
columns: UserColumns,
}),
[view]
)
const users = useUser()
const { getUsers } = useUserApi()
const { status, fetchRequest, loading, reloading, STATUS } =
useFetch(getUsers)
const { INIT, PENDING } = STATUS
useEffect(() => {
fetchRequest()
}, [filterPool])
if (users?.length === 0 && [INIT, PENDING].includes(status)) {
return <SkeletonTable />
}
return (
<EnhancedTable
columns={columns}
data={users}
isLoading={loading || reloading}
data={data}
rootProps={rootProps}
searchProps={searchProps}
refetch={refetch}
isLoading={isFetching}
getRowId={(row) => String(row.ID)}
RowComponent={UserRow}
{...props}
{...rest}
/>
)
}
UsersTable.propTypes = { ...EnhancedTable.propTypes }
UsersTable.displayName = 'UsersTable'
export default UsersTable

View File

@ -13,46 +13,56 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo, useEffect } from 'react'
import { useFetch } from 'client/hooks'
import {
useVNetworkTemplate,
useVNetworkTemplateApi,
} from 'client/features/One'
import { useMemo, ReactElement } from 'react'
import { SkeletonTable, EnhancedTable } from 'client/components/Tables'
import { useAuth } from 'client/features/Auth'
import { useGetVNTemplatesQuery } from 'client/features/OneApi/networkTemplate'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import VNetworkTemplateColumns from 'client/components/Tables/VNetworkTemplates/columns'
import VNetworkTemplateRow from 'client/components/Tables/VNetworkTemplates/row'
import { RESOURCE_NAMES } from 'client/constants'
const VNetworkTemplatesTable = () => {
const columns = useMemo(() => VNetworkTemplateColumns, [])
const DEFAULT_DATA_CY = 'vnet-templates'
const vNetworkTemplates = useVNetworkTemplate()
const { getVNetworkTemplates } = useVNetworkTemplateApi()
/**
* @param {object} props - Props
* @returns {ReactElement} Virtual Network Templates table
*/
const VNetworkTemplatesTable = (props) => {
const { rootProps = {}, searchProps = {}, ...rest } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { status, fetchRequest, loading, reloading, STATUS } =
useFetch(getVNetworkTemplates)
const { INIT, PENDING } = STATUS
const { view, getResourceView } = useAuth()
const { data = [], isFetching, refetch } = useGetVNTemplatesQuery()
useEffect(() => {
fetchRequest()
}, [])
if (vNetworkTemplates?.length === 0 && [INIT, PENDING].includes(status)) {
return <SkeletonTable />
}
const columns = useMemo(
() =>
createColumns({
filters: getResourceView(RESOURCE_NAMES.VN_TEMPLATE)?.filters,
columns: VNetworkTemplateColumns,
}),
[view]
)
return (
<EnhancedTable
columns={columns}
data={vNetworkTemplates}
isLoading={loading || reloading}
data={data}
rootProps={rootProps}
searchProps={searchProps}
refetch={refetch}
isLoading={isFetching}
getRowId={(row) => String(row.ID)}
RowComponent={VNetworkTemplateRow}
{...rest}
/>
)
}
VNetworkTemplatesTable.propTypes = { ...EnhancedTable.propTypes }
VNetworkTemplatesTable.displayName = 'VNetworkTemplatesTable'
export default VNetworkTemplatesTable

View File

@ -13,51 +13,60 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo, useEffect } from 'react'
import { useMemo, ReactElement } from 'react'
import { useFetch } from 'client/hooks'
import { useVNetwork, useVNetworkApi } from 'client/features/One'
import { useAuth } from 'client/features/Auth'
import { useGetVNetworksQuery } from 'client/features/OneApi/network'
import {
SkeletonTable,
EnhancedTable,
EnhancedTableProps,
} from 'client/components/Tables'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import VNetworkColumns from 'client/components/Tables/VNetworks/columns'
import VNetworkRow from 'client/components/Tables/VNetworks/row'
import { RESOURCE_NAMES } from 'client/constants'
const DEFAULT_DATA_CY = 'vnets'
/**
* @param {object} props - Props
* @returns {ReactElement} Virtual networks table
*/
const VNetworksTable = (props) => {
const columns = useMemo(() => VNetworkColumns, [])
const {
rootProps = {},
searchProps = {},
useQuery = useGetVNetworksQuery,
...rest
} = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const vNetworks = useVNetwork()
const { getVNetworks } = useVNetworkApi()
const { view, getResourceView } = useAuth()
const { data = [], isFetching, refetch } = useQuery()
const { status, fetchRequest, loading, reloading, STATUS } =
useFetch(getVNetworks)
const { INIT, PENDING } = STATUS
useEffect(() => {
fetchRequest()
}, [])
if (vNetworks?.length === 0 && [INIT, PENDING].includes(status)) {
return <SkeletonTable />
}
const columns = useMemo(
() =>
createColumns({
filters: getResourceView(RESOURCE_NAMES.VNET)?.filters,
columns: VNetworkColumns,
}),
[view]
)
return (
<EnhancedTable
columns={columns}
data={vNetworks}
isLoading={loading || reloading}
data={data}
rootProps={rootProps}
searchProps={searchProps}
refetch={refetch}
isLoading={isFetching}
getRowId={(row) => String(row.ID)}
RowComponent={VNetworkRow}
{...props}
{...rest}
/>
)
}
VNetworksTable.propTypes = EnhancedTableProps
VNetworksTable.displayName = 'VNetworksTable'
VNetworksTable.propTypes = { ...EnhancedTable.propTypes }
VNetworksTable.displayName = 'VirtualNetworksTable'
export default VNetworksTable

View File

@ -13,73 +13,22 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { memo } from 'react'
import PropTypes from 'prop-types'
import api from 'client/features/OneApi/network'
import { NetworkCard } from 'client/components/Cards'
import { User, Group, Lock, Server, Cloud } from 'iconoir-react'
import { Typography } from '@mui/material'
const Row = memo(
({ original, ...props }) => {
const state = api.endpoints.getVNetworks.useQueryState(undefined, {
selectFromResult: ({ data = [] }) =>
data.find((network) => +network.ID === +original.ID),
})
import { LinearProgressWithLabel } from 'client/components/Status'
import { rowStyles } from 'client/components/Tables/styles'
import * as VirtualNetworkModel from 'client/models/VirtualNetwork'
const Row = ({ original, value, ...props }) => {
const classes = rowStyles()
const {
ID,
NAME,
UNAME,
GNAME,
LOCK,
CLUSTERS,
USED_LEASES,
TOTAL_LEASES,
PROVISION_ID,
} = value
const { percentOfUsed, percentLabel } =
VirtualNetworkModel.getLeasesInfo(original)
return (
<div {...props} data-cy={`network-${ID}`}>
<div className={classes.main}>
<div className={classes.title}>
<Typography component="span">{NAME}</Typography>
<span className={classes.labels}>{LOCK && <Lock />}</span>
</div>
<div className={classes.caption}>
<span>{`#${ID}`}</span>
<span title={`Owner: ${UNAME}`}>
<User />
<span>{` ${UNAME}`}</span>
</span>
<span title={`Group: ${GNAME}`}>
<Group />
<span>{` ${GNAME}`}</span>
</span>
<span title={`Total Clusters: ${CLUSTERS}`}>
<Server />
<span>{` ${CLUSTERS}`}</span>
</span>
{PROVISION_ID && (
<span title={`Provision ID: #${PROVISION_ID}`}>
<Cloud />
<span>{` ${PROVISION_ID}`}</span>
</span>
)}
</div>
</div>
<div className={classes.secondary}>
<LinearProgressWithLabel
title={`Used / Total Leases: ${USED_LEASES} / ${TOTAL_LEASES}`}
value={percentOfUsed}
label={percentLabel}
/>
</div>
</div>
)
}
return <NetworkCard network={state ?? original} rootProps={props} />
},
(prev, next) => prev.className === next.className
)
Row.propTypes = {
original: PropTypes.object,
@ -88,4 +37,6 @@ Row.propTypes = {
handleClick: PropTypes.func,
}
Row.displayName = 'VirtualNetworkRow'
export default Row

View File

@ -13,43 +13,55 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo, useEffect } from 'react'
import { useMemo, ReactElement } from 'react'
import { useFetch } from 'client/hooks'
import { useVRouter, useVRouterApi } from 'client/features/One'
import { useAuth } from 'client/features/Auth'
import { useGetVRoutersQuery } from 'client/features/OneApi/vrouter'
import { SkeletonTable, EnhancedTable } from 'client/components/Tables'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import VRouterColumns from 'client/components/Tables/VRouters/columns'
import VRouterRow from 'client/components/Tables/VRouters/row'
import { RESOURCE_NAMES } from 'client/constants'
const VRoutersTable = () => {
const columns = useMemo(() => VRouterColumns, [])
const DEFAULT_DATA_CY = 'vrouters'
const vRouters = useVRouter()
const { getVRouters } = useVRouterApi()
/**
* @param {object} props - Props
* @returns {ReactElement} Virtual Routers table
*/
const VRoutersTable = (props) => {
const { rootProps = {}, searchProps = {}, ...rest } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { status, fetchRequest, loading, reloading, STATUS } =
useFetch(getVRouters)
const { INIT, PENDING } = STATUS
const { view, getResourceView } = useAuth()
const { data = [], isFetching, refetch } = useGetVRoutersQuery()
useEffect(() => {
fetchRequest()
}, [])
if (vRouters?.length === 0 && [INIT, PENDING].includes(status)) {
return <SkeletonTable />
}
const columns = useMemo(
() =>
createColumns({
filters: getResourceView(RESOURCE_NAMES.V_ROUTER)?.filters,
columns: VRouterColumns,
}),
[view]
)
return (
<EnhancedTable
columns={columns}
data={vRouters}
isLoading={loading || reloading}
data={data}
rootProps={rootProps}
searchProps={searchProps}
refetch={refetch}
isLoading={isFetching}
getRowId={(row) => String(row.ID)}
RowComponent={VRouterRow}
{...rest}
/>
)
}
VRoutersTable.propTypes = { ...EnhancedTable.propTypes }
VRoutersTable.displayName = 'VRoutersTable'
export default VRoutersTable

View File

@ -17,7 +17,6 @@
import { useMemo } from 'react'
import { useHistory } from 'react-router-dom'
import {
RefreshDouble,
AddSquare,
Import,
Trash,
@ -28,7 +27,12 @@ import {
} from 'iconoir-react'
import { useAuth } from 'client/features/Auth'
import { useVmTemplateApi } from 'client/features/One'
import {
useLockTemplateMutation,
useUnlockTemplateMutation,
useCloneTemplateMutation,
useRemoveTemplateMutation,
} from 'client/features/OneApi/vmTemplate'
import { Tr, Translate } from 'client/components/HOC'
import { CloneForm } from 'client/components/Forms/VmTemplate'
@ -63,22 +67,16 @@ MessageToConfirmAction.displayName = 'MessageToConfirmAction'
const Actions = () => {
const history = useHistory()
const { view, getResourceView } = useAuth()
const { getVmTemplate, getVmTemplates, lock, unlock, clone, remove } =
useVmTemplateApi()
const [lock] = useLockTemplateMutation()
const [unlock] = useUnlockTemplateMutation()
const [clone] = useCloneTemplateMutation()
const [remove] = useRemoveTemplateMutation()
const vmTemplateActions = useMemo(
() =>
createActions({
filters: getResourceView('VM-TEMPLATE')?.actions,
filters: getResourceView(RESOURCE_NAMES.VM_TEMPLATE)?.actions,
actions: [
{
accessor: VM_TEMPLATE_ACTIONS.REFRESH,
tooltip: T.Refresh,
icon: RefreshDouble,
action: async () => {
await getVmTemplates()
},
},
{
accessor: VM_TEMPLATE_ACTIONS.CREATE_DIALOG,
tooltip: T.Create,
@ -169,26 +167,18 @@ const Actions = () => {
return CloneForm(stepProps, initialValues)
},
onSubmit: async (formData, rows) => {
try {
const { prefix } = formData
const { prefix, ...restOfData } = formData
const vmTemplates = rows?.map?.(
({ original: { ID, NAME } = {} }) => {
// overwrite all names with prefix+NAME
const formatData = prefix
? { name: `${prefix} ${NAME}` }
: {}
const vmTemplates = rows?.map?.(
({ original: { ID, NAME } = {} }) => {
// overwrite all names with prefix+NAME
const name = prefix ? `${prefix} ${NAME}` : NAME
return { id: ID, data: { ...formData, ...formatData } }
}
)
return { id: ID, ...restOfData, name }
}
)
await Promise.all(
vmTemplates.map(({ id, data }) => clone(id, data))
)
} finally {
await getVmTemplates()
}
await Promise.all(vmTemplates.map(clone))
},
},
],
@ -245,8 +235,7 @@ const Actions = () => {
},
onSubmit: async (_, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => lock(id)))
await Promise.all(ids.map((id) => getVmTemplate(id)))
await Promise.all(ids.map((id) => lock({ id })))
},
},
{
@ -260,7 +249,6 @@ const Actions = () => {
onSubmit: async (_, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => unlock(id)))
await Promise.all(ids.map((id) => getVmTemplate(id)))
},
},
],
@ -280,8 +268,7 @@ const Actions = () => {
},
onSubmit: async (_, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => remove(id)))
await getVmTemplates()
await Promise.all(ids.map((id) => remove({ id })))
},
},
],

View File

@ -13,62 +13,55 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo, useEffect } from 'react'
import { useMemo, ReactElement } from 'react'
import { useAuth } from 'client/features/Auth'
import { useFetch } from 'client/hooks'
import { useVmTemplate, useVmTemplateApi } from 'client/features/One'
import { useGetTemplatesQuery } from 'client/features/OneApi/vmTemplate'
import {
SkeletonTable,
EnhancedTable,
EnhancedTableProps,
} from 'client/components/Tables'
import { createColumns } from 'client/components/Tables/Enhanced/Utils'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import VmTemplateColumns from 'client/components/Tables/VmTemplates/columns'
import VmTemplateRow from 'client/components/Tables/VmTemplates/row'
import { RESOURCE_NAMES } from 'client/constants'
const DEFAULT_DATA_CY = 'vm-templates'
/**
* @param {object} props - Props
* @returns {ReactElement} VM Templates table
*/
const VmTemplatesTable = (props) => {
const { view, getResourceView, filterPool } = useAuth()
const { rootProps = {}, searchProps = {}, ...rest } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useAuth()
const { data = [], isFetching, refetch } = useGetTemplatesQuery()
const columns = useMemo(
() =>
createColumns({
filters: getResourceView('VM-TEMPLATE')?.filters,
filters: getResourceView(RESOURCE_NAMES.VM_TEMPLATE)?.filters,
columns: VmTemplateColumns,
}),
[view]
)
const vmTemplates = useVmTemplate()
const { getVmTemplates } = useVmTemplateApi()
const { status, fetchRequest, loading, reloading, STATUS } =
useFetch(getVmTemplates)
const { INIT, PENDING } = STATUS
useEffect(() => {
fetchRequest()
}, [filterPool])
if (vmTemplates?.length === 0 && [INIT, PENDING].includes(status)) {
return <SkeletonTable />
}
return (
<EnhancedTable
columns={columns}
data={vmTemplates}
isLoading={loading || reloading}
data={data}
rootProps={rootProps}
searchProps={searchProps}
refetch={refetch}
isLoading={isFetching}
getRowId={(row) => String(row.ID)}
RowComponent={VmTemplateRow}
{...props}
{...rest}
/>
)
}
VmTemplatesTable.propTypes = EnhancedTableProps
VmTemplatesTable.propTypes = { ...EnhancedTable.propTypes }
VmTemplatesTable.displayName = 'VmTemplatesTable'
export default VmTemplatesTable

View File

@ -13,76 +13,34 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo } from 'react'
import { memo } from 'react'
import PropTypes from 'prop-types'
import vmTemplateApi from 'client/features/OneApi/vmTemplate'
import { VmTemplateCard } from 'client/components/Cards'
import { User, Group, Lock } from 'iconoir-react'
import { Typography } from '@mui/material'
const Row = memo(
({ original, ...props }) => {
const state = vmTemplateApi.endpoints.getTemplates.useQueryState(
undefined,
{
selectFromResult: ({ data = [] }) =>
data.find((template) => +template.ID === +original.ID),
}
)
import { StatusChip } from 'client/components/Status'
import { rowStyles } from 'client/components/Tables/styles'
import Image from 'client/components/Image'
import * as Helper from 'client/models/Helper'
import { isExternalURL } from 'client/utils'
import { LOGO_IMAGES_URL } from 'client/constants'
const Row = ({ original, value, ...props }) => {
const classes = rowStyles()
const { ID, NAME, UNAME, GNAME, REGTIME, LOCK, VROUTER, LOGO = '' } = value
const [logoSource] = useMemo(() => {
const external = isExternalURL(LOGO)
const cleanLogoAttribute = String(LOGO).split('/').at(-1)
const src = external ? LOGO : `${LOGO_IMAGES_URL}/${cleanLogoAttribute}`
return [src, external]
}, [LOGO])
const time = Helper.timeFromMilliseconds(+REGTIME)
const timeAgo = `registered ${time.toRelative()}`
return (
<div {...props} data-cy={`template-${ID}`}>
<div className={classes.figure}>
<Image
alt="logo"
src={logoSource}
imgProps={{ className: classes.image }}
/>
</div>
<div className={classes.main}>
<div className={classes.title}>
<Typography component="span">{NAME}</Typography>
<span className={classes.labels}>
{LOCK && <Lock />}
{VROUTER && <StatusChip text={VROUTER} />}
</span>
</div>
<div className={classes.caption}>
<span title={time.toFormat('ff')} className="full-width">
{`#${ID} ${timeAgo}`}
</span>
<span title={`Owner: ${UNAME}`}>
<User />
<span>{` ${UNAME}`}</span>
</span>
<span title={`Group: ${GNAME}`}>
<Group />
<span>{` ${GNAME}`}</span>
</span>
</div>
</div>
</div>
)
}
return <VmTemplateCard template={state ?? original} rootProps={props} />
},
(prev, next) => prev.className === next.className
)
Row.propTypes = {
original: PropTypes.object,
value: PropTypes.object,
isSelected: PropTypes.bool,
className: PropTypes.string,
handleClick: PropTypes.func,
}
Row.displayName = 'VmTemplateRow'
export default Row

Some files were not shown because too many files have changed in this diff Show More