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

L #-: Lint for (by) Prettier (#1633)

This commit is contained in:
Sergio Betanzos 2021-11-30 12:37:00 +01:00 committed by GitHub
parent 62c0d1fc51
commit b34b0eb642
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
703 changed files with 15728 additions and 12440 deletions

View File

@ -47,14 +47,14 @@ const ProvisionApp = () => {
const { changeAppTitle } = useGeneralApi()
useEffect(() => {
(async () => {
;(async () => {
appTitle !== APP_NAME && changeAppTitle(APP_NAME)
try {
if (jwt) {
getAuthUser()
!providerConfig && await getProviderConfig()
!provisionTemplate?.length && await getProvisionsTemplates()
!providerConfig && (await getProviderConfig())
!provisionTemplate?.length && (await getProvisionsTemplates())
}
} catch {
logout()
@ -62,10 +62,10 @@ const ProvisionApp = () => {
})()
}, [jwt])
const endpoints = useMemo(() => [
...ENDPOINTS,
...(isDevelopment() ? DEV_ENDPOINTS : [])
], [])
const endpoints = useMemo(
() => [...ENDPOINTS, ...(isDevelopment() ? DEV_ENDPOINTS : [])],
[]
)
if (jwt && firstRender) {
return <LoadingScreen />

View File

@ -67,7 +67,7 @@ const Provision = ({ store = {}, location = '', context = {} }) => (
Provision.propTypes = {
location: PropTypes.string,
context: PropTypes.object,
store: PropTypes.object
store: PropTypes.object,
}
Provision.displayName = 'ProvisionApp'

View File

@ -17,34 +17,49 @@ import {
ReportColumns as DashboardIcon,
DatabaseSettings as ProvidersIcon,
SettingsCloud as ProvisionsIcon,
Settings as SettingsIcon
Settings as SettingsIcon,
} from 'iconoir-react'
import loadable from '@loadable/component'
const Dashboard = loadable(() => import('client/containers/Dashboard/Provision'), { ssr: false })
const Dashboard = loadable(
() => import('client/containers/Dashboard/Provision'),
{ ssr: false }
)
const Providers = loadable(() => import('client/containers/Providers'), { ssr: false })
const CreateProvider = loadable(() => import('client/containers/Providers/Create'), { ssr: false })
const Providers = loadable(() => import('client/containers/Providers'), {
ssr: false,
})
const CreateProvider = loadable(
() => import('client/containers/Providers/Create'),
{ ssr: false }
)
const Provisions = loadable(() => import('client/containers/Provisions'), { ssr: false })
const CreateProvision = loadable(() => import('client/containers/Provisions/Create'), { ssr: false })
const Provisions = loadable(() => import('client/containers/Provisions'), {
ssr: false,
})
const CreateProvision = loadable(
() => import('client/containers/Provisions/Create'),
{ ssr: false }
)
const Settings = loadable(() => import('client/containers/Settings'), { ssr: false })
const Settings = loadable(() => import('client/containers/Settings'), {
ssr: false,
})
export const PATH = {
DASHBOARD: '/dashboard',
PROVIDERS: {
LIST: '/providers',
CREATE: '/providers/create',
EDIT: '/providers/edit/:id'
EDIT: '/providers/edit/:id',
},
PROVISIONS: {
LIST: '/provisions',
CREATE: '/provisions/create',
EDIT: '/provisions/edit/:id'
EDIT: '/provisions/edit/:id',
},
SETTINGS: '/settings'
SETTINGS: '/settings',
}
export const ENDPOINTS = [
@ -53,49 +68,49 @@ export const ENDPOINTS = [
path: PATH.DASHBOARD,
sidebar: true,
icon: DashboardIcon,
Component: Dashboard
Component: Dashboard,
},
{
label: 'Providers',
path: PATH.PROVIDERS.LIST,
sidebar: true,
icon: ProvidersIcon,
Component: Providers
Component: Providers,
},
{
label: 'Create Provider',
path: PATH.PROVIDERS.CREATE,
Component: CreateProvider
Component: CreateProvider,
},
{
label: 'Edit Provider template',
path: PATH.PROVIDERS.EDIT,
Component: CreateProvider
Component: CreateProvider,
},
{
label: 'Provisions',
path: PATH.PROVISIONS.LIST,
sidebar: true,
icon: ProvisionsIcon,
Component: Provisions
Component: Provisions,
},
{
label: 'Create Provision',
path: PATH.PROVISIONS.CREATE,
Component: CreateProvision
Component: CreateProvision,
},
{
label: 'Edit Provision template',
path: PATH.PROVISIONS.EDIT,
Component: CreateProvision
Component: CreateProvision,
},
{
label: 'Settings',
path: PATH.SETTINGS,
sidebar: true,
icon: SettingsIcon,
Component: Settings
}
Component: Settings,
},
]
export default { PATH, ENDPOINTS }

View File

@ -22,7 +22,7 @@ export default {
light: '#2a2d3d',
main: '#222431',
dark: '#191924',
contrastText: '#ffffff'
contrastText: '#ffffff',
},
secondary: {
100: '#ffeae4',
@ -37,7 +37,7 @@ export default {
light: '#ffd6c8',
main: '#fe835a',
dark: '#fe5a23',
contrastText: '#ffffff'
}
}
contrastText: '#ffffff',
},
},
}

View File

@ -16,7 +16,11 @@
import { useEffect, useMemo, JSXElementConstructor } from 'react'
import Router from 'client/router'
import { ENDPOINTS, PATH, getEndpointsByView } from 'client/apps/sunstone/routes'
import {
ENDPOINTS,
PATH,
getEndpointsByView,
} from 'client/apps/sunstone/routes'
import { ENDPOINTS as ONE_ENDPOINTS } from 'client/apps/sunstone/routesOne'
import { ENDPOINTS as DEV_ENDPOINTS } from 'client/router/dev'
@ -39,7 +43,8 @@ export const APP_NAME = _APPS.sunstone.name
*/
const SunstoneApp = () => {
const { isLogged, jwt, firstRender, view, views, config } = useAuth()
const { getAuthUser, logout, getSunstoneViews, getSunstoneConfig } = useAuthApi()
const { getAuthUser, logout, getSunstoneViews, getSunstoneConfig } =
useAuthApi()
const { appTitle } = useGeneral()
const { changeAppTitle } = useGeneralApi()
@ -47,14 +52,14 @@ const SunstoneApp = () => {
const { getOneConfig } = useSystemApi()
useEffect(() => {
(async () => {
;(async () => {
appTitle !== APP_NAME && changeAppTitle(APP_NAME)
try {
if (jwt) {
getAuthUser()
!view && await getSunstoneViews()
!config && await getSunstoneConfig()
!view && (await getSunstoneViews())
!config && (await getSunstoneConfig())
!oneConfig && getOneConfig()
}
} catch {
@ -63,11 +68,14 @@ const SunstoneApp = () => {
})()
}, [jwt])
const endpoints = useMemo(() => [
...ENDPOINTS,
...(view ? getEndpointsByView(views?.[view], ONE_ENDPOINTS) : []),
...(isDevelopment() ? DEV_ENDPOINTS : [])
], [view])
const endpoints = useMemo(
() => [
...ENDPOINTS,
...(view ? getEndpointsByView(views?.[view], ONE_ENDPOINTS) : []),
...(isDevelopment() ? DEV_ENDPOINTS : []),
],
[view]
)
if (jwt && firstRender) {
return <LoadingScreen />

View File

@ -45,12 +45,12 @@ const Sunstone = ({ store = {}, location = '', context = {} }) => (
<MuiProvider theme={theme}>
<NotistackProvider>
{location && context ? (
// server build
// server build
<StaticRouter location={location} context={context}>
<App />
</StaticRouter>
) : (
// browser build
// browser build
<BrowserRouter basename={`${APP_URL}/${SunstoneAppName}`}>
<App />
</BrowserRouter>
@ -64,7 +64,7 @@ const Sunstone = ({ store = {}, location = '', context = {} }) => (
Sunstone.propTypes = {
location: PropTypes.string,
context: PropTypes.shape({}),
store: PropTypes.shape({})
store: PropTypes.shape({}),
}
Sunstone.displayName = 'SunstoneApp'

View File

@ -15,17 +15,22 @@
* ------------------------------------------------------------------------- */
import {
ReportColumns as DashboardIcon,
Settings as SettingsIcon
Settings as SettingsIcon,
} from 'iconoir-react'
import loadable from '@loadable/component'
const Dashboard = loadable(() => import('client/containers/Dashboard/Sunstone'), { ssr: false })
const Settings = loadable(() => import('client/containers/Settings'), { ssr: false })
const Dashboard = loadable(
() => import('client/containers/Dashboard/Sunstone'),
{ ssr: false }
)
const Settings = loadable(() => import('client/containers/Settings'), {
ssr: false,
})
export const PATH = {
DASHBOARD: '/dashboard',
SETTINGS: '/settings'
SETTINGS: '/settings',
}
export const ENDPOINTS = [
@ -35,7 +40,7 @@ export const ENDPOINTS = [
sidebar: true,
icon: DashboardIcon,
position: 1,
Component: Dashboard
Component: Dashboard,
},
{
label: 'Settings',
@ -43,8 +48,8 @@ export const ENDPOINTS = [
sidebar: true,
icon: SettingsIcon,
position: -1,
Component: Settings
}
Component: Settings,
},
]
/**
@ -69,7 +74,7 @@ export const getEndpointsByView = (views, endpoints = []) => {
* @param {string} [route.path] - Pathname route
* @returns {boolean | object} If user view yaml contains the route, return it
*/
const hasRoutePermission = route =>
const hasRoutePermission = (route) =>
views?.some(({ resource_name: name = '', actions: bulkActions = [] }) => {
// eg: '/vm-template/instantiate' => ['vm-template', 'instantiate']
const paths = route?.path

View File

@ -15,7 +15,7 @@
* ------------------------------------------------------------------------- */
import {
List as TemplatesIcons,
Cell4X4 as InstancesIcons
Cell4X4 as InstancesIcons,
} from 'iconoir-react'
import loadable from '@loadable/component'
@ -39,11 +39,11 @@ export const PATH = {
APPLICATIONS_TEMPLATES: {
LIST: '/applications-templates',
CREATE: '/applications-templates/create',
EDIT: '/applications-templates/edit/:id'
EDIT: '/applications-templates/edit/:id',
},
APPLICATIONS: {
LIST: '/applications'
}
LIST: '/applications',
},
}
export const ENDPOINTS = [
@ -52,25 +52,25 @@ export const ENDPOINTS = [
path: PATH.APPLICATIONS_TEMPLATES.LIST,
sidebar: true,
icon: TemplatesIcons,
Component: ApplicationsTemplates
Component: ApplicationsTemplates,
},
{
label: 'Create Application template',
path: PATH.APPLICATIONS_TEMPLATES.CREATE,
Component: ApplicationsTemplatesFormCreate
Component: ApplicationsTemplatesFormCreate,
},
{
label: 'Edit Application template',
path: PATH.APPLICATIONS_TEMPLATES.EDIT,
Component: ApplicationsTemplatesFormCreate
Component: ApplicationsTemplatesFormCreate,
},
{
label: 'Service Instances',
path: PATH.APPLICATIONS.LIST,
sidebar: true,
icon: InstancesIcons,
Component: ApplicationsInstances
}
Component: ApplicationsInstances,
},
]
export default { PATH, ENDPOINTS }

View File

@ -17,66 +17,107 @@ import {
Cell4X4 as InstancesIcons,
ModernTv as VmsIcons,
Shuffle as VRoutersIcons,
Archive as TemplatesIcon,
GoogleDocs as TemplateIcon,
Box as StorageIcon,
Db as DatastoreIcon,
BoxIso as ImageIcon,
SimpleCart as MarketplaceIcon,
CloudDownload as MarketplaceAppIcon,
ServerConnection as NetworksIcon,
NetworkAlt as NetworkIcon,
KeyframesCouple as NetworkTemplateIcon,
CloudSync as InfrastructureIcon,
Server as ClusterIcon,
HardDrive as HostIcon,
MinusPinAlt as ZoneIcon,
Home as SystemIcon,
User as UserIcon,
Group as GroupIcon
Group as GroupIcon,
} from 'iconoir-react'
import loadable from '@loadable/component'
import { RESOURCE_NAMES } from 'client/constants'
const VirtualMachines = loadable(() => import('client/containers/VirtualMachines'), { ssr: false })
const VirtualMachineDetail = loadable(() => import('client/containers/VirtualMachines/Detail'), { ssr: false })
const VirtualRouters = loadable(() => import('client/containers/VirtualRouters'), { ssr: false })
const VirtualMachines = loadable(
() => import('client/containers/VirtualMachines'),
{ ssr: false }
)
const VirtualMachineDetail = loadable(
() => import('client/containers/VirtualMachines/Detail'),
{ ssr: false }
)
const VirtualRouters = loadable(
() => import('client/containers/VirtualRouters'),
{ ssr: false }
)
const VmTemplates = loadable(() => import('client/containers/VmTemplates'), { ssr: false })
const InstantiateVmTemplate =
loadable(() => import('client/containers/VmTemplates/Instantiate'), { ssr: false })
const CreateVmTemplate = loadable(() => import('client/containers/VmTemplates/Create'), { ssr: false })
const VmTemplates = loadable(() => import('client/containers/VmTemplates'), {
ssr: false,
})
const InstantiateVmTemplate = loadable(
() => import('client/containers/VmTemplates/Instantiate'),
{ ssr: false }
)
const CreateVmTemplate = loadable(
() => import('client/containers/VmTemplates/Create'),
{ ssr: false }
)
// const VrTemplates = loadable(() => import('client/containers/VrTemplates'), { ssr: false })
// const VmGroups = loadable(() => import('client/containers/VmGroups'), { ssr: false })
const Datastores = loadable(() => import('client/containers/Datastores'), { ssr: false })
const Images = loadable(() => import('client/containers/Images'), { ssr: false })
const Marketplaces = loadable(() => import('client/containers/Marketplaces'), { ssr: false })
const MarketplaceApps = loadable(() => import('client/containers/MarketplaceApps'), { ssr: false })
const CreateMarketplaceApp = loadable(() => import('client/containers/MarketplaceApps/Create'), { ssr: false })
const Datastores = loadable(() => import('client/containers/Datastores'), {
ssr: false,
})
const Images = loadable(() => import('client/containers/Images'), {
ssr: false,
})
const Marketplaces = loadable(() => import('client/containers/Marketplaces'), {
ssr: false,
})
const MarketplaceApps = loadable(
() => import('client/containers/MarketplaceApps'),
{ ssr: false }
)
const CreateMarketplaceApp = loadable(
() => import('client/containers/MarketplaceApps/Create'),
{ ssr: false }
)
const VirtualNetworks = loadable(() => import('client/containers/VirtualNetworks'), { ssr: false })
const VNetworkTemplates = loadable(() => import('client/containers/VNetworkTemplates'), { ssr: false })
const VirtualNetworks = loadable(
() => import('client/containers/VirtualNetworks'),
{ ssr: false }
)
const VNetworkTemplates = loadable(
() => import('client/containers/VNetworkTemplates'),
{ ssr: false }
)
// const NetworkTopologies = loadable(() => import('client/containers/NetworkTopologies'), { ssr: false })
// const SecurityGroups = loadable(() => import('client/containers/SecurityGroups'), { ssr: false })
const Clusters = loadable(() => import('client/containers/Clusters'), { ssr: false })
const ClusterDetail = loadable(() => import('client/containers/Clusters/Detail'), { ssr: false })
const Clusters = loadable(() => import('client/containers/Clusters'), {
ssr: false,
})
const ClusterDetail = loadable(
() => import('client/containers/Clusters/Detail'),
{ ssr: false }
)
const Hosts = loadable(() => import('client/containers/Hosts'), { ssr: false })
const HostDetail = loadable(() => import('client/containers/Hosts/Detail'), { ssr: false })
const HostDetail = loadable(() => import('client/containers/Hosts/Detail'), {
ssr: false,
})
const Zones = loadable(() => import('client/containers/Zones'), { ssr: false })
const Users = loadable(() => import('client/containers/Users'), { ssr: false })
const UserDetail = loadable(() => import('client/containers/Users/Detail'), { ssr: false })
const Groups = loadable(() => import('client/containers/Groups'), { ssr: false })
const GroupDetail = loadable(() => import('client/containers/Groups/Detail'), { ssr: false })
const UserDetail = loadable(() => import('client/containers/Users/Detail'), {
ssr: false,
})
const Groups = loadable(() => import('client/containers/Groups'), {
ssr: false,
})
const GroupDetail = loadable(() => import('client/containers/Groups/Detail'), {
ssr: false,
})
// const VDCs = loadable(() => import('client/containers/VDCs'), { ssr: false })
// const ACLs = loadable(() => import('client/containers/ACLs'), { ssr: false })
@ -84,77 +125,77 @@ export const PATH = {
INSTANCE: {
VMS: {
LIST: `/${RESOURCE_NAMES.VM}`,
DETAIL: `/${RESOURCE_NAMES.VM}/:id`
DETAIL: `/${RESOURCE_NAMES.VM}/:id`,
},
VROUTERS: {
LIST: `/${RESOURCE_NAMES.V_ROUTER}`
}
LIST: `/${RESOURCE_NAMES.V_ROUTER}`,
},
},
TEMPLATE: {
VMS: {
LIST: `/${RESOURCE_NAMES.VM_TEMPLATE}`,
DETAIL: `/${RESOURCE_NAMES.VM_TEMPLATE}/:id`,
INSTANTIATE: `/${RESOURCE_NAMES.VM_TEMPLATE}/instantiate`,
CREATE: `/${RESOURCE_NAMES.VM_TEMPLATE}/create`
}
CREATE: `/${RESOURCE_NAMES.VM_TEMPLATE}/create`,
},
},
STORAGE: {
DATASTORES: {
LIST: `/${RESOURCE_NAMES.DATASTORE}`,
DETAIL: `/${RESOURCE_NAMES.DATASTORE}/:id`
DETAIL: `/${RESOURCE_NAMES.DATASTORE}/:id`,
},
IMAGES: {
LIST: `/${RESOURCE_NAMES.IMAGE}`,
DETAIL: `/${RESOURCE_NAMES.IMAGE}/:id`
DETAIL: `/${RESOURCE_NAMES.IMAGE}/:id`,
},
MARKETPLACES: {
LIST: `/${RESOURCE_NAMES.MARKETPLACE}`,
DETAIL: `/${RESOURCE_NAMES.MARKETPLACE}/:id`
DETAIL: `/${RESOURCE_NAMES.MARKETPLACE}/:id`,
},
MARKETPLACE_APPS: {
LIST: `/${RESOURCE_NAMES.APP}`,
DETAIL: `/${RESOURCE_NAMES.APP}/:id`,
CREATE: `/${RESOURCE_NAMES.APP}/create`
}
CREATE: `/${RESOURCE_NAMES.APP}/create`,
},
},
NETWORK: {
VNETS: {
LIST: `/${RESOURCE_NAMES.VNET}`,
DETAIL: `/${RESOURCE_NAMES.VNET}/:id`
DETAIL: `/${RESOURCE_NAMES.VNET}/:id`,
},
VN_TEMPLATES: {
LIST: `/${RESOURCE_NAMES.VN_TEMPLATE}`,
DETAIL: `/${RESOURCE_NAMES.VN_TEMPLATE}/:id`
DETAIL: `/${RESOURCE_NAMES.VN_TEMPLATE}/:id`,
},
SEC_GROUPS: {
LIST: `/${RESOURCE_NAMES.SEC_GROUP}`,
DETAIL: `/${RESOURCE_NAMES.SEC_GROUP}/:id`
}
DETAIL: `/${RESOURCE_NAMES.SEC_GROUP}/:id`,
},
},
INFRASTRUCTURE: {
CLUSTERS: {
LIST: `/${RESOURCE_NAMES.CLUSTER}`,
DETAIL: `/${RESOURCE_NAMES.CLUSTER}/:id`
DETAIL: `/${RESOURCE_NAMES.CLUSTER}/:id`,
},
HOSTS: {
LIST: `/${RESOURCE_NAMES.HOST}`,
DETAIL: `/${RESOURCE_NAMES.HOST}/:id`
DETAIL: `/${RESOURCE_NAMES.HOST}/:id`,
},
ZONES: {
LIST: `/${RESOURCE_NAMES.ZONE}`,
DETAIL: `/${RESOURCE_NAMES.ZONE}/:id`
}
DETAIL: `/${RESOURCE_NAMES.ZONE}/:id`,
},
},
SYSTEM: {
USERS: {
LIST: `/${RESOURCE_NAMES.USER}`,
DETAIL: `/${RESOURCE_NAMES.USER}/:id`
DETAIL: `/${RESOURCE_NAMES.USER}/:id`,
},
GROUPS: {
LIST: `/${RESOURCE_NAMES.GROUP}`,
DETAIL: `/${RESOURCE_NAMES.GROUP}/:id`
}
}
DETAIL: `/${RESOURCE_NAMES.GROUP}/:id`,
},
},
}
const ENDPOINTS = [
@ -168,21 +209,21 @@ const ENDPOINTS = [
path: PATH.INSTANCE.VMS.LIST,
sidebar: true,
icon: VmsIcons,
Component: VirtualMachines
Component: VirtualMachines,
},
{
label: params => `VM #${params.id}`,
label: (params) => `VM #${params.id}`,
path: PATH.INSTANCE.VMS.DETAIL,
Component: VirtualMachineDetail
Component: VirtualMachineDetail,
},
{
label: 'Virtual Routers',
path: PATH.INSTANCE.VROUTERS.LIST,
sidebar: true,
icon: VRoutersIcons,
Component: VirtualRouters
}
]
Component: VirtualRouters,
},
],
},
{
label: 'Templates',
@ -194,19 +235,19 @@ const ENDPOINTS = [
path: PATH.TEMPLATE.VMS.LIST,
sidebar: true,
icon: TemplateIcon,
Component: VmTemplates
Component: VmTemplates,
},
{
label: 'Instantiate VM Template',
path: PATH.TEMPLATE.VMS.INSTANTIATE,
Component: InstantiateVmTemplate
Component: InstantiateVmTemplate,
},
{
label: 'Create VM Template',
path: PATH.TEMPLATE.VMS.CREATE,
Component: CreateVmTemplate
}
]
Component: CreateVmTemplate,
},
],
},
{
label: 'Storage',
@ -218,35 +259,35 @@ const ENDPOINTS = [
path: PATH.STORAGE.DATASTORES.LIST,
sidebar: true,
icon: DatastoreIcon,
Component: Datastores
Component: Datastores,
},
{
label: 'Images',
path: PATH.STORAGE.IMAGES.LIST,
sidebar: true,
icon: ImageIcon,
Component: Images
Component: Images,
},
{
label: 'Marketplaces',
path: PATH.STORAGE.MARKETPLACES.LIST,
sidebar: true,
icon: MarketplaceIcon,
Component: Marketplaces
Component: Marketplaces,
},
{
label: 'Apps',
path: PATH.STORAGE.MARKETPLACE_APPS.LIST,
sidebar: true,
icon: MarketplaceAppIcon,
Component: MarketplaceApps
Component: MarketplaceApps,
},
{
label: 'Create Marketplace App',
path: PATH.STORAGE.MARKETPLACE_APPS.CREATE,
Component: CreateMarketplaceApp
}
]
Component: CreateMarketplaceApp,
},
],
},
{
label: 'Networks',
@ -258,16 +299,16 @@ const ENDPOINTS = [
path: PATH.NETWORK.VNETS.LIST,
sidebar: true,
icon: NetworkIcon,
Component: VirtualNetworks
Component: VirtualNetworks,
},
{
label: 'Network Templates',
path: PATH.NETWORK.VN_TEMPLATES.LIST,
sidebar: true,
icon: NetworkTemplateIcon,
Component: VNetworkTemplates
}
]
Component: VNetworkTemplates,
},
],
},
{
label: 'Infrastructure',
@ -279,33 +320,33 @@ const ENDPOINTS = [
path: PATH.INFRASTRUCTURE.CLUSTERS.LIST,
sidebar: true,
icon: ClusterIcon,
Component: Clusters
Component: Clusters,
},
{
label: params => `Clusters #${params.id}`,
label: (params) => `Clusters #${params.id}`,
path: PATH.INFRASTRUCTURE.CLUSTERS.DETAIL,
Component: ClusterDetail
Component: ClusterDetail,
},
{
label: 'Hosts',
path: PATH.INFRASTRUCTURE.HOSTS.LIST,
sidebar: true,
icon: HostIcon,
Component: Hosts
Component: Hosts,
},
{
label: params => `Hosts #${params.id}`,
label: (params) => `Hosts #${params.id}`,
path: PATH.INFRASTRUCTURE.HOSTS.DETAIL,
Component: HostDetail
Component: HostDetail,
},
{
label: 'Zones',
path: PATH.INFRASTRUCTURE.ZONES.LIST,
sidebar: true,
icon: ZoneIcon,
Component: Zones
}
]
Component: Zones,
},
],
},
{
label: 'System',
@ -317,27 +358,27 @@ const ENDPOINTS = [
path: PATH.SYSTEM.USERS.LIST,
sidebar: true,
icon: UserIcon,
Component: Users
Component: Users,
},
{
label: params => `User #${params.id}`,
label: (params) => `User #${params.id}`,
path: PATH.SYSTEM.USERS.DETAIL,
Component: UserDetail
Component: UserDetail,
},
{
label: 'Groups',
path: PATH.SYSTEM.GROUPS.LIST,
sidebar: true,
icon: GroupIcon,
Component: Groups
Component: Groups,
},
{
label: params => `Group #${params.id}`,
label: (params) => `Group #${params.id}`,
path: PATH.SYSTEM.GROUPS.DETAIL,
Component: GroupDetail
}
]
}
Component: GroupDetail,
},
],
},
]
export { ENDPOINTS }

View File

@ -22,7 +22,7 @@ export default {
light: '#2a2d3d',
main: '#222431',
dark: '#191924',
contrastText: '#ffffff'
contrastText: '#ffffff',
},
secondary: {
100: '#dff2f8',
@ -37,7 +37,7 @@ export default {
light: '#bfe6f0',
main: '#40b3da',
dark: '#0099c3',
contrastText: '#fff'
}
}
contrastText: '#fff',
},
},
}

View File

@ -28,11 +28,11 @@ const AlertError = ({ children, ...props }) => (
)
AlertError.propTypes = {
children: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
}
AlertError.defaultProps = {
children: 'Error!'
children: 'Error!',
}
export default AlertError

View File

@ -20,112 +20,114 @@ import { Chip, Slide } from '@mui/material'
import makeStyles from '@mui/styles/makeStyles'
import { Download as GoToBottomIcon } from 'iconoir-react'
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
scrollable: {
padding: theme.spacing(1),
overflowY: 'scroll',
'&::-webkit-scrollbar': {
width: 14
width: 14,
},
'&::-webkit-scrollbar-thumb': {
backgroundClip: 'content-box',
border: '4px solid transparent',
borderRadius: 7,
boxShadow: 'inset 0 0 0 10px',
color: theme.palette.secondary.light
}
color: theme.palette.secondary.light,
},
},
wrapperButton: {
top: 5,
position: 'sticky',
textAlign: 'center'
textAlign: 'center',
},
button: { padding: theme.spacing(0, 2) }
button: { padding: theme.spacing(0, 2) },
}))
const AutoScrollBox = memo(({
children,
className,
height,
autoButtonText,
preventInteraction,
scrollBehavior,
showOption,
dataCy
}) => {
const classes = useStyles()
const [autoScroll, setAutoScroll] = useState(true)
const containerElement = useRef(null)
const style = {
const AutoScrollBox = memo(
({
children,
className,
height,
scrollBehavior: 'auto',
pointerEvents: preventInteraction ? 'none' : 'auto'
}
autoButtonText,
preventInteraction,
scrollBehavior,
showOption,
dataCy,
}) => {
const classes = useStyles()
const [autoScroll, setAutoScroll] = useState(true)
const containerElement = useRef(null)
/**
* Handle mousewheel events on the scroll container.
*/
const onWheel = () => {
const { current } = containerElement
if (current && showOption) {
setAutoScroll(
current.scrollTop + current.offsetHeight === current.scrollHeight
)
const style = {
height,
scrollBehavior: 'auto',
pointerEvents: preventInteraction ? 'none' : 'auto',
}
}
// Apply the scroll behavior property after the first render,
// so that the initial render is scrolled all the way to the bottom.
useEffect(() => {
setTimeout(() => {
/**
* Handle mousewheel events on the scroll container.
*/
const onWheel = () => {
const { current } = containerElement
if (current && showOption) {
setAutoScroll(
current.scrollTop + current.offsetHeight === current.scrollHeight
)
}
}
// Apply the scroll behavior property after the first render,
// so that the initial render is scrolled all the way to the bottom.
useEffect(() => {
setTimeout(() => {
const { current } = containerElement
if (current) {
current.style.scrollBehavior = scrollBehavior
}
}, 0)
}, [containerElement, scrollBehavior])
// When the children are updated, scroll the container to the bottom.
useEffect(() => {
if (!autoScroll) {
return
}
const { current } = containerElement
if (current) {
current.style.scrollBehavior = scrollBehavior
current.scrollTop = current.scrollHeight
}
}, 0)
}, [containerElement, scrollBehavior])
}, [children, containerElement, autoScroll])
// When the children are updated, scroll the container to the bottom.
useEffect(() => {
if (!autoScroll) {
return
}
const { current } = containerElement
if (current) {
current.scrollTop = current.scrollHeight
}
}, [children, containerElement, autoScroll])
return (
<div style={{ height }} className={className}>
<div
className={classes.scrollable}
onWheel={onWheel}
ref={containerElement}
style={style}
data-cy={dataCy}
>
<Slide in={!autoScroll} direction="down" mountOnEnter unmountOnExit>
<div className={classes.wrapperButton}>
<Chip
avatar={<GoToBottomIcon />}
color='secondary'
className={classes.button}
label={autoButtonText}
onClick={() => setAutoScroll(true)}
/>
</div>
</Slide>
{children}
return (
<div style={{ height }} className={className}>
<div
className={classes.scrollable}
onWheel={onWheel}
ref={containerElement}
style={style}
data-cy={dataCy}
>
<Slide in={!autoScroll} direction="down" mountOnEnter unmountOnExit>
<div className={classes.wrapperButton}>
<Chip
avatar={<GoToBottomIcon />}
color="secondary"
className={classes.button}
label={autoButtonText}
onClick={() => setAutoScroll(true)}
/>
</div>
</Slide>
{children}
</div>
</div>
</div>
)
})
)
}
)
AutoScrollBox.propTypes = {
// Children to render in the scroll container.
@ -133,10 +135,7 @@ AutoScrollBox.propTypes = {
// Extra CSS class names.
className: PropTypes.object,
// Height value of the scroll container.
height: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]),
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
// Text to use for the auto scroll option.
autoButtonText: PropTypes.string,
// Prevent all mouse interaction with the scroll container.
@ -145,7 +144,7 @@ AutoScrollBox.propTypes = {
scrollBehavior: PropTypes.oneOf(['smooth', 'auto']),
// Show the auto scroll option.
showOption: PropTypes.bool,
dataCy: PropTypes.string
dataCy: PropTypes.string,
}
AutoScrollBox.defaultProps = {
@ -156,7 +155,7 @@ AutoScrollBox.defaultProps = {
preventInteraction: false,
scrollBehavior: 'smooth',
showOption: true,
dataCy: 'auto-scroll'
dataCy: 'auto-scroll',
}
AutoScrollBox.displayName = 'AutoScrollBox'

View File

@ -24,52 +24,55 @@ import SelectCard from 'client/components/Cards/SelectCard'
import { Tr } from 'client/components/HOC'
import { T, APPLICATION_STATES } from 'client/constants'
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
content: {
display: 'flex',
gap: theme.spacing(1)
}
gap: theme.spacing(1),
},
}))
const ApplicationCard = memo(
({ value, handleShow, handleRemove }) => {
const classes = useStyles()
const { ID, NAME, TEMPLATE } = value
const { description, state } = TEMPLATE.BODY
const ApplicationCard = memo(({ value, handleShow, handleRemove }) => {
const classes = useStyles()
const { ID, NAME, TEMPLATE } = value
const { description, state } = TEMPLATE.BODY
const stateInfo = APPLICATION_STATES[state]
const stateInfo = APPLICATION_STATES[state]
return (
<SelectCard
icon={<FileIcon />}
title={`${ID} - ${NAME}`}
subheader={description}
>
<CardContent>
<Box className={classes.content}>
<Chip
size="small"
label={stateInfo?.name}
style={{ backgroundColor: stateInfo?.color }}
/>
</Box>
</CardContent>
<CardActions>
{handleShow && (
<Button variant="contained" size="small" onClick={handleShow} disableElevation>
{Tr(T.Info)}
</Button>
)}
{handleRemove && (
<Button size="small" onClick={handleRemove} disableElevation>
{Tr(T.Remove)}
</Button>
)}
</CardActions>
</SelectCard>
)
}
)
return (
<SelectCard
icon={<FileIcon />}
title={`${ID} - ${NAME}`}
subheader={description}
>
<CardContent>
<Box className={classes.content}>
<Chip
size="small"
label={stateInfo?.name}
style={{ backgroundColor: stateInfo?.color }}
/>
</Box>
</CardContent>
<CardActions>
{handleShow && (
<Button
variant="contained"
size="small"
onClick={handleShow}
disableElevation
>
{Tr(T.Info)}
</Button>
)}
{handleRemove && (
<Button size="small" onClick={handleRemove} disableElevation>
{Tr(T.Remove)}
</Button>
)}
</CardActions>
</SelectCard>
)
})
ApplicationCard.propTypes = {
value: PropTypes.shape({
@ -80,18 +83,18 @@ ApplicationCard.propTypes = {
description: PropTypes.string,
state: PropTypes.number,
networks: PropTypes.object,
roles: PropTypes.arrayOf(PropTypes.object)
}).isRequired
}).isRequired
roles: PropTypes.arrayOf(PropTypes.object),
}).isRequired,
}).isRequired,
}),
handleShow: PropTypes.func,
handleRemove: PropTypes.func
handleRemove: PropTypes.func,
}
ApplicationCard.defaultProps = {
value: {},
handleShow: undefined,
handleRemove: undefined
handleRemove: undefined,
}
ApplicationCard.displayName = 'Application TemplateCard'

View File

@ -22,54 +22,55 @@ import SelectCard from 'client/components/Cards/SelectCard'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
const ApplicationNetworkCard = memo(({
value,
isSelected,
handleClick,
handleEdit,
handleClone,
handleRemove
}) => {
const { mandatory, name, description } = value
const ApplicationNetworkCard = memo(
({
value,
isSelected,
handleClick,
handleEdit,
handleClone,
handleRemove,
}) => {
const { mandatory, name, description } = value
return (
<SelectCard
icon={mandatory ? 'M' : undefined}
title={name}
subheader={description}
isSelected={isSelected}
handleClick={handleClick}
>
<CardActions>
{handleEdit && (
<Button
variant="contained"
size="small"
onClick={handleEdit}
disableElevation
>
{Tr(T.Edit)}
</Button>
)}
{handleClone && (
<Button
variant="contained"
size="small"
onClick={handleClone}
disableElevation
>
{Tr(T.Clone)}
</Button>
)}
{handleRemove && (
<Button size="small" onClick={handleRemove} disableElevation>
{Tr(T.Remove)}
</Button>
)}
</CardActions>
</SelectCard>
)
}
return (
<SelectCard
icon={mandatory ? 'M' : undefined}
title={name}
subheader={description}
isSelected={isSelected}
handleClick={handleClick}
>
<CardActions>
{handleEdit && (
<Button
variant="contained"
size="small"
onClick={handleEdit}
disableElevation
>
{Tr(T.Edit)}
</Button>
)}
{handleClone && (
<Button
variant="contained"
size="small"
onClick={handleClone}
disableElevation
>
{Tr(T.Clone)}
</Button>
)}
{handleRemove && (
<Button size="small" onClick={handleRemove} disableElevation>
{Tr(T.Remove)}
</Button>
)}
</CardActions>
</SelectCard>
)
}
)
ApplicationNetworkCard.propTypes = {
@ -79,13 +80,13 @@ ApplicationNetworkCard.propTypes = {
description: PropTypes.string,
type: PropTypes.string,
id: PropTypes.string,
extra: PropTypes.string
extra: PropTypes.string,
}),
isSelected: PropTypes.bool,
handleClick: PropTypes.func,
handleEdit: PropTypes.func,
handleClone: PropTypes.func,
handleRemove: PropTypes.func
handleRemove: PropTypes.func,
}
ApplicationNetworkCard.defaultProps = {
@ -94,7 +95,7 @@ ApplicationNetworkCard.defaultProps = {
handleClick: undefined,
handleEdit: undefined,
handleClone: undefined,
handleRemove: undefined
handleRemove: undefined,
}
ApplicationNetworkCard.displayName = 'ApplicationNetworkCard'

View File

@ -21,18 +21,18 @@ import makeStyles from '@mui/styles/makeStyles'
import {
Page as FileIcon,
HardDrive as HostIcon,
Network as NetworkIcon
Network as NetworkIcon,
} from 'iconoir-react'
import SelectCard from 'client/components/Cards/SelectCard'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
badgesWrapper: {
display: 'flex',
gap: theme.typography.pxToRem(12)
}
gap: theme.typography.pxToRem(12),
},
}))
const ApplicationTemplateCard = memo(
@ -47,11 +47,7 @@ const ApplicationTemplateCard = memo(
const badgePosition = { vertical: 'top', horizontal: 'right' }
return (
<SelectCard
icon={<FileIcon />}
title={NAME}
subheader={description}
>
<SelectCard icon={<FileIcon />} title={NAME} subheader={description}>
<CardContent>
<Box className={classes.badgesWrapper}>
<Badge
@ -126,14 +122,14 @@ ApplicationTemplateCard.propTypes = {
BODY: PropTypes.shape({
description: PropTypes.string,
networks: PropTypes.object,
roles: PropTypes.arrayOf(PropTypes.object)
}).isRequired
}).isRequired
roles: PropTypes.arrayOf(PropTypes.object),
}).isRequired,
}).isRequired,
}),
handleEdit: PropTypes.func,
handleDeploy: PropTypes.func,
handleShow: PropTypes.func,
handleRemove: PropTypes.func
handleRemove: PropTypes.func,
}
ApplicationTemplateCard.defaultProps = {
@ -141,7 +137,7 @@ ApplicationTemplateCard.defaultProps = {
handleEdit: undefined,
handleDeploy: undefined,
handleShow: undefined,
handleRemove: undefined
handleRemove: undefined,
}
ApplicationTemplateCard.displayName = 'Application TemplateCard'

View File

@ -22,18 +22,18 @@ import {
Server as ClusterIcon,
HardDrive as HostIcon,
NetworkAlt as NetworkIcon,
Folder as DatastoreIcon
Folder as DatastoreIcon,
} from 'iconoir-react'
import { SelectCard } from 'client/components/Cards'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
badgesWrapper: {
display: 'flex',
gap: theme.typography.pxToRem(12)
}
gap: theme.typography.pxToRem(12),
},
}))
const ClusterCard = memo(
@ -100,25 +100,25 @@ ClusterCard.propTypes = {
NAME: PropTypes.string.isRequired,
HOSTS: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.object),
PropTypes.object
PropTypes.object,
]),
VNETS: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.object),
PropTypes.object
PropTypes.object,
]),
DATASTORES: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.object),
PropTypes.object
])
PropTypes.object,
]),
}),
isSelected: PropTypes.bool,
handleClick: PropTypes.func
handleClick: PropTypes.func,
}
ClusterCard.defaultProps = {
value: {},
isSelected: false,
handleClick: undefined
handleClick: undefined,
}
ClusterCard.displayName = 'ClusterCard'

View File

@ -21,22 +21,26 @@ import makeStyles from '@mui/styles/makeStyles'
import { Folder as DatastoreIcon } from 'iconoir-react'
import SelectCard, { Action } from 'client/components/Cards/SelectCard'
import { StatusBadge, StatusChip, LinearProgressWithLabel } from 'client/components/Status'
import {
StatusBadge,
StatusChip,
LinearProgressWithLabel,
} from 'client/components/Status'
import * as DatastoreModel from 'client/models/Datastore'
const useStyles = makeStyles(({
const useStyles = makeStyles({
title: {
display: 'flex',
gap: '0.5rem'
gap: '0.5rem',
},
content: {
padding: '2em',
display: 'flex',
flexFlow: 'column',
gap: '1em'
}
}))
gap: '1em',
},
})
const DatastoreCard = memo(
({ value, isSelected, handleClick, actions }) => {
@ -47,16 +51,14 @@ const DatastoreCard = memo(
const type = DatastoreModel.getType(value)
const state = DatastoreModel.getState(value)
const {
percentOfUsed,
percentLabel
} = DatastoreModel.getCapacityInfo(value)
const { percentOfUsed, percentLabel } =
DatastoreModel.getCapacityInfo(value)
return (
<SelectCard
action={actions?.map(action =>
action={actions?.map((action) => (
<Action key={action?.cy} {...action} />
)}
))}
icon={
<StatusBadge stateColor={state.color}>
<DatastoreIcon />
@ -64,7 +66,7 @@ const DatastoreCard = memo(
}
title={
<span className={classes.title}>
<Typography title={NAME} noWrap component='span'>
<Typography title={NAME} noWrap component="span">
{NAME}
</Typography>
<StatusChip text={type.name} />
@ -80,10 +82,9 @@ const DatastoreCard = memo(
</SelectCard>
)
},
(prev, next) => (
(prev, next) =>
prev.isSelected === next.isSelected &&
prev.value?.STATE === next.value?.STATE
)
)
DatastoreCard.propTypes = {
@ -94,7 +95,7 @@ DatastoreCard.propTypes = {
STATE: PropTypes.string,
TOTAL_MB: PropTypes.string,
FREE_MB: PropTypes.string,
USED_MB: PropTypes.string
USED_MB: PropTypes.string,
}),
isSelected: PropTypes.bool,
handleClick: PropTypes.func,
@ -102,16 +103,16 @@ DatastoreCard.propTypes = {
PropTypes.shape({
handleClick: PropTypes.func.isRequired,
icon: PropTypes.node.isRequired,
cy: PropTypes.string
cy: PropTypes.string,
})
)
),
}
DatastoreCard.defaultProps = {
value: {},
isSelected: false,
handleClick: undefined,
actions: undefined
actions: undefined,
}
DatastoreCard.displayName = 'DatastoreCard'

View File

@ -21,16 +21,16 @@ import makeStyles from '@mui/styles/makeStyles'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
root: {
height: '100%'
height: '100%',
},
content: {
height: '100%',
minHeight: 140,
padding: theme.spacing(1),
textAlign: 'center'
}
textAlign: 'center',
},
}))
const EmptyCard = memo(({ title }) => {
@ -49,11 +49,11 @@ const EmptyCard = memo(({ title }) => {
})
EmptyCard.propTypes = {
title: PropTypes.oneOfType([PropTypes.string, PropTypes.array])
title: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
}
EmptyCard.defaultProps = {
title: undefined
title: undefined,
}
EmptyCard.displayName = 'EmptyCard'

View File

@ -21,21 +21,25 @@ import makeStyles from '@mui/styles/makeStyles'
import { HardDrive as HostIcon } from 'iconoir-react'
import SelectCard, { Action } from 'client/components/Cards/SelectCard'
import { StatusBadge, StatusChip, LinearProgressWithLabel } from 'client/components/Status'
import {
StatusBadge,
StatusChip,
LinearProgressWithLabel,
} from 'client/components/Status'
import * as HostModel from 'client/models/Host'
const useStyles = makeStyles({
title: {
display: 'flex',
gap: '0.5rem'
gap: '0.5rem',
},
content: {
padding: '2em',
display: 'flex',
flexFlow: 'column',
gap: '1em'
}
gap: '1em',
},
})
const HostCard = memo(
@ -44,12 +48,8 @@ const HostCard = memo(
const { ID, NAME, IM_MAD, VM_MAD } = value
const {
percentCpuUsed,
percentCpuLabel,
percentMemUsed,
percentMemLabel
} = HostModel.getAllocatedInfo(value)
const { percentCpuUsed, percentCpuLabel, percentMemUsed, percentMemLabel } =
HostModel.getAllocatedInfo(value)
const state = HostModel.getState(value)
@ -57,9 +57,9 @@ const HostCard = memo(
return (
<SelectCard
action={actions?.map(action =>
action={actions?.map((action) => (
<Action key={action?.cy} {...action} />
)}
))}
icon={
<StatusBadge title={state?.name} stateColor={state.color}>
<HostIcon />
@ -67,7 +67,7 @@ const HostCard = memo(
}
title={
<span className={classes.title}>
<Typography title={NAME} noWrap component='span'>
<Typography title={NAME} noWrap component="span">
{NAME}
</Typography>
<StatusChip text={mad} />
@ -78,16 +78,21 @@ const HostCard = memo(
handleClick={handleClick}
>
<div className={classes.content}>
<LinearProgressWithLabel value={percentCpuUsed} label={percentCpuLabel} />
<LinearProgressWithLabel value={percentMemUsed} label={percentMemLabel} />
<LinearProgressWithLabel
value={percentCpuUsed}
label={percentCpuLabel}
/>
<LinearProgressWithLabel
value={percentMemUsed}
label={percentMemLabel}
/>
</div>
</SelectCard>
)
},
(prev, next) => (
(prev, next) =>
prev.isSelected === next.isSelected &&
prev.value?.STATE === next.value?.STATE
)
)
HostCard.propTypes = {
@ -102,8 +107,8 @@ HostCard.propTypes = {
CPU_USAGE: PropTypes.string,
TOTAL_CPU: PropTypes.string,
MEM_USAGE: PropTypes.string,
TOTAL_MEM: PropTypes.string
})
TOTAL_MEM: PropTypes.string,
}),
}),
isSelected: PropTypes.bool,
handleClick: PropTypes.func,
@ -111,16 +116,16 @@ HostCard.propTypes = {
PropTypes.shape({
handleClick: PropTypes.func.isRequired,
icon: PropTypes.node.isRequired,
cy: PropTypes.string
cy: PropTypes.string,
})
)
),
}
HostCard.defaultProps = {
value: {},
isSelected: false,
handleClick: undefined,
actions: undefined
actions: undefined,
}
HostCard.displayName = 'HostCard'

View File

@ -28,14 +28,16 @@ const NetworkCard = memo(
const addresses = [AR_POOL?.AR ?? []].flat()
const totalLeases = addresses.reduce((res, ar) => +ar.SIZE + res, 0)
const percentOfUsed = +USED_LEASES * 100 / +totalLeases || 0
const percentLabel = `${USED_LEASES} / ${totalLeases} (${Math.round(percentOfUsed)}%)`
const percentOfUsed = (+USED_LEASES * 100) / +totalLeases || 0
const percentLabel = `${USED_LEASES} / ${totalLeases} (${Math.round(
percentOfUsed
)}%)`
return (
<SelectCard
action={actions?.map(action =>
action={actions?.map((action) => (
<Action key={action?.cy} {...action} />
)}
))}
icon={<NetworkIcon />}
title={NAME}
subheader={`#${ID}`}
@ -59,8 +61,8 @@ NetworkCard.propTypes = {
STATE: PropTypes.string,
USED_LEASES: PropTypes.string,
AR_POOL: PropTypes.shape({
AR: PropTypes.oneOfType([PropTypes.object, PropTypes.array])
})
AR: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
}),
}),
isSelected: PropTypes.bool,
handleClick: PropTypes.func,
@ -68,16 +70,16 @@ NetworkCard.propTypes = {
PropTypes.shape({
handleClick: PropTypes.func.isRequired,
icon: PropTypes.node.isRequired,
cy: PropTypes.string
cy: PropTypes.string,
})
)
),
}
NetworkCard.defaultProps = {
value: {},
isSelected: false,
handleClick: undefined,
actions: undefined
actions: undefined,
}
NetworkCard.displayName = 'NetworkCard'

View File

@ -28,33 +28,36 @@ const useStyles = makeStyles(() => ({
height: '100%',
minHeight: 140,
display: 'flex',
flexDirection: 'column'
flexDirection: 'column',
},
content: {
minHeight: 260
}
minHeight: 260,
},
}))
const PolicyCard = memo(
({ id, cy, fields, handleRemove, cardProps }) => {
const classes = useStyles()
const PolicyCard = memo(({ id, cy, fields, handleRemove, cardProps }) => {
const classes = useStyles()
return (
<Card variant="outlined" className={classes.root} {...cardProps}>
<CardContent className={classes.content}>
<FormWithSchema id={id} cy={cy} fields={fields} />
</CardContent>
<CardActions>
{handleRemove && (
<Button variant="contained" size="small" onClick={handleRemove} disableElevation>
{Tr(T.Remove)}
</Button>
)}
</CardActions>
</Card>
)
}
)
return (
<Card variant="outlined" className={classes.root} {...cardProps}>
<CardContent className={classes.content}>
<FormWithSchema id={id} cy={cy} fields={fields} />
</CardContent>
<CardActions>
{handleRemove && (
<Button
variant="contained"
size="small"
onClick={handleRemove}
disableElevation
>
{Tr(T.Remove)}
</Button>
)}
</CardActions>
</Card>
)
})
PolicyCard.propTypes = {
id: PropTypes.string,
@ -63,7 +66,7 @@ PolicyCard.propTypes = {
handleEdit: PropTypes.func,
handleClone: PropTypes.func,
handleRemove: PropTypes.func,
cardProps: PropTypes.object
cardProps: PropTypes.object,
}
PolicyCard.defaultProps = {
@ -73,7 +76,7 @@ PolicyCard.defaultProps = {
handleEdit: undefined,
handleClone: undefined,
handleRemove: undefined,
cardProps: undefined
cardProps: undefined,
}
PolicyCard.displayName = 'PolicyCard'

View File

@ -28,12 +28,24 @@ import {
PROVISIONS_STATES,
PROVIDER_IMAGES_URL,
PROVISION_IMAGES_URL,
DEFAULT_IMAGE
DEFAULT_IMAGE,
} from 'client/constants'
const ProvisionCard = memo(
({ value, image: propImage, isSelected, handleClick, isProvider, actions, deleteAction }) => {
const { ID, NAME, TEMPLATE: { BODY = {} } } = value
({
value,
image: propImage,
isSelected,
handleClick,
isProvider,
actions,
deleteAction,
}) => {
const {
ID,
NAME,
TEMPLATE: { BODY = {} },
} = value
const IMAGES_URL = isProvider ? PROVIDER_IMAGES_URL : PROVISION_IMAGES_URL
@ -43,22 +55,22 @@ const ProvisionCard = memo(
const isExternalImage = useMemo(() => isExternalURL(image), [image])
const imageUrl = useMemo(
() => isExternalImage ? image : `${IMAGES_URL}/${image}`,
() => (isExternalImage ? image : `${IMAGES_URL}/${image}`),
[isExternalImage]
)
return (
<SelectCard
action={(actions?.length > 0 || deleteAction) && (
<>
{actions?.map(action =>
<Action key={action?.cy} {...action} />
)}
{deleteAction && (
<ButtonToTriggerForm {...deleteAction} />
)}
</>
)}
action={
(actions?.length > 0 || deleteAction) && (
<>
{actions?.map((action) => (
<Action key={action?.cy} {...action} />
))}
{deleteAction && <ButtonToTriggerForm {...deleteAction} />}
</>
)
}
dataCy={isProvider ? 'provider' : 'provision'}
handleClick={handleClick}
icon={
@ -74,21 +86,18 @@ const ProvisionCard = memo(
mediaProps={{
component: 'div',
children: (
<Image
src={imageUrl}
withSources={image && !isExternalImage}
/>
)
<Image src={imageUrl} withSources={image && !isExternalImage} />
),
}}
subheader={`#${ID}`}
title={NAME}
disableFilterImage={isExternalImage}
/>
)
}, (prev, next) => (
},
(prev, next) =>
prev.isSelected === next.isSelected &&
prev.value?.TEMPLATE?.BODY?.state === next.value?.TEMPLATE?.BODY?.state
)
)
ProvisionCard.propTypes = {
@ -102,9 +111,9 @@ ProvisionCard.propTypes = {
PropTypes.shape({
handleClick: PropTypes.func.isRequired,
icon: PropTypes.object.isRequired,
cy: PropTypes.string
cy: PropTypes.string,
})
)
),
}
ProvisionCard.defaultProps = {
@ -114,7 +123,7 @@ ProvisionCard.defaultProps = {
isSelected: undefined,
image: undefined,
deleteAction: undefined,
value: {}
value: {},
}
ProvisionCard.displayName = 'ProvisionCard'

View File

@ -16,7 +16,10 @@
import { memo, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Db as ProviderIcon, SettingsCloud as ProvisionIcon } from 'iconoir-react'
import {
Db as ProviderIcon,
SettingsCloud as ProvisionIcon,
} from 'iconoir-react'
import { SelectCard } from 'client/components/Cards'
import Image from 'client/components/Image'
@ -32,7 +35,7 @@ const ProvisionTemplateCard = memo(
const isExternalImage = useMemo(() => isExternalURL(image), [image])
const imageUrl = useMemo(
() => isExternalImage ? image : `${IMAGES_URL}/${image}`,
() => (isExternalImage ? image : `${IMAGES_URL}/${image}`),
[isExternalImage]
)
@ -47,11 +50,8 @@ const ProvisionTemplateCard = memo(
mediaProps={{
component: 'div',
children: (
<Image
src={imageUrl}
withSources={image && !isExternalImage}
/>
)
<Image src={imageUrl} withSources={image && !isExternalImage} />
),
}}
subheader={description}
title={name}
@ -67,7 +67,7 @@ ProvisionTemplateCard.propTypes = {
isSelected: PropTypes.bool,
isValid: PropTypes.bool,
image: PropTypes.string,
value: PropTypes.object
value: PropTypes.object,
}
ProvisionTemplateCard.defaultProps = {
@ -76,7 +76,7 @@ ProvisionTemplateCard.defaultProps = {
isSelected: false,
isValid: true,
image: undefined,
value: { name: '', description: '' }
value: { name: '', description: '' },
}
ProvisionTemplateCard.displayName = 'ProvisionTemplateCard'

View File

@ -19,21 +19,16 @@ import PropTypes from 'prop-types'
import useFetch from 'client/hooks/useFetch'
import { SubmitButton } from 'client/components/FormControl'
const Action = memo(({
cy,
handleClick,
stopPropagation,
...props
}) => {
const { fetchRequest, data, loading } = useFetch(
e => Promise.resolve(handleClick?.(e))
const Action = memo(({ cy, handleClick, stopPropagation, ...props }) => {
const { fetchRequest, data, loading } = useFetch((e) =>
Promise.resolve(handleClick?.(e))
)
return (
<SubmitButton
data-cy={cy}
isSubmitting={loading}
onClick={evt => {
onClick={(evt) => {
stopPropagation && evt?.stopPropagation?.()
fetchRequest()
}}
@ -47,12 +42,12 @@ Action.propTypes = {
cy: PropTypes.string,
handleClick: PropTypes.func.isRequired,
icon: PropTypes.node,
stopPropagation: PropTypes.bool
stopPropagation: PropTypes.bool,
}
Action.defaultProps = {
icon: undefined,
cy: 'action-card'
cy: 'action-card',
}
Action.displayName = 'ActionCard'

View File

@ -23,7 +23,7 @@ import {
CardHeader,
CardActions,
CardMedia,
Skeleton
Skeleton,
} from '@mui/material'
import useNearScreen from 'client/hooks/useNearScreen'
@ -32,129 +32,139 @@ import { ConditionalWrap } from 'client/components/HOC'
import { Action } from 'client/components/Cards/SelectCard'
import selectCardStyles from 'client/components/Cards/SelectCard/styles'
const SelectCard = memo(({
action,
actions,
cardActionsProps,
cardHeaderProps,
cardProps,
cardActionAreaProps,
children,
dataCy,
disableFilterImage,
handleClick,
icon,
isSelected,
mediaProps,
observerOff,
skeletonHeight,
stylesProps,
subheader,
title
}) => {
const classes = selectCardStyles({ ...stylesProps, isSelected, disableFilterImage })
const { isNearScreen, fromRef } = useNearScreen({
distance: '100px'
})
const SelectCard = memo(
({
action,
actions,
cardActionsProps,
cardHeaderProps,
cardProps,
cardActionAreaProps,
children,
dataCy,
disableFilterImage,
handleClick,
icon,
isSelected,
mediaProps,
observerOff,
skeletonHeight,
stylesProps,
subheader,
title,
}) => {
const classes = selectCardStyles({
...stylesProps,
isSelected,
disableFilterImage,
})
const { isNearScreen, fromRef } = useNearScreen({
distance: '100px',
})
return (
<ConditionalWrap
condition={!observerOff}
wrap={children => <span ref={fromRef}>{children}</span>}>
{observerOff || isNearScreen ? (
<Card
{...cardProps}
className={clsx(classes.root, cardProps?.className, {
[classes.actionArea]: !handleClick
})}
data-cy={dataCy ? `${dataCy}-card` : undefined}
>
{/* CARD ACTION AREA */}
<ConditionalWrap
condition={handleClick && !action}
wrap={children =>
<CardActionArea
{...cardActionAreaProps}
className={clsx(classes.actionArea, cardActionAreaProps?.className)}
onClick={handleClick}
data-cy={(dataCy && isSelected) && `${dataCy}-card-selected`}
>
{children}
</CardActionArea>
}
return (
<ConditionalWrap
condition={!observerOff}
wrap={(children) => <span ref={fromRef}>{children}</span>}
>
{observerOff || isNearScreen ? (
<Card
{...cardProps}
className={clsx(classes.root, cardProps?.className, {
[classes.actionArea]: !handleClick,
})}
data-cy={dataCy ? `${dataCy}-card` : undefined}
>
{/* CARD HEADER */}
{(title || subheader || icon || action) && (
<CardHeader
{...cardHeaderProps}
action={action}
avatar={icon}
classes={{
root: classes.headerRoot,
content: classes.headerContent,
avatar: classes.headerAvatar
}}
title={title}
titleTypographyProps={{
variant: 'body1',
noWrap: true,
className: classes.header,
title: typeof title === 'string' ? title : undefined,
...(dataCy && { 'data-cy': `${dataCy}-card-title` })
}}
subheader={subheader}
subheaderTypographyProps={{
variant: 'body2',
noWrap: true,
className: classes.subheader,
title: typeof subheader === 'string' ? subheader : undefined,
...(dataCy && { 'data-cy': `${dataCy}-card-subheader` })
}}
{...cardHeaderProps}
/>
)}
{/* CARD ACTION AREA */}
<ConditionalWrap
condition={handleClick && !action}
wrap={(children) => (
<CardActionArea
{...cardActionAreaProps}
className={clsx(
classes.actionArea,
cardActionAreaProps?.className
)}
onClick={handleClick}
data-cy={dataCy && isSelected && `${dataCy}-card-selected`}
>
{children}
</CardActionArea>
)}
>
{/* CARD HEADER */}
{(title || subheader || icon || action) && (
<CardHeader
{...cardHeaderProps}
action={action}
avatar={icon}
classes={{
root: classes.headerRoot,
content: classes.headerContent,
avatar: classes.headerAvatar,
}}
title={title}
titleTypographyProps={{
variant: 'body1',
noWrap: true,
className: classes.header,
title: typeof title === 'string' ? title : undefined,
...(dataCy && { 'data-cy': `${dataCy}-card-title` }),
}}
subheader={subheader}
subheaderTypographyProps={{
variant: 'body2',
noWrap: true,
className: classes.subheader,
title:
typeof subheader === 'string' ? subheader : undefined,
...(dataCy && { 'data-cy': `${dataCy}-card-subheader` }),
}}
{...cardHeaderProps}
/>
)}
{/* CARD CONTENT */}
{children}
{/* CARD CONTENT */}
{children}
{/* CARD MEDIA */}
{mediaProps && (
<ConditionalWrap
condition={handleClick && action}
wrap={children =>
<CardActionArea
className={classes.mediaActionArea}
onClick={handleClick}
>
{children}
</CardActionArea>
}
>
<CardMedia className={classes.media} {...mediaProps} />
</ConditionalWrap>
)}
{/* CARD MEDIA */}
{mediaProps && (
<ConditionalWrap
condition={handleClick && action}
wrap={(children) => (
<CardActionArea
className={classes.mediaActionArea}
onClick={handleClick}
>
{children}
</CardActionArea>
)}
>
<CardMedia className={classes.media} {...mediaProps} />
</ConditionalWrap>
)}
{/* CARD ACTIONS */}
{actions?.length > 0 && (
<CardActions {...cardActionsProps}>
{actions?.map(action => (
<Action key={action?.cy} {...action} />
))}
</CardActions>
)}
</ConditionalWrap>
</Card>
) : (
<Skeleton
variant="rectangular"
width="100%"
height={skeletonHeight}
/>
)}
</ConditionalWrap>
)
})
{/* CARD ACTIONS */}
{actions?.length > 0 && (
<CardActions {...cardActionsProps}>
{actions?.map((action) => (
<Action key={action?.cy} {...action} />
))}
</CardActions>
)}
</ConditionalWrap>
</Card>
) : (
<Skeleton
variant="rectangular"
width="100%"
height={skeletonHeight}
/>
)}
</ConditionalWrap>
)
}
)
export const SelectCardProps = {
stylesProps: PropTypes.object,
@ -163,25 +173,16 @@ export const SelectCardProps = {
PropTypes.shape({
handleClick: PropTypes.func.isRequired,
icon: PropTypes.node.isRequired,
cy: PropTypes.string
cy: PropTypes.string,
})
),
cardActionsProps: PropTypes.shape({
className: PropTypes.string,
style: PropTypes.object
style: PropTypes.object,
}),
icon: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object
]),
title: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object
]),
subheader: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object
]),
icon: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
title: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
subheader: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
cardHeaderProps: PropTypes.object,
mediaProps: PropTypes.shape({
classes: PropTypes.object,
@ -189,7 +190,7 @@ export const SelectCardProps = {
component: PropTypes.elementType,
image: PropTypes.string,
src: PropTypes.string,
style: PropTypes.object
style: PropTypes.object,
}),
isSelected: PropTypes.bool,
handleClick: PropTypes.func,
@ -199,11 +200,11 @@ export const SelectCardProps = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.string
PropTypes.string,
]),
dataCy: PropTypes.string,
disableFilterImage: PropTypes.bool,
skeletonHeight: PropTypes.number
skeletonHeight: PropTypes.number,
}
SelectCard.defaultProps = {
@ -224,7 +225,7 @@ SelectCard.defaultProps = {
stylesProps: undefined,
subheader: undefined,
title: undefined,
skeletonHeight: 140
skeletonHeight: 140,
}
SelectCard.propTypes = SelectCardProps

View File

@ -13,7 +13,9 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import SelectCard, { SelectCardProps } from 'client/components/Cards/SelectCard/SelectCard'
import SelectCard, {
SelectCardProps,
} from 'client/components/Cards/SelectCard/SelectCard'
import Action from 'client/components/Cards/SelectCard/Action'
export { Action, SelectCardProps }

View File

@ -17,34 +17,34 @@ import makeStyles from '@mui/styles/makeStyles'
import { SCHEMES } from 'client/constants'
const styles = makeStyles(theme => ({
const styles = makeStyles((theme) => ({
root: ({ isSelected }) => ({
height: '100%',
transition: theme.transitions.create(
['background-color', 'box-shadow'], { duration: '0.2s' }
),
transition: theme.transitions.create(['background-color', 'box-shadow'], {
duration: '0.2s',
}),
'&:hover': {
boxShadow: theme.shadows['5']
boxShadow: theme.shadows['5'],
},
...(isSelected && {
color: theme.palette.secondary.contrastText,
backgroundColor: theme.palette.secondary.main,
'& .badge': {
color: theme.palette.secondary.main,
backgroundColor: theme.palette.secondary.contrastText
}
})
backgroundColor: theme.palette.secondary.contrastText,
},
}),
}),
actionArea: {
'&:disabled': {
filter: 'brightness(0.5)'
}
filter: 'brightness(0.5)',
},
},
mediaActionArea: {
'&:hover': {
backgroundColor: theme.palette.secondary.contrastText,
'& $media': { filter: 'none' }
}
'& $media': { filter: 'none' },
},
},
media: {
width: '100%',
@ -58,26 +58,27 @@ const styles = makeStyles(theme => ({
height: '100%',
objectFit: 'cover',
position: 'absolute',
userSelect: 'none'
userSelect: 'none',
},
transition: theme.transitions.create('filter', { duration: '0.2s' }),
filter: ({ isSelected, disableFilterImage }) =>
disableFilterImage
? 'none'
: (theme.palette.mode === SCHEMES.DARK || isSelected)
? 'contrast(0) brightness(2)'
: 'contrast(0) brightness(0.8)'
: theme.palette.mode === SCHEMES.DARK || isSelected
? 'contrast(0) brightness(2)'
: 'contrast(0) brightness(0.8)',
},
headerRoot: {
// align header icon to top
alignItems: 'start'
alignItems: 'start',
},
headerContent: { overflow: 'auto' },
headerAvatar: {
display: 'flex',
color: ({ isSelected }) => isSelected
? theme.palette.secondary.contrastText
: theme.palette.text.primary
color: ({ isSelected }) =>
isSelected
? theme.palette.secondary.contrastText
: theme.palette.text.primary,
},
subheader: {
color: ({ isSelected }) =>
@ -87,8 +88,8 @@ const styles = makeStyles(theme => ({
whiteSpace: 'initial',
display: '-webkit-box',
lineClamp: 2,
boxOrient: 'vertical'
}
boxOrient: 'vertical',
},
}))
export default styles

View File

@ -23,63 +23,63 @@ import SelectCard from 'client/components/Cards/SelectCard'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
const TierCard = memo(
({ value, handleEdit, handleRemove, cardProps }) => {
const { name, cardinality } = value
const TierCard = memo(({ value, handleEdit, handleRemove, cardProps }) => {
const { name, cardinality } = value
return (
<SelectCard
observerOff
icon={
<Badge
badgeContent={cardinality}
color="primary"
anchorOrigin={{
vertical: 'top',
horizontal: 'left'
}}
return (
<SelectCard
observerOff
icon={
<Badge
badgeContent={cardinality}
color="primary"
anchorOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<TierIcon />
</Badge>
}
title={name}
cardProps={cardProps}
>
<CardActions>
{handleEdit && (
<Button
variant="contained"
size="small"
onClick={handleEdit}
disableElevation
>
<TierIcon />
</Badge>
}
title={name}
cardProps={cardProps}
>
<CardActions>
{handleEdit && (
<Button variant="contained" size="small" onClick={handleEdit} disableElevation>
{Tr(T.Edit)}
</Button>
)}
{handleRemove && (
<Button size="small" onClick={handleRemove} disableElevation>
{Tr(T.Remove)}
</Button>
)}
</CardActions>
</SelectCard>
)
}
)
{Tr(T.Edit)}
</Button>
)}
{handleRemove && (
<Button size="small" onClick={handleRemove} disableElevation>
{Tr(T.Remove)}
</Button>
)}
</CardActions>
</SelectCard>
)
})
TierCard.propTypes = {
value: PropTypes.shape({
name: PropTypes.string,
cardinality: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
])
cardinality: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
}),
handleEdit: PropTypes.func,
handleRemove: PropTypes.func,
cardProps: PropTypes.object
cardProps: PropTypes.object,
}
TierCard.defaultProps = {
value: {},
handleEdit: undefined,
handleRemove: undefined,
cardProps: undefined
cardProps: undefined,
}
TierCard.displayName = 'TierCard'

View File

@ -28,17 +28,17 @@ const VirtualMachineCard = memo(
return (
<SelectCard
action={actions?.map(action =>
action={actions?.map((action) => (
<Action key={action?.cy} {...action} />
)}
))}
skeletonHeight={75}
dataCy={`vm-${ID}`}
handleClick={handleClick}
icon={(
icon={
<StatusBadge title={name} stateColor={color}>
<VmIcon />
</StatusBadge>
)}
}
isSelected={isSelected}
subheader={`#${ID}`}
title={NAME}
@ -59,16 +59,16 @@ VirtualMachineCard.propTypes = {
PropTypes.shape({
handleClick: PropTypes.func.isRequired,
icon: PropTypes.object.isRequired,
cy: PropTypes.string
cy: PropTypes.string,
})
)
),
}
VirtualMachineCard.defaultProps = {
handleClick: undefined,
isSelected: false,
value: {},
actions: undefined
actions: undefined,
}
VirtualMachineCard.displayName = 'VirtualMachineCard'

View File

@ -23,9 +23,11 @@ import makeStyles from '@mui/styles/makeStyles'
import { addOpacityToColor } from 'client/utils'
import { SCHEMES } from 'client/constants'
const useStyles = makeStyles(theme => {
const getBackgroundColor = theme.palette.mode === SCHEMES.DARK ? darken : lighten
const getContrastBackgroundColor = theme.palette.mode === SCHEMES.LIGHT ? darken : lighten
const useStyles = makeStyles((theme) => {
const getBackgroundColor =
theme.palette.mode === SCHEMES.DARK ? darken : lighten
const getContrastBackgroundColor =
theme.palette.mode === SCHEMES.LIGHT ? darken : lighten
return {
root: {
@ -36,8 +38,8 @@ const useStyles = makeStyles(theme => {
[theme.breakpoints.only('xs')]: {
display: 'flex',
alignItems: 'baseline',
gap: '1em'
}
gap: '1em',
},
},
icon: {
position: 'absolute',
@ -49,8 +51,8 @@ const useStyles = makeStyles(theme => {
'& > svg': {
color: addOpacityToColor(theme.palette.common.white, 0.2),
height: '100%',
width: '30%'
}
width: '30%',
},
},
wave: {
display: 'block',
@ -60,57 +62,66 @@ const useStyles = makeStyles(theme => {
left: '50%',
width: 220,
height: 220,
borderRadius: '43%'
borderRadius: '43%',
},
wave1: {
backgroundColor: ({ bgColor }) => getContrastBackgroundColor(bgColor, 0.3),
animation: '$drift 7s infinite linear'
backgroundColor: ({ bgColor }) =>
getContrastBackgroundColor(bgColor, 0.3),
animation: '$drift 7s infinite linear',
},
wave2: {
backgroundColor: ({ bgColor }) => getContrastBackgroundColor(bgColor, 0.5),
animation: '$drift 5s infinite linear'
backgroundColor: ({ bgColor }) =>
getContrastBackgroundColor(bgColor, 0.5),
animation: '$drift 5s infinite linear',
},
'@keyframes drift': {
from: { transform: 'rotate(0deg)' },
to: { transform: 'rotate(360deg)' }
}
to: { transform: 'rotate(360deg)' },
},
}
})
const WavesCard = memo(({ text, value, bgColor, icon: Icon }) => {
const classes = useStyles({ bgColor })
const WavesCard = memo(
({ text, value, bgColor, icon: Icon }) => {
const classes = useStyles({ bgColor })
return (
<Paper className={classes.root}>
<Typography variant='h6' zIndex={2}>{text}</Typography>
<Typography variant='h4' zIndex={2}>{value}</Typography>
<span className={clsx(classes.wave, classes.wave1)} />
<span className={clsx(classes.wave, classes.wave2)} />
{Icon && (
<span className={classes.icon}>
<Icon />
</span>
)}
</Paper>
)
}, (prev, next) => prev.value === next.value)
return (
<Paper className={classes.root}>
<Typography variant="h6" zIndex={2}>
{text}
</Typography>
<Typography variant="h4" zIndex={2}>
{value}
</Typography>
<span className={clsx(classes.wave, classes.wave1)} />
<span className={clsx(classes.wave, classes.wave2)} />
{Icon && (
<span className={classes.icon}>
<Icon />
</span>
)}
</Paper>
)
},
(prev, next) => prev.value === next.value
)
WavesCard.propTypes = {
text: PropTypes.string,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.element
PropTypes.element,
]),
bgColor: PropTypes.string,
icon: PropTypes.any
icon: PropTypes.any,
}
WavesCard.defaultProps = {
text: undefined,
value: undefined,
bgColor: '#ffffff00',
icon: undefined
icon: undefined,
}
WavesCard.displayName = 'WavesCard'

View File

@ -44,5 +44,5 @@ export {
SelectCard,
TierCard,
VirtualMachineCard,
WavesCard
WavesCard,
}

View File

@ -26,31 +26,35 @@ import NumberEasing from 'client/components/NumberEasing'
* @param {string} props.color - Color of component: primary, secondary or inherit
* @returns {JSXElementConstructor} Circular progress bar component
*/
const Circle = memo(({ color = 'secondary' }) => {
const [progress, setProgress] = useState(0)
const Circle = memo(
({ color = 'secondary' }) => {
const [progress, setProgress] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setProgress(prevProgress => {
const nextProgress = prevProgress + 2
if (nextProgress === 100) clearInterval(timer)
return nextProgress
})
}, 50)
useEffect(() => {
const timer = setInterval(() => {
setProgress((prevProgress) => {
const nextProgress = prevProgress + 2
if (nextProgress === 100) clearInterval(timer)
return () => clearInterval(timer)
}, [])
return nextProgress
})
}, 50)
return (
<CircularProgress
color={color}
size={150}
thickness={5}
value={progress}
variant='determinate'
/>
)
}, (prev, next) => prev.color === next.color)
return () => clearInterval(timer)
}, [])
return (
<CircularProgress
color={color}
size={150}
thickness={5}
value={progress}
variant="determinate"
/>
)
},
(prev, next) => prev.color === next.color
)
Circle.propTypes = { color: PropTypes.string }
Circle.displayName = 'Circle'
@ -67,32 +71,39 @@ Circle.displayName = 'Circle'
* @param {object} props.labelProps - Props of text
* @returns {JSXElementConstructor} Circular chart component
*/
const CircleChart = memo(({ label, labelProps }) => (
<Box position='relative' display='inline-flex' width={1}>
<Box display='flex' flexDirection='column' alignItems='center' width={1}>
<Circle />
</Box>
<Box top={0} left={0} bottom={0} right={0}
position='absolute'
display='flex'
alignItems='center'
justifyContent='center'
>
<Typography
variant='h4'
component='div'
style={{ cursor: 'pointer' }}
{...labelProps}
const CircleChart = memo(
({ label, labelProps }) => (
<Box position="relative" display="inline-flex" width={1}>
<Box display="flex" flexDirection="column" alignItems="center" width={1}>
<Circle />
</Box>
<Box
top={0}
left={0}
bottom={0}
right={0}
position="absolute"
display="flex"
alignItems="center"
justifyContent="center"
>
<NumberEasing value={label} />
</Typography>
<Typography
variant="h4"
component="div"
style={{ cursor: 'pointer' }}
{...labelProps}
>
<NumberEasing value={label} />
</Typography>
</Box>
</Box>
</Box>
), (prev, next) => prev.label === next.label)
),
(prev, next) => prev.label === next.label
)
CircleChart.propTypes = {
label: PropTypes.string,
labelProps: PropTypes.object
labelProps: PropTypes.object,
}
CircleChart.displayName = 'CircleChart'

View File

@ -22,11 +22,11 @@ import makeStyles from '@mui/styles/makeStyles'
import { TypographyWithPoint } from 'client/components/Typography'
import { addOpacityToColor } from 'client/utils'
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
legend: {
display: 'grid',
gridGap: '1rem',
gridTemplateColumns: 'repeat(auto-fit, minmax(125px, 1fr))'
gridTemplateColumns: 'repeat(auto-fit, minmax(125px, 1fr))',
},
bar: {
marginTop: '1rem',
@ -36,11 +36,11 @@ const useStyles = makeStyles(theme => ({
backgroundColor: '#616161e0',
transition: '1s',
gridTemplateColumns: ({ fragments }) =>
fragments?.map(fragment => `${fragment}fr`)?.join(' '),
fragments?.map((fragment) => `${fragment}fr`)?.join(' '),
[theme.breakpoints.only('xs')]: {
display: 'none'
}
}
display: 'none',
},
},
}))
/**
@ -53,7 +53,7 @@ const useStyles = makeStyles(theme => ({
* @returns {JSXElementConstructor} Chart bar component
*/
const SingleBar = ({ legend, data, total = 0 }) => {
const fragments = data.map(data => Math.floor(data * 10 / (total || 1)))
const fragments = data.map((data) => Math.floor((data * 10) / (total || 1)))
const classes = useStyles({ fragments })
@ -62,7 +62,11 @@ const SingleBar = ({ legend, data, total = 0 }) => {
{/* LEGEND */}
<div className={classes.legend}>
{legend?.map(({ name, color }, idx) => (
<TypographyWithPoint key={name} pointColor={color} data-attr={data[idx]}>
<TypographyWithPoint
key={name}
pointColor={color}
data-attr={data[idx]}
>
{name}
</TypographyWithPoint>
))}
@ -75,11 +79,16 @@ const SingleBar = ({ legend, data, total = 0 }) => {
const color = legend[idx]?.color
const style = {
backgroundColor: color,
'&:hover': { backgroundColor: addOpacityToColor(color, 0.6) }
'&:hover': { backgroundColor: addOpacityToColor(color, 0.6) },
}
return (
<Tooltip arrow key={label} placement='top' title={`${label}: ${value}`}>
<Tooltip
arrow
key={label}
placement="top"
title={`${label}: ${value}`}
>
<div style={style}></div>
</Tooltip>
)
@ -90,17 +99,16 @@ const SingleBar = ({ legend, data, total = 0 }) => {
}
SingleBar.propTypes = {
legend: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
color: PropTypes.string
})),
data: PropTypes.arrayOf(
PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
])
legend: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string,
color: PropTypes.string,
})
),
total: PropTypes.number
data: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.string, PropTypes.number])
),
total: PropTypes.number,
}
SingleBar.displayName = 'SingleBar'

View File

@ -16,7 +16,4 @@
import CircleChart from 'client/components/Charts/CircleChart'
import SingleBar from 'client/components/Charts/SingleBar'
export {
CircleChart,
SingleBar
}
export { CircleChart, SingleBar }

View File

@ -15,8 +15,9 @@
* ------------------------------------------------------------------------- */
// Reference to https://github.com/sindresorhus/ansi-regex
// eslint-disable-next-line no-control-regex
export const _regANSI = /(?:(?:\u001b\[)|\u009b)(?:(?:[0-9]{1,3})?(?:(?:;[0-9]{0,3})*)?[A-M|f-m])|\u001b[A-M]/
export const _regANSI =
// eslint-disable-next-line no-control-regex
/(?:(?:\u001b\[)|\u009b)(?:(?:[0-9]{1,3})?(?:(?:;[0-9]{0,3})*)?[A-M|f-m])|\u001b[A-M]/
const _defColors = {
reset: ['fff', '000'], // [FOREGROUND_COLOR, BACKGROUND_COLOR]
@ -28,7 +29,7 @@ const _defColors = {
magenta: 'ff00ff',
cyan: '00ffee',
lightgrey: 'f0f0f0',
darkgrey: '888'
darkgrey: '888',
}
const _styles = {
30: 'black',
@ -38,7 +39,7 @@ const _styles = {
34: 'blue',
35: 'magenta',
36: 'cyan',
37: 'lightgrey'
37: 'lightgrey',
}
const _openTags = {
1: 'font-weight:bold', // bold
@ -46,12 +47,12 @@ const _openTags = {
3: '<i>', // italic
4: '<u>', // underscore
8: 'display:none', // hidden
9: '<del>' // delete
9: '<del>', // delete
}
const _closeTags = {
23: '</i>', // reset italic
24: '</u>', // reset underscore
29: '</del>' // reset delete
29: '</del>', // reset delete
}
;[0, 21, 22, 27, 28, 39, 49].forEach(function (n) {
@ -64,40 +65,46 @@ const _closeTags = {
* @param {string} text - Text
* @returns {string} HTML as string
*/
export default function ansiHTML (text) {
export default function ansiHTML(text) {
// Returns the text if the string has no ANSI escape code.
if (!_regANSI.test(text)) {
return text
}
// Cache opened sequence.
var ansiCodes = []
const ansiCodes = []
// Replace with markup.
var ret = text.replace(/\033\[(\d+)*m/g, function (match, seq) {
let ret = text.replace(/\033\[(\d+)*m/g, function (match, seq) {
const ot = _openTags[seq]
if (ot) {
// If current sequence has been opened, close it.
if (!!~ansiCodes.indexOf(seq)) { // eslint-disable-line no-extra-boolean-cast
if (~ansiCodes.indexOf(seq)) {
// eslint-disable-line no-extra-boolean-cast
ansiCodes.pop()
return '</span>'
}
// Open tag.
ansiCodes.push(seq)
return ot[0] === '<' ? ot : '<span style="' + ot + ';">'
return ot[0] === '<' ? ot : `<span style="${ot};">`
}
const ct = _closeTags[seq]
if (ct) {
// Pop sequence
ansiCodes.pop()
return ct
}
return ''
})
// Make sure tags are closed.
var l = ansiCodes.length
;(l > 0) && (ret += Array(l + 1).join('</span>'))
const l = ansiCodes.length
l > 0 && (ret += Array(l + 1).join('</span>'))
return ret
}
@ -109,29 +116,43 @@ export default function ansiHTML (text) {
*/
ansiHTML.setColors = function (colors) {
if (typeof colors !== 'object') {
throw new Error('`colors` parameter must be an Object.')
throw new Error("'colors' parameter must be an Object.")
}
var _finalColors = {}
const _finalColors = {}
for (const key in _defColors) {
var hex = Object.prototype.hasOwnProperty.call(colors, key) ? colors[key] : null
let hex = Object.prototype.hasOwnProperty.call(colors, key)
? colors[key]
: null
if (!hex) {
_finalColors[key] = _defColors[key]
continue
}
if (key === 'reset') {
if (typeof hex === 'string') {
hex = [hex]
}
if (!Array.isArray(hex) || hex.length === 0 || hex.some(function (h) {
return typeof h !== 'string'
})) {
throw new Error('The value of `' + key + '` property must be an Array and each item could only be a hex string, e.g.: FF0000')
if (
!Array.isArray(hex) ||
hex.length === 0 ||
hex.some(function (h) {
return typeof h !== 'string'
})
) {
throw new Error(
`The value of '${key}' property must be an Array and each item could only be a hex string, e.g.: FF0000`
)
}
var defHexColor = _defColors[key]
const defHexColor = _defColors[key]
if (!hex[0]) {
hex[0] = defHexColor[0]
}
if (hex.length === 1 || !hex[1]) {
hex = [hex[0]]
hex.push(defHexColor[1])
@ -139,8 +160,11 @@ ansiHTML.setColors = function (colors) {
hex = hex.slice(0, 2)
} else if (typeof hex !== 'string') {
throw new Error('The value of `' + key + '` property must be a hex string, e.g.: FF0000')
throw new Error(
`The value of '${key}' property must be a hex string, e.g.: FF0000`
)
}
_finalColors[key] = hex
}
_setTags(_finalColors)
@ -162,10 +186,14 @@ ansiHTML.tags = {}
if (Object.defineProperty) {
Object.defineProperty(ansiHTML.tags, 'open', {
get: function () { return _openTags }
get: function () {
return _openTags
},
})
Object.defineProperty(ansiHTML.tags, 'close', {
get: function () { return _closeTags }
get: function () {
return _closeTags
},
})
} else {
ansiHTML.tags.open = _openTags
@ -174,17 +202,17 @@ if (Object.defineProperty) {
const _setTags = (colors) => {
// reset all
_openTags['0'] = 'font-weight:normal;opacity:1;color:#' + colors.reset[0] + ';background:#' + colors.reset[1]
_openTags[0] = `font-weight:normal;opacity:1;color:#${colors.reset[0]};background:#${colors.reset[1]}`
// inverse
_openTags['7'] = 'color:#' + colors.reset[1] + ';background:#' + colors.reset[0]
_openTags[7] = `color:#${colors.reset[1]};background:#${colors.reset[0]}`
// dark grey
_openTags['90'] = 'color:#' + colors.darkgrey
_openTags[90] = `color:#${colors.darkgrey}`
for (const code in _styles) {
const color = _styles[code]
const oriColor = colors[color] || '000'
_openTags[code] = 'color:#' + oriColor
_openTags[(parseInt(code) + 10).toString()] = 'background:#' + oriColor
_openTags[code] = `color:#${oriColor}`
_openTags[(parseInt(code) + 10).toString()] = `background:#${oriColor}`
}
}

View File

@ -21,101 +21,103 @@ import makeStyles from '@mui/styles/makeStyles'
import { DEBUG_LEVEL } from 'client/constants'
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
border: `1px solid ${theme.palette.divider}`,
flexWrap: 'wrap',
marginBottom: '0.8em'
marginBottom: '0.8em',
},
grouped: {
margin: theme.spacing(0.5),
border: 'none',
'&:not(:first-child)': {
borderRadius: theme.shape.borderRadius
borderRadius: theme.shape.borderRadius,
},
'&:first-child': {
borderRadius: theme.shape.borderRadius
}
}
borderRadius: theme.shape.borderRadius,
},
},
}))
const Filters = memo(({ log, filters, setFilters }) => {
const classes = useStyles()
const Filters = memo(
({ log, filters, setFilters }) => {
const classes = useStyles()
const commands = Object.keys(log)
const commands = Object.keys(log)
const handleFilterCommands = (_, filterCommand) => {
setFilters(prev => ({ ...prev, command: filterCommand }))
}
const handleFilterCommands = (_, filterCommand) => {
setFilters((prev) => ({ ...prev, command: filterCommand }))
}
const handleFilterSeverity = (_, filterCommand) => {
setFilters(prev => ({ ...prev, severity: filterCommand }))
}
const handleFilterSeverity = (_, filterCommand) => {
setFilters((prev) => ({ ...prev, severity: filterCommand }))
}
return (
<Paper elevation={0} className={classes.root}>
{/* SEVERITY FILTER */}
<ToggleButtonGroup
classes={{
grouped: classes.grouped
}}
value={filters.severity}
exclusive
size='small'
onChange={handleFilterSeverity}
>
{Object.values(DEBUG_LEVEL).map(severity => (
<ToggleButton key={severity} value={severity}>
{severity}
</ToggleButton>
))}
</ToggleButtonGroup>
<Divider flexItem orientation="vertical" className={classes.divider} />
{/* COMMANDS FILTER */}
{commands.length > 1 && (
return (
<Paper elevation={0} className={classes.root}>
{/* SEVERITY FILTER */}
<ToggleButtonGroup
classes={{
grouped: classes.grouped
grouped: classes.grouped,
}}
value={filters.command}
value={filters.severity}
exclusive
size='small'
onChange={handleFilterCommands}
size="small"
onChange={handleFilterSeverity}
>
{commands?.map(command => (
<ToggleButton key={command} value={command}>
{command}
{Object.values(DEBUG_LEVEL).map((severity) => (
<ToggleButton key={severity} value={severity}>
{severity}
</ToggleButton>
))}
</ToggleButtonGroup>
)}
</Paper>
)
}, (prev, next) =>
Object.keys(prev.log).length === Object.keys(next.log).length &&
prev.filters.command === next.filters.command &&
prev.filters.severity === next.filters.severity
<Divider flexItem orientation="vertical" className={classes.divider} />
{/* COMMANDS FILTER */}
{commands.length > 1 && (
<ToggleButtonGroup
classes={{
grouped: classes.grouped,
}}
value={filters.command}
exclusive
size="small"
onChange={handleFilterCommands}
>
{commands?.map((command) => (
<ToggleButton key={command} value={command}>
{command}
</ToggleButton>
))}
</ToggleButtonGroup>
)}
</Paper>
)
},
(prev, next) =>
Object.keys(prev.log).length === Object.keys(next.log).length &&
prev.filters.command === next.filters.command &&
prev.filters.severity === next.filters.severity
)
Filters.propTypes = {
filters: PropTypes.shape({
command: PropTypes.string,
severity: PropTypes.string
severity: PropTypes.string,
}),
log: PropTypes.object.isRequired,
setFilters: PropTypes.func
setFilters: PropTypes.func,
}
Filters.defaultProps = {
filters: {
command: undefined,
severity: undefined
severity: undefined,
},
log: {},
setFilters: () => undefined
setFilters: () => undefined,
}
Filters.displayName = 'Filters'

View File

@ -23,11 +23,11 @@ import MessageList from 'client/components/DebugLog/messagelist'
import Filters from 'client/components/DebugLog/filters'
import * as LogUtils from 'client/components/DebugLog/utils'
const debugLogStyles = makeStyles(theme => ({
const debugLogStyles = makeStyles((theme) => ({
root: {
display: 'flex',
flexFlow: 'column',
height: '100%'
height: '100%',
},
containerScroll: {
width: '100%',
@ -37,71 +37,74 @@ const debugLogStyles = makeStyles(theme => ({
backgroundColor: '#1d1f21',
wordBreak: 'break-word',
'&::-webkit-scrollbar': {
width: 14
width: 14,
},
'&::-webkit-scrollbar-thumb': {
backgroundClip: 'content-box',
border: '4px solid transparent',
borderRadius: 7,
boxShadow: 'inset 0 0 0 10px',
color: theme.palette.secondary.light
}
}
color: theme.palette.secondary.light,
},
},
}))
const DebugLog = memo(({ uuid, socket, logDefault, title }) => {
const classes = debugLogStyles()
const DebugLog = memo(
({ uuid, socket, logDefault, title }) => {
const classes = debugLogStyles()
const [log, setLog] = useState(logDefault)
const [log, setLog] = useState(logDefault)
const [filters, setFilters] = useState(() => ({
command: undefined,
severity: undefined
}))
const [filters, setFilters] = useState(() => ({
command: undefined,
severity: undefined,
}))
useEffect(() => {
const { on, off } = socket((socketData = {}) => {
socketData.id === uuid &&
setLog(prevLog => LogUtils.concatNewMessageToLog(prevLog, socketData))
})
useEffect(() => {
const { on, off } = socket((socketData = {}) => {
socketData.id === uuid &&
setLog((prevLog) =>
LogUtils.concatNewMessageToLog(prevLog, socketData)
)
})
uuid && on()
return off
}, [])
uuid && on()
return (
<div className={classes.root}>
{title}
return off
}, [])
<Filters log={log} filters={filters} setFilters={setFilters} />
return (
<div className={classes.root}>
{title}
<div className={classes.containerScroll}>
<AutoScrollBox scrollBehavior='auto'>
<MessageList log={log} filters={filters} />
</AutoScrollBox>
<Filters log={log} filters={filters} setFilters={setFilters} />
<div className={classes.containerScroll}>
<AutoScrollBox scrollBehavior="auto">
<MessageList log={log} filters={filters} />
</AutoScrollBox>
</div>
</div>
</div>
)
}, (prev, next) => prev.uuid === next.uuid)
)
},
(prev, next) => prev.uuid === next.uuid
)
DebugLog.propTypes = {
uuid: PropTypes.string,
socket: PropTypes.func.isRequired,
logDefault: PropTypes.object,
title: PropTypes.oneOfType([
PropTypes.element,
PropTypes.string
])
title: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
}
DebugLog.defaultProps = {
uuid: undefined,
socket: {
on: () => undefined,
off: () => undefined
off: () => undefined,
},
logDefault: {},
title: null
title: null,
}
DebugLog.displayName = 'DebugLog'

View File

@ -18,14 +18,17 @@ import PropTypes from 'prop-types'
import clsx from 'clsx'
import makeStyles from '@mui/styles/makeStyles'
import { NavArrowRight as CollapseIcon, NavArrowDown as ExpandMoreIcon } from 'iconoir-react'
import {
NavArrowRight as CollapseIcon,
NavArrowDown as ExpandMoreIcon,
} from 'iconoir-react'
import { DEBUG_LEVEL } from 'client/constants'
import AnsiHtml from 'client/components/DebugLog/ansiHtml'
const MAX_CHARS = 80
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
root: {
marginBottom: '0.3em',
padding: '0.5em 0',
@ -33,23 +36,29 @@ const useStyles = makeStyles(theme => ({
fontFamily: 'monospace',
color: '#fafafa',
'&:hover': {
background: '#333537'
background: '#333537',
},
display: 'grid',
gridTemplateColumns: '32px 220px 1fr',
gap: '1em',
alignItems: 'center',
cursor: ({ isMoreThanMaxChars }) =>
isMoreThanMaxChars ? 'pointer' : 'default'
isMoreThanMaxChars ? 'pointer' : 'default',
},
message: {
transition: 'all 0.3s ease-out',
whiteSpace: 'pre-line'
whiteSpace: 'pre-line',
},
[DEBUG_LEVEL.ERROR]: {
borderLeft: `0.3em solid ${theme.palette.error.light}`,
},
[DEBUG_LEVEL.WARN]: {
borderLeft: `0.3em solid ${theme.palette.warning.light}`,
},
[DEBUG_LEVEL.ERROR]: { borderLeft: `0.3em solid ${theme.palette.error.light}` },
[DEBUG_LEVEL.WARN]: { borderLeft: `0.3em solid ${theme.palette.warning.light}` },
[DEBUG_LEVEL.INFO]: { borderLeft: `0.3em solid ${theme.palette.info.light}` },
[DEBUG_LEVEL.DEBUG]: { borderLeft: `0.3em solid ${theme.palette.debug.main}` }
[DEBUG_LEVEL.DEBUG]: {
borderLeft: `0.3em solid ${theme.palette.debug.main}`,
},
}))
const Message = memo(({ timestamp, severity, message }) => {
@ -57,28 +66,28 @@ const Message = memo(({ timestamp, severity, message }) => {
const [isCollapsed, setCollapse] = useState(() => isMoreThanMaxChars)
const classes = useStyles({ isMoreThanMaxChars })
const textToShow = (isCollapsed && isMoreThanMaxChars)
? `${message?.slice(0, MAX_CHARS)}`
: message
const textToShow =
isCollapsed && isMoreThanMaxChars
? `${message?.slice(0, MAX_CHARS)}`
: message
const html = AnsiHtml(textToShow)
return (
<div
className={clsx(classes.root, classes[severity])}
onClick={() => setCollapse(prev => !prev)}
data-cy='message'
onClick={() => setCollapse((prev) => !prev)}
data-cy="message"
>
<span>
{isMoreThanMaxChars && (isCollapsed ? (
<CollapseIcon />
) : (
<ExpandMoreIcon />
))}
{isMoreThanMaxChars &&
(isCollapsed ? <CollapseIcon /> : <ExpandMoreIcon />)}
</span>
<div>{timestamp}</div>
<div className={classes.message}
dangerouslySetInnerHTML={{ __html: html }} />
<div
className={classes.message}
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
)
})
@ -86,13 +95,13 @@ const Message = memo(({ timestamp, severity, message }) => {
Message.propTypes = {
timestamp: PropTypes.string,
severity: PropTypes.oneOf(Object.keys(DEBUG_LEVEL)),
message: PropTypes.string
message: PropTypes.string,
}
Message.defaultProps = {
timestamp: '',
severity: DEBUG_LEVEL.DEBUG,
message: ''
message: '',
}
Message.displayName = 'Message'

View File

@ -20,40 +20,40 @@ import Message from 'client/components/DebugLog/message'
import { getMessageInfo } from 'client/components/DebugLog/utils'
const MessageList = ({ log = {}, filters = {} }) =>
Object.entries(log)?.map(([command, entries]) => (
// filter by command
(!filters.command || filters.command.includes(command)) && (
Object.entries(entries)?.map(([commandId, messages]) =>
Array.isArray(messages) && messages?.map((data, index) => {
const { severity, ...messageInfo } = getMessageInfo(data)
Object.entries(log)?.map(
([command, entries]) =>
// filter by command
(!filters.command || filters.command.includes(command)) &&
Object.entries(entries)?.map(
([commandId, messages]) =>
Array.isArray(messages) &&
messages?.map((data, index) => {
const { severity, ...messageInfo } = getMessageInfo(data)
// filter by severity
if (filters.severity && filters.severity !== severity) return null
// filter by severity
if (filters.severity && filters.severity !== severity) return null
const key = `${index}-${command}-${commandId}`
const key = `${index}-${command}-${commandId}`
return (
<Message key={key} severity={severity} {...messageInfo} />
)
})
return <Message key={key} severity={severity} {...messageInfo} />
})
)
)
))
)
MessageList.propTypes = {
filters: PropTypes.shape({
command: PropTypes.string,
severity: PropTypes.string
severity: PropTypes.string,
}).isRequired,
log: PropTypes.object
log: PropTypes.object,
}
MessageList.defaultProps = {
filters: {
command: undefined,
severity: undefined
severity: undefined,
},
log: undefined
log: undefined,
}
MessageList.displayName = 'MessageList'

View File

@ -21,14 +21,14 @@ import { DEBUG_LEVEL } from 'client/constants'
* @param {string} data - Message text
* @returns {string} Severity type (debug level)
*/
export const getSeverityFromData = data =>
export const getSeverityFromData = (data) =>
data.includes(DEBUG_LEVEL.ERROR)
? DEBUG_LEVEL.ERROR
: data.includes(DEBUG_LEVEL.INFO)
? DEBUG_LEVEL.INFO
: data.includes(DEBUG_LEVEL.WARN)
? DEBUG_LEVEL.WARN
: DEBUG_LEVEL.DEBUG
? DEBUG_LEVEL.INFO
: data.includes(DEBUG_LEVEL.WARN)
? DEBUG_LEVEL.WARN
: DEBUG_LEVEL.DEBUG
/**
* Returns the message information as json.
@ -67,7 +67,7 @@ export const concatNewMessageToLog = (log, message = {}) => {
return {
...log,
[command]: {
[commandId]: [...(log?.[command]?.[commandId] ?? []), data]
}
[commandId]: [...(log?.[command]?.[commandId] ?? []), data],
},
}
}

View File

@ -23,7 +23,7 @@ import {
DialogContent,
DialogActions,
Typography,
IconButton
IconButton,
} from '@mui/material'
import { Box } from '@mui/system'
import { Cancel as CancelIcon } from 'iconoir-react'
@ -64,9 +64,9 @@ const DialogConfirmation = memo(
handleEntering,
fixedWidth,
fixedHeight,
children
children,
}) => {
const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs'))
const isMobile = useMediaQuery((theme) => theme.breakpoints.only('xs'))
return (
<Dialog
@ -75,43 +75,45 @@ const DialogConfirmation = memo(
elevation: 0,
sx: {
minWidth: fixedWidth ? '80vw' : 'auto',
minHeight: fixedHeight ? '80vh' : 'auto'
}
minHeight: fixedHeight ? '80vh' : 'auto',
},
}}
open={open}
onClose={handleCancel}
maxWidth='lg'
scroll='paper'
maxWidth="lg"
scroll="paper"
TransitionProps={{
onEntering: handleEntering
}}>
onEntering: handleEntering,
}}
>
<DialogTitle
sx={{
display: 'flex',
flexWrap: 'nowrap',
alignItems: 'center',
gap: '2em'
gap: '2em',
}}
>
<Box flexGrow={1}>
{title && (
<Typography variant='h6'>
<Typography variant="h6">
{typeof title === 'string' ? Tr(title) : title}
</Typography>
)}
{subheader && (
<Typography variant='body1'>
<Typography variant="body1">
{typeof subheader === 'string' ? Tr(subheader) : subheader}
</Typography>
)}
</Box>
{handleCancel && (
<IconButton
aria-label='close'
aria-label="close"
onClick={handleCancel}
data-cy='dg-cancel-button'
data-cy="dg-cancel-button"
{...cancelButtonProps}
size="large">
size="large"
>
<CancelIcon />
</IconButton>
)}
@ -124,9 +126,9 @@ const DialogConfirmation = memo(
{handleAccept && (
<DialogActions>
<Action
aria-label='accept'
color='secondary'
data-cy='dg-accept-button'
aria-label="accept"
color="secondary"
data-cy="dg-accept-button"
handleClick={handleAccept}
label={T.Accept}
{...acceptButtonProps}
@ -140,14 +142,8 @@ const DialogConfirmation = memo(
export const DialogPropTypes = {
open: PropTypes.bool,
title: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node
]),
subheader: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node
]),
title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
subheader: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
contentProps: PropTypes.object,
handleAccept: PropTypes.func,
acceptButtonProps: PropTypes.object,
@ -156,7 +152,7 @@ export const DialogPropTypes = {
handleEntering: PropTypes.func,
fixedWidth: PropTypes.bool,
fixedHeight: PropTypes.bool,
children: PropTypes.any
children: PropTypes.any,
}
DialogConfirmation.propTypes = DialogPropTypes

View File

@ -21,9 +21,11 @@ import makeStyles from '@mui/styles/makeStyles'
import { useForm, FormProvider } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import DialogConfirmation, { DialogPropTypes } from 'client/components/Dialogs/DialogConfirmation'
import DialogConfirmation, {
DialogPropTypes,
} from 'client/components/Dialogs/DialogConfirmation'
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
content: {
width: '80vw',
height: '60vh',
@ -34,42 +36,46 @@ const useStyles = makeStyles(theme => ({
flexDirection: 'column',
[theme.breakpoints.only('xs')]: {
width: '100vw',
height: '100vh'
}
}
height: '100vh',
},
},
}))
const DialogForm = ({ values, resolver, handleSubmit, dialogProps, children }) => {
const DialogForm = ({
values,
resolver,
handleSubmit,
dialogProps,
children,
}) => {
const classes = useStyles()
const { className, ...contentProps } = dialogProps.contentProps ?? {}
dialogProps.contentProps = {
className: clsx(classes.content, className),
...contentProps
...contentProps,
}
const methods = useForm({
mode: 'onBlur',
reValidateMode: 'onSubmit',
defaultValues: values,
resolver: yupResolver(resolver())
resolver: yupResolver(resolver()),
})
return (
<DialogConfirmation
handleAccept={handleSubmit && methods.handleSubmit(handleSubmit)}
acceptButtonProps={{
isSubmitting: methods.formState.isSubmitting
isSubmitting: methods.formState.isSubmitting,
}}
cancelButtonProps={{
disabled: methods.formState.isSubmitting
disabled: methods.formState.isSubmitting,
}}
{...dialogProps}
>
<FormProvider {...methods}>
{children}
</FormProvider>
<FormProvider {...methods}>{children}</FormProvider>
</DialogConfirmation>
)
}
@ -77,7 +83,7 @@ const DialogForm = ({ values, resolver, handleSubmit, dialogProps, children }) =
DialogForm.propTypes = {
values: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.any),
PropTypes.objectOf(PropTypes.any)
PropTypes.objectOf(PropTypes.any),
]),
resolver: PropTypes.func.isRequired,
handleSubmit: PropTypes.func,
@ -85,8 +91,8 @@ DialogForm.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func
])
PropTypes.func,
]),
}
export default DialogForm

View File

@ -24,16 +24,16 @@ import makeStyles from '@mui/styles/makeStyles'
import { useFetch } from 'client/hooks'
import { DialogConfirmation } from 'client/components/Dialogs'
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
backdrop: {
zIndex: theme.zIndex.drawer + 1,
color: theme.palette.common.white
color: theme.palette.common.white,
},
withTabs: {
overflow: 'hidden',
display: 'flex',
flexDirection: 'column'
}
flexDirection: 'column',
},
}))
const DialogRequest = ({ withTabs, request, dialogProps, children }) => {
@ -41,14 +41,16 @@ const DialogRequest = ({ withTabs, request, dialogProps, children }) => {
const fetchProps = useFetch(request)
const { data, fetchRequest, loading, error } = fetchProps
useEffect(() => { fetchRequest() }, [])
useEffect(() => {
fetchRequest()
}, [])
error && dialogProps?.handleCancel()
if (!data || loading) {
return (
<Backdrop open className={classes.backdrop}>
<CircularProgress color='inherit' />
<CircularProgress color="inherit" />
</Backdrop>
)
}
@ -58,7 +60,7 @@ const DialogRequest = ({ withTabs, request, dialogProps, children }) => {
dialogProps.contentProps = {
className: clsx(classes.withTabs, className),
...contentProps
...contentProps,
}
}
@ -79,9 +81,9 @@ DialogRequest.propTypes = {
acceptButtonProps: PropTypes.objectOf(PropTypes.any),
handleCancel: PropTypes.func,
cancelButtonProps: PropTypes.objectOf(PropTypes.any),
handleEntering: PropTypes.func
handleEntering: PropTypes.func,
}),
children: PropTypes.func
children: PropTypes.func,
}
DialogRequest.defaultProps = {
@ -94,9 +96,9 @@ DialogRequest.defaultProps = {
acceptButtonProps: undefined,
handleCancel: undefined,
cancelButtonProps: undefined,
handleEntering: undefined
handleEntering: undefined,
},
children: () => undefined
children: () => undefined,
}
DialogRequest.displayName = 'DialogRequest'

View File

@ -18,8 +18,4 @@ import DialogRequest from 'client/components/Dialogs/DialogRequest'
import DialogConfirmation from 'client/components/Dialogs/DialogConfirmation'
export * from 'client/components/Dialogs/DialogConfirmation'
export {
DialogConfirmation,
DialogForm,
DialogRequest
}
export { DialogConfirmation, DialogForm, DialogRequest }

View File

@ -20,27 +20,25 @@ import clsx from 'clsx'
import { Fab } from '@mui/material'
import makeStyles from '@mui/styles/makeStyles'
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
root: {
transition: '0.5s ease',
zIndex: theme.zIndex.appBar,
position: 'absolute',
bottom: 60,
right: theme.spacing(5)
}
right: theme.spacing(5),
},
}))
const FloatingActionButton = memo(
({ icon, className, ...props }) => {
const classes = useStyles()
const FloatingActionButton = memo(({ icon, className, ...props }) => {
const classes = useStyles()
return (
<Fab className={clsx(classes.root, className)} {...props}>
{icon}
</Fab>
)
}
)
return (
<Fab className={clsx(classes.root, className)} {...props}>
{icon}
</Fab>
)
})
FloatingActionButton.propTypes = {
icon: PropTypes.node.isRequired,
@ -48,7 +46,7 @@ FloatingActionButton.propTypes = {
color: PropTypes.oneOf(['inherit', 'primary', 'secondary']),
disabled: PropTypes.bool,
size: PropTypes.oneOf(['large', 'medium', 'small']),
variant: PropTypes.oneOf(['extended', 'circular'])
variant: PropTypes.oneOf(['extended', 'circular']),
}
FloatingActionButton.defaultProps = {
@ -57,7 +55,7 @@ FloatingActionButton.defaultProps = {
color: 'primary',
disabled: false,
size: 'large',
variant: 'circular'
variant: 'circular',
}
FloatingActionButton.displayName = 'FloatingActionButton'

View File

@ -32,15 +32,15 @@ const FooterBox = styled('footer')(({ theme }) => ({
right: 0,
zIndex: theme.zIndex.appBar,
textAlign: 'center',
padding: theme.spacing(0.6)
padding: theme.spacing(0.6),
}))
const HeartIcon = styled('span')(({ theme }) => ({
margin: theme.spacing(0, 1),
color: theme.palette.error.dark,
'&:before': {
content: "'❤️'"
}
content: "'❤️'",
},
}))
const Footer = memo(() => {
@ -54,15 +54,13 @@ const Footer = memo(() => {
return (
<FooterBox>
<Typography variant='body2'>
<Typography variant="body2">
{'Made with'}
<HeartIcon role='img' aria-label='heart-emoji' />
<Link href={BY.url} color='primary.contrastText'>
<HeartIcon role="img" aria-label="heart-emoji" />
<Link href={BY.url} color="primary.contrastText">
{BY.text}
</Link>
{version && (
<StatusChip stateColor='secondary' text={version} mx={1} />
)}
{version && <StatusChip stateColor="secondary" text={version} mx={1} />}
</Typography>
</FooterBox>
)

View File

@ -32,11 +32,11 @@ const AutocompleteController = memo(
tooltip = '',
multiple = false,
values = [],
fieldProps: { separators, ...fieldProps } = {}
fieldProps: { separators, ...fieldProps } = {},
}) => {
const {
field: { value: renderValue, onBlur, onChange },
fieldState: { error }
fieldState: { error },
} = useController({ name, control })
const selected = multiple
@ -46,13 +46,13 @@ const AutocompleteController = memo(
return (
<Autocomplete
fullWidth
color='secondary'
color="secondary"
onBlur={onBlur}
onChange={(_, newValue) => {
const newValueToChange = multiple
? newValue?.map(value =>
typeof value === 'string' ? value : ({ text: value, value })
)
? newValue?.map((value) =>
typeof value === 'string' ? value : { text: value, value }
)
: newValue?.value
return onChange(newValueToChange ?? '')
@ -65,28 +65,30 @@ const AutocompleteController = memo(
tags.map((tag, index) => (
<Chip
key={tag}
size='small'
variant='outlined'
size="small"
variant="outlined"
label={tag}
{...getTagProps({ index })}
/>
))
}
getOptionLabel={option => option.text}
isOptionEqualToValue={option => option.value === renderValue}
getOptionLabel={(option) => option.text}
isOptionEqualToValue={(option) => option.value === renderValue}
renderInput={({ inputProps, ...inputParams }) => (
<TextField
label={labelCanBeTranslated(label) ? Tr(label) : label}
inputProps={{ ...inputProps, 'data-cy': cy }}
error={Boolean(error)}
helperText={Boolean(error) && <ErrorHelper label={error?.message} />}
helperText={
Boolean(error) && <ErrorHelper label={error?.message} />
}
FormHelperTextProps={{ 'data-cy': `${cy}-error` }}
{...inputParams}
/>
)}
{...(tooltip && {
loading: true,
loadingText: labelCanBeTranslated(tooltip) ? Tr(tooltip) : tooltip
loadingText: labelCanBeTranslated(tooltip) ? Tr(tooltip) : tooltip,
})}
{...(Array.isArray(separators) && {
autoSelect: true,
@ -95,16 +97,15 @@ const AutocompleteController = memo(
event.target.blur()
event.target.focus()
}
}
},
})}
{...fieldProps}
/>
)
},
(prevProps, nextProps) => (
prevProps.error === nextProps.error &&
prevProps.values === nextProps.values
))
(prevProps, nextProps) =>
prevProps.error === nextProps.error && prevProps.values === nextProps.values
)
AutocompleteController.propTypes = {
control: PropTypes.object,
@ -114,7 +115,7 @@ AutocompleteController.propTypes = {
tooltip: PropTypes.any,
multiple: PropTypes.bool,
values: PropTypes.arrayOf(PropTypes.object),
fieldProps: PropTypes.object
fieldProps: PropTypes.object,
}
AutocompleteController.displayName = 'AutocompleteController'

View File

@ -16,7 +16,13 @@
import { memo } from 'react'
import PropTypes from 'prop-types'
import { styled, FormControl, FormControlLabel, FormHelperText, Checkbox } from '@mui/material'
import {
styled,
FormControl,
FormControlLabel,
FormHelperText,
Checkbox,
} from '@mui/material'
import { useController } from 'react-hook-form'
import { ErrorHelper, Tooltip } from 'client/components/FormControl'
@ -26,7 +32,7 @@ import { generateKey } from 'client/utils'
const Label = styled('span')({
display: 'flex',
alignItems: 'center',
gap: '0.5em'
gap: '0.5em',
})
const CheckboxController = memo(
@ -36,22 +42,22 @@ const CheckboxController = memo(
name = '',
label = '',
tooltip,
fieldProps = {}
fieldProps = {},
}) => {
const {
field: { value = false, onChange },
fieldState: { error }
fieldState: { error },
} = useController({ name, control })
return (
<FormControl fullWidth error={Boolean(error)} margin='dense'>
<FormControl fullWidth error={Boolean(error)} margin="dense">
<FormControlLabel
control={
<Checkbox
onChange={e => onChange(e.target.checked)}
onChange={(e) => onChange(e.target.checked)}
name={name}
checked={Boolean(value)}
color='secondary'
color="secondary"
inputProps={{ 'data-cy': cy }}
{...fieldProps}
/>
@ -62,7 +68,7 @@ const CheckboxController = memo(
{tooltip && <Tooltip title={tooltip} />}
</Label>
}
labelPlacement='end'
labelPlacement="end"
/>
{Boolean(error) && (
<FormHelperText data-cy={`${cy}-error`}>
@ -81,7 +87,7 @@ CheckboxController.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.any,
tooltip: PropTypes.any,
fieldProps: PropTypes.object
fieldProps: PropTypes.object,
}
CheckboxController.displayName = 'CheckboxController'

View File

@ -24,20 +24,26 @@ import { Tr, labelCanBeTranslated } from 'client/components/HOC'
const ErrorTypo = styled(Typography)(({ theme }) => ({
...theme.typography.body1,
paddingLeft: theme.spacing(1),
overflowWrap: 'anywhere'
overflowWrap: 'anywhere',
}))
const ErrorHelper = memo(({ label, ...rest }) => (
<Stack component='span' color='error.dark' direction='row' alignItems='center' {...rest}>
<Stack
component="span"
color="error.dark"
direction="row"
alignItems="center"
{...rest}
>
<WarningIcon />
<ErrorTypo component='span' data-cy='error-text'>
<ErrorTypo component="span" data-cy="error-text">
{labelCanBeTranslated(label) ? Tr(label) : label}
</ErrorTypo>
</Stack>
))
ErrorHelper.propTypes = {
label: oneOfType([string, node])
label: oneOfType([string, node]),
}
ErrorHelper.displayName = 'ErrorHelper'

View File

@ -20,7 +20,11 @@ import { styled, FormControl, FormHelperText } from '@mui/material'
import { Check as CheckIcon, Page as FileIcon } from 'iconoir-react'
import { useController } from 'react-hook-form'
import { ErrorHelper, Tooltip, SubmitButton } from 'client/components/FormControl'
import {
ErrorHelper,
Tooltip,
SubmitButton,
} from 'client/components/FormControl'
import { Tr, labelCanBeTranslated } from 'client/components/HOC'
import { generateKey } from 'client/utils'
@ -31,8 +35,8 @@ const Label = styled('label')(({ theme, error }) => ({
alignItems: 'center',
gap: '1em',
...(error && {
color: theme.palette.error.main
})
color: theme.palette.error.main,
}),
}))
const FileController = memo(
@ -45,30 +49,33 @@ const FileController = memo(
validationBeforeTransform,
transform,
fieldProps = {},
formContext = {}
formContext = {},
}) => {
const { setValue, setError, clearErrors, watch } = formContext
const {
field: { ref, value, onChange, ...inputProps },
fieldState: { error }
fieldState: { error },
} = useController({ name, control })
const [isLoading, setLoading] = useState(() => false)
const [success, setSuccess] = useState(() => !error && !!watch(name))
const timer = useRef()
useEffect(() => () => {
clearTimeout(timer.current)
}, [])
useEffect(
() => () => {
clearTimeout(timer.current)
},
[]
)
/**
* Simulate 1 second loading, then set success or error.
*
* @param {string} message - Message
*/
const handleDelayState = message => {
// simulate is loading for one second
const handleDelayState = (message) => {
// simulate is loading for one second
timer.current = setTimeout(() => {
setSuccess(!message)
setLoading(false)
@ -82,7 +89,7 @@ const FileController = memo(
*
* @param {ChangeEvent} event - Change event object
*/
const handleChange = async event => {
const handleChange = async (event) => {
try {
const file = event.target.files?.[0]
@ -108,19 +115,19 @@ const FileController = memo(
}
return (
<FormControl fullWidth margin='dense'>
<FormControl fullWidth margin="dense">
<HiddenInput
{...inputProps}
ref={ref}
id={cy}
type='file'
type="file"
onChange={handleChange}
{...fieldProps}
/>
<Label htmlFor={cy} error={error ? 'error' : undefined}>
<SubmitButton
color={success ? 'success' : 'secondary'}
component='span'
component="span"
data-cy={`${cy}-button`}
isSubmitting={isLoading}
label={success ? <CheckIcon /> : <FileIcon />}
@ -151,7 +158,7 @@ FileController.propTypes = {
validationBeforeTransform: PropTypes.arrayOf(
PropTypes.shape({
message: PropTypes.string,
test: PropTypes.func
test: PropTypes.func,
})
),
transform: PropTypes.func,
@ -161,8 +168,8 @@ FileController.propTypes = {
setError: PropTypes.func,
clearErrors: PropTypes.func,
watch: PropTypes.func,
register: PropTypes.func
})
register: PropTypes.func,
}),
}
FileController.displayName = 'FileController'

View File

@ -37,7 +37,7 @@ const WrapperToLoadMode = ({ children, mode }) => {
// remove all styles when component will be unmounted
document
.querySelectorAll('[id^=ace]')
.forEach(child => child.parentNode.removeChild(child))
.forEach((child) => child.parentNode.removeChild(child))
}
}, [])
@ -64,7 +64,7 @@ const InputCode = ({ code, mode, ...props }) => (
editorProps={{ $blockScrolling: true }}
setOptions={{
useWorker: false,
tabSize: 2
tabSize: 2,
}}
{...props}
/>
@ -81,13 +81,13 @@ InputCode.propTypes = {
'css',
'dockerfile',
'markdown',
'xml'
])
'xml',
]),
}
InputCode.defaultProps = {
code: '',
mode: 'json'
mode: 'json',
}
export default InputCode

View File

@ -21,44 +21,46 @@ import { EyeEmpty as Visibility, EyeOff as VisibilityOff } from 'iconoir-react'
import { TextController } from 'client/components/FormControl'
const PasswordController = memo(({ fieldProps, ...props }) => {
const [showPassword, setShowPassword] = useState(() => false)
const PasswordController = memo(
({ fieldProps, ...props }) => {
const [showPassword, setShowPassword] = useState(() => false)
const handleClickShowPassword = useCallback(() => {
setShowPassword(prev => !prev)
}, [setShowPassword])
const handleClickShowPassword = useCallback(() => {
setShowPassword((prev) => !prev)
}, [setShowPassword])
return (
<TextController
{...props}
type={showPassword ? 'text' : 'password'}
fieldProps={{
InputProps: {
endAdornment: <InputAdornment position='end'>
<IconButton
aria-label='toggle password visibility'
onClick={handleClickShowPassword}
>
{showPassword ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
},
...fieldProps
}}
/>
)
},
(prevProps, nextProps) =>
prevProps.error === nextProps.error &&
prevProps.type === nextProps.type
return (
<TextController
{...props}
type={showPassword ? 'text' : 'password'}
fieldProps={{
InputProps: {
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
>
{showPassword ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
),
},
...fieldProps,
}}
/>
)
},
(prevProps, nextProps) =>
prevProps.error === nextProps.error && prevProps.type === nextProps.type
)
PasswordController.propTypes = {
fieldProps: PropTypes.object
fieldProps: PropTypes.object,
}
PasswordController.defaultProps = {
fieldProps: undefined
fieldProps: undefined,
}
PasswordController.displayName = 'PasswordController'

View File

@ -33,17 +33,23 @@ const SelectController = memo(
values = [],
renderValue,
tooltip,
fieldProps = {}
fieldProps = {},
}) => {
const defaultValue = multiple ? [values?.[0]?.value] : values?.[0]?.value
const {
field: { ref, value: optionSelected = defaultValue, onChange, ...inputProps },
fieldState: { error }
field: {
ref,
value: optionSelected = defaultValue,
onChange,
...inputProps
},
fieldState: { error },
} = useController({ name, control })
const needShrink = useMemo(
() => multiple || values.find(v => v.value === optionSelected)?.text !== '',
() =>
multiple || values.find((v) => v.value === optionSelected)?.text !== '',
[optionSelected]
)
@ -58,16 +64,22 @@ const SelectController = memo(
{...inputProps}
inputRef={ref}
value={optionSelected}
onChange={!multiple ? onChange : evt => {
const { target: { options } } = evt
const newValue = []
onChange={
!multiple
? onChange
: (evt) => {
const {
target: { options },
} = evt
const newValue = []
for (const option of options) {
option.selected && newValue.push(option.value)
}
for (const option of options) {
option.selected && newValue.push(option.value)
}
onChange(newValue)
}}
onChange(newValue)
}
}
select
fullWidth
SelectProps={{ native: true, multiple }}
@ -76,7 +88,7 @@ const SelectController = memo(
InputProps={{
startAdornment:
(optionSelected && renderValue?.(optionSelected)) ||
(tooltip && <Tooltip title={tooltip} position='start' />)
(tooltip && <Tooltip title={tooltip} position="start" />),
}}
inputProps={{ 'data-cy': cy }}
error={Boolean(error)}
@ -84,11 +96,11 @@ const SelectController = memo(
FormHelperTextProps={{ 'data-cy': `${cy}-error` }}
{...fieldProps}
>
{values?.map(({ text, value = '' }) =>
{values?.map(({ text, value = '' }) => (
<option key={`${name}-${value}`} value={value}>
{text}
</option>
)}
))}
</TextField>
)
},
@ -109,7 +121,7 @@ SelectController.propTypes = {
multiple: PropTypes.bool,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
renderValue: PropTypes.func,
fieldProps: PropTypes.object
fieldProps: PropTypes.object,
}
SelectController.displayName = 'SelectController'

View File

@ -16,7 +16,13 @@
import { memo } from 'react'
import PropTypes from 'prop-types'
import { Typography, TextField, Slider, FormHelperText, Grid } from '@mui/material'
import {
Typography,
TextField,
Slider,
FormHelperText,
Grid,
} from '@mui/material'
import { useController } from 'react-hook-form'
import { ErrorHelper } from 'client/components/FormControl'
@ -29,11 +35,11 @@ const SliderController = memo(
cy = `slider-${generateKey()}`,
name = '',
label = '',
fieldProps = {}
fieldProps = {},
}) => {
const {
field: { value, onChange, ...inputProps },
fieldState: { error }
fieldState: { error },
} = useController({ name, control })
const sliderId = `${cy}-slider`
@ -44,13 +50,13 @@ const SliderController = memo(
<Typography id={sliderId} gutterBottom>
{labelCanBeTranslated(label) ? Tr(label) : label}
</Typography>
<Grid container spacing={2} alignItems='center'>
<Grid container spacing={2} alignItems="center">
<Grid item xs>
<Slider
color='secondary'
color="secondary"
value={typeof value === 'number' ? value : 0}
aria-labelledby={sliderId}
valueLabelDisplay='auto'
valueLabelDisplay="auto"
data-cy={sliderId}
onChange={(_, val) => onChange(val)}
{...fieldProps}
@ -62,15 +68,17 @@ const SliderController = memo(
fullWidth
value={value}
error={Boolean(error)}
type='number'
type="number"
inputProps={{
'data-cy': inputId,
'aria-labelledby': sliderId,
...fieldProps
...fieldProps,
}}
onChange={evt => onChange(
evt.target.value === '' ? '0' : Number(evt.target.value)
)}
onChange={(evt) =>
onChange(
evt.target.value === '' ? '0' : Number(evt.target.value)
)
}
/>
</Grid>
</Grid>
@ -93,7 +101,7 @@ SliderController.propTypes = {
tooltip: PropTypes.any,
multiple: PropTypes.bool,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
fieldProps: PropTypes.object
fieldProps: PropTypes.object,
}
SliderController.displayName = 'SliderController'

View File

@ -17,22 +17,28 @@ import { forwardRef, memo } from 'react'
import PropTypes from 'prop-types'
import clsx from 'clsx'
import { CircularProgress, Button, IconButton, Tooltip, Typography } from '@mui/material'
import {
CircularProgress,
Button,
IconButton,
Tooltip,
Typography,
} from '@mui/material'
import makeStyles from '@mui/styles/makeStyles'
import { Tr, ConditionalWrap } from 'client/components/HOC'
import { T } from 'client/constants'
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
root: {
transition: 'disabled 0.5s ease',
boxShadow: 'none'
boxShadow: 'none',
},
disabled: {
'& svg': {
color: theme.palette.action.disabled
}
}
color: theme.palette.action.disabled,
},
},
}))
const ButtonComponent = forwardRef(
@ -42,8 +48,9 @@ const ButtonComponent = forwardRef(
{children}
</IconButton>
) : (
<Button ref={ref}
type='submit'
<Button
ref={ref}
type="submit"
endIcon={endicon}
variant={variant}
{...props}
@ -56,16 +63,14 @@ const ButtonComponent = forwardRef(
const TooltipComponent = ({ tooltip, tooltipProps, children }) => (
<ConditionalWrap
condition={tooltip && tooltip !== ''}
wrap={wrapperChildren => (
wrap={(wrapperChildren) => (
<Tooltip
arrow
placement='bottom'
title={<Typography variant='subtitle2'>{tooltip}</Typography>}
placement="bottom"
title={<Typography variant="subtitle2">{tooltip}</Typography>}
{...tooltipProps}
>
<span>
{wrapperChildren}
</span>
<span>{wrapperChildren}</span>
</Tooltip>
)}
>
@ -81,18 +86,16 @@ const SubmitButton = memo(
return (
<TooltipComponent {...props}>
<ButtonComponent
className={clsx(
classes.root,
className,
{ [classes.disabled]: disabled }
)}
className={clsx(classes.root, className, {
[classes.disabled]: disabled,
})}
disabled={disabled || isSubmitting}
icon={icon}
aria-label={label ?? T.Submit}
{...props}
>
{isSubmitting && (
<CircularProgress color='secondary' size={progressSize} />
<CircularProgress color="secondary" size={progressSize} />
)}
{!isSubmitting && (icon ?? label ?? Tr(T.Submit))}
</ButtonComponent>
@ -118,7 +121,7 @@ export const SubmitButtonPropTypes = {
className: PropTypes.string,
color: PropTypes.string,
size: PropTypes.string,
variant: PropTypes.string
variant: PropTypes.string,
}
TooltipComponent.propTypes = SubmitButtonPropTypes

View File

@ -16,7 +16,13 @@
import { memo } from 'react'
import PropTypes from 'prop-types'
import { styled, FormControl, FormControlLabel, FormHelperText, Switch } from '@mui/material'
import {
styled,
FormControl,
FormControlLabel,
FormHelperText,
Switch,
} from '@mui/material'
import { useController } from 'react-hook-form'
import { ErrorHelper, Tooltip } from 'client/components/FormControl'
@ -26,7 +32,7 @@ import { generateKey } from 'client/utils'
const Label = styled('span')({
display: 'flex',
alignItems: 'center',
gap: '0.5em'
gap: '0.5em',
})
const SwitchController = memo(
@ -36,22 +42,22 @@ const SwitchController = memo(
name = '',
label = '',
tooltip,
fieldProps = {}
fieldProps = {},
}) => {
const {
field: { value = false, onChange },
fieldState: { error }
fieldState: { error },
} = useController({ name, control })
return (
<FormControl fullWidth error={Boolean(error)} margin='dense'>
<FormControl fullWidth error={Boolean(error)} margin="dense">
<FormControlLabel
control={
<Switch
onChange={e => onChange(e.target.checked)}
onChange={(e) => onChange(e.target.checked)}
name={name}
checked={Boolean(value)}
color='secondary'
color="secondary"
inputProps={{ 'data-cy': cy }}
{...fieldProps}
/>
@ -62,7 +68,7 @@ const SwitchController = memo(
{tooltip && <Tooltip title={tooltip} />}
</Label>
}
labelPlacement='end'
labelPlacement="end"
/>
{Boolean(error) && (
<FormHelperText data-cy={`${cy}-error`}>
@ -81,7 +87,7 @@ SwitchController.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.any,
tooltip: PropTypes.any,
fieldProps: PropTypes.object
fieldProps: PropTypes.object,
}
SwitchController.displayName = 'SwitchController'

View File

@ -21,11 +21,16 @@ import Legend from 'client/components/Forms/Legend'
import { ErrorHelper } from 'client/components/FormControl'
import { generateKey } from 'client/utils'
const defaultGetRowId = item => typeof item === 'object' ? item?.id ?? item?.ID : item
const defaultGetRowId = (item) =>
typeof item === 'object' ? item?.id ?? item?.ID : item
const getSelectedRowIds = value => [value ?? []]
.flat()
.reduce((initialSelected, rowId) => ({ ...initialSelected, [rowId]: true }), {})
const getSelectedRowIds = (value) =>
[value ?? []]
.flat()
.reduce(
(initialSelected, rowId) => ({ ...initialSelected, [rowId]: true }),
{}
)
const TableController = memo(
({
@ -38,16 +43,18 @@ const TableController = memo(
singleSelect = true,
getRowId = defaultGetRowId,
formContext = {},
fieldProps: { initialState, ...fieldProps } = {}
fieldProps: { initialState, ...fieldProps } = {},
}) => {
const { clearErrors } = formContext
const {
field: { value, onChange },
fieldState: { error }
fieldState: { error },
} = useController({ name, control })
const [initialRows, setInitialRows] = useState(() => getSelectedRowIds(value))
const [initialRows, setInitialRows] = useState(() =>
getSelectedRowIds(value)
)
useEffect(() => {
onChange(singleSelect ? undefined : [])
@ -58,11 +65,7 @@ const TableController = memo(
<>
<Legend title={label} tooltip={tooltip} />
{error && (
<ErrorHelper
data-cy={`${cy}-error`}
label={error?.message}
mb={2}
/>
<ErrorHelper data-cy={`${cy}-error`} label={error?.message} mb={2} />
)}
<Table
pageSize={4}
@ -71,7 +74,7 @@ const TableController = memo(
onlyGlobalSelectedRows
getRowId={getRowId}
initialState={{ ...initialState, selectedRowIds: initialRows }}
onSelectedRowsChange={rows => {
onSelectedRowsChange={(rows) => {
const rowValues = rows?.map(({ original }) => getRowId(original))
onChange(singleSelect ? rowValues?.[0] : rowValues)
@ -105,8 +108,8 @@ TableController.propTypes = {
setError: PropTypes.func,
clearErrors: PropTypes.func,
watch: PropTypes.func,
register: PropTypes.func
})
register: PropTypes.func,
}),
}
TableController.displayName = 'TableController'

View File

@ -34,17 +34,19 @@ const TextController = memo(
tooltip,
watcher,
dependencies,
fieldProps = {}
fieldProps = {},
}) => {
const watch = dependencies && useWatch({
control,
name: dependencies,
disabled: dependencies === null
})
const watch =
dependencies &&
useWatch({
control,
name: dependencies,
disabled: dependencies === null,
})
const {
field: { ref, value = '', onChange, ...inputProps },
fieldState: { error }
fieldState: { error },
} = useController({ name, control })
useEffect(() => {
@ -66,7 +68,7 @@ const TextController = memo(
type={type}
label={labelCanBeTranslated(label) ? Tr(label) : label}
InputProps={{
endAdornment: tooltip && <Tooltip title={tooltip} />
endAdornment: tooltip && <Tooltip title={tooltip} />,
}}
inputProps={{ 'data-cy': cy }}
error={Boolean(error)}
@ -95,9 +97,9 @@ TextController.propTypes = {
watcher: PropTypes.func,
dependencies: PropTypes.oneOfType([
PropTypes.string,
PropTypes.arrayOf(PropTypes.string)
PropTypes.arrayOf(PropTypes.string),
]),
fieldProps: PropTypes.object
fieldProps: PropTypes.object,
}
TextController.displayName = 'TextController'

View File

@ -27,7 +27,7 @@ const WrapperToLoadLib = ({ children, id, lib }) => {
const [loading, setLoading] = useState(true)
useEffect(() => {
const loadLib = async lib => {
const loadLib = async (lib) => {
try {
await import(lib)
} finally {
@ -41,7 +41,7 @@ const WrapperToLoadLib = ({ children, id, lib }) => {
// remove all styles when component will be unmounted
document
.querySelectorAll(`[id^=${id}]`)
.forEach(child => child.parentNode.removeChild(child))
.forEach((child) => child.parentNode.removeChild(child))
}
}, [])
@ -50,7 +50,10 @@ const WrapperToLoadLib = ({ children, id, lib }) => {
const TimeController = memo(
({ control, cy, name, label, error, fieldProps }) => (
<WrapperToLoadLib id='flatpicker' lib={'flatpickr/dist/themes/material_blue.css'}>
<WrapperToLoadLib
id="flatpicker"
lib={'flatpickr/dist/themes/material_blue.css'}
>
<Controller
render={({ value, onChange, onBlur }) => {
const translated = typeof label === 'string' ? Tr(label) : label
@ -60,7 +63,9 @@ const TimeController = memo(
onblur={onBlur}
onChange={onChange}
// onCreate={function (flatpickr) { this.calendar = flatpickr }}
onDestroy={() => { onChange(undefined) }}
onDestroy={() => {
onChange(undefined)
}}
data-enable-time
options={{ allowInput: true }}
render={({ defaultValue, ...props }, ref) => (
@ -103,8 +108,8 @@ TimeController.propTypes = {
setError: PropTypes.func,
clearErrors: PropTypes.func,
watch: PropTypes.func,
register: PropTypes.func
})
register: PropTypes.func,
}),
}
TimeController.defaultProps = {
@ -113,7 +118,7 @@ TimeController.defaultProps = {
name: '',
label: '',
error: false,
fieldProps: undefined
fieldProps: undefined,
}
TimeController.displayName = 'TimeController'

View File

@ -30,11 +30,11 @@ const TimeController = memo(
name = '',
label = '',
type = 'datetime-local',
fieldProps = {}
fieldProps = {},
}) => {
const {
field: { ref, value, ...inputProps },
fieldState: { error }
fieldState: { error },
} = useController({ name, control })
return (
@ -53,8 +53,7 @@ const TimeController = memo(
)
},
(prevProps, nextProps) =>
prevProps.error === nextProps.error &&
prevProps.label === nextProps.label
prevProps.error === nextProps.error && prevProps.label === nextProps.label
)
TimeController.propTypes = {
@ -71,8 +70,8 @@ TimeController.propTypes = {
setError: PropTypes.func,
clearErrors: PropTypes.func,
watch: PropTypes.func,
register: PropTypes.func
})
register: PropTypes.func,
}),
}
TimeController.displayName = 'TimeController'

View File

@ -21,7 +21,7 @@ import {
FormControl,
ToggleButtonGroup,
ToggleButton,
FormHelperText
FormHelperText,
} from '@mui/material'
import { useController } from 'react-hook-form'
@ -34,8 +34,8 @@ const Label = styled('label')(({ theme, error }) => ({
alignItems: 'center',
gap: '1em',
...(error && {
color: theme.palette.error.main
})
color: theme.palette.error.main,
}),
}))
const ToggleController = memo(
@ -47,24 +47,24 @@ const ToggleController = memo(
multiple = false,
values = [],
tooltip,
fieldProps = {}
fieldProps = {},
}) => {
const defaultValue = multiple ? [values?.[0]?.value] : values?.[0]?.value
const {
field: { ref, value: optionSelected = defaultValue, onChange },
fieldState: { error: { message } = {} }
fieldState: { error: { message } = {} },
} = useController({ name, control })
useEffect(() => {
if (optionSelected) {
const exists = values?.find(option => option.value === optionSelected)
const exists = values?.find((option) => option.value === optionSelected)
!exists && onChange()
}
}, [])
return (
<FormControl fullWidth margin='dense'>
<FormControl fullWidth margin="dense">
{label && (
<Label htmlFor={cy} error={Boolean(message)}>
{labelCanBeTranslated(label) ? Tr(label) : label}
@ -81,11 +81,11 @@ const ToggleController = memo(
data-cy={cy}
{...fieldProps}
>
{values?.map(({ text, value = '' }) =>
{values?.map(({ text, value = '' }) => (
<ToggleButton key={`${name}-${value}`} value={value}>
{text}
</ToggleButton>
)}
))}
</ToggleButtonGroup>
{Boolean(message) && (
<FormHelperText data-cy={`${cy}-error`}>
@ -111,7 +111,7 @@ ToggleController.propTypes = {
multiple: PropTypes.bool,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
renderValue: PropTypes.func,
fieldProps: PropTypes.object
fieldProps: PropTypes.object,
}
ToggleController.displayName = 'ToggleController'

View File

@ -20,40 +20,47 @@ import { QuestionMarkCircle } from 'iconoir-react'
import { InputAdornment, Typography, Tooltip } from '@mui/material'
import { Tr, labelCanBeTranslated } from 'client/components/HOC'
const AdornmentWithTooltip = memo(({ title, position = 'end', children }) => {
if (!title || title === '' || (Array.isArray(title) && title.length === 0)) {
return null
}
const AdornmentWithTooltip = memo(
({ title, position = 'end', children }) => {
if (
!title ||
title === '' ||
(Array.isArray(title) && title.length === 0)
) {
return null
}
return (
<Tooltip
arrow
placement='bottom'
title={
<Typography variant='subtitle2'>
{labelCanBeTranslated(title) ? Tr(title) : title}
</Typography>
}
>
<InputAdornment position={position} style={{ cursor: 'help' }}>
{children ?? <QuestionMarkCircle />}
</InputAdornment>
</Tooltip>
)
}, (prevProps, nextProps) =>
Array.isArray(nextProps.title)
? prevProps.title?.[0] === nextProps.title?.[0] || prevProps.title === nextProps.title?.[0]
: prevProps.title === nextProps.title
return (
<Tooltip
arrow
placement="bottom"
title={
<Typography variant="subtitle2">
{labelCanBeTranslated(title) ? Tr(title) : title}
</Typography>
}
>
<InputAdornment position={position} style={{ cursor: 'help' }}>
{children ?? <QuestionMarkCircle />}
</InputAdornment>
</Tooltip>
)
},
(prevProps, nextProps) =>
Array.isArray(nextProps.title)
? prevProps.title?.[0] === nextProps.title?.[0] ||
prevProps.title === nextProps.title?.[0]
: prevProps.title === nextProps.title
)
AdornmentWithTooltip.propTypes = {
title: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node,
PropTypes.object
PropTypes.object,
]),
children: PropTypes.any,
position: PropTypes.oneOf(['start', 'end'])
position: PropTypes.oneOf(['start', 'end']),
}
AdornmentWithTooltip.displayName = 'AdornmentWithTooltip'

View File

@ -25,7 +25,9 @@ import TextController from 'client/components/FormControl/TextController'
import TimeController from 'client/components/FormControl/TimeController'
import ToggleController from 'client/components/FormControl/ToggleController'
import SubmitButton, { SubmitButtonPropTypes } from 'client/components/FormControl/SubmitButton'
import SubmitButton, {
SubmitButtonPropTypes,
} from 'client/components/FormControl/SubmitButton'
import InputCode from 'client/components/FormControl/InputCode'
import ErrorHelper from 'client/components/FormControl/ErrorHelper'
import Tooltip from 'client/components/FormControl/Tooltip'
@ -42,10 +44,9 @@ export {
TextController,
TimeController,
ToggleController,
SubmitButton,
SubmitButtonPropTypes,
InputCode,
ErrorHelper,
Tooltip
Tooltip,
}

View File

@ -18,26 +18,29 @@ import PropTypes from 'prop-types'
import { Button, MobileStepper, Typography, Box, alpha } from '@mui/material'
import makeStyles from '@mui/styles/makeStyles'
import { NavArrowLeft as PreviousIcon, NavArrowRight as NextIcon } from 'iconoir-react'
import {
NavArrowLeft as PreviousIcon,
NavArrowRight as NextIcon,
} from 'iconoir-react'
import { Translate, labelCanBeTranslated } from 'client/components/HOC'
import { T } from 'client/constants'
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
root: {
position: 'sticky',
top: -15,
background: alpha(theme.palette.primary.light, 0.65),
zIndex: theme.zIndex.mobileStepper,
margin: theme.spacing(2, 0)
margin: theme.spacing(2, 0),
},
title: {
padding: theme.spacing(1, 2),
color: theme.palette.primary.contrastText
color: theme.palette.primary.contrastText,
},
error: { padding: theme.spacing(1, 2) },
button: { color: theme.palette.action.active },
stepper: { background: 'transparent' }
stepper: { background: 'transparent' },
}))
const CustomMobileStepper = ({
@ -48,7 +51,7 @@ const CustomMobileStepper = ({
disabledBack,
handleNext,
handleBack,
errors
errors,
}) => {
const classes = useStyles()
const { id, label } = steps[activeStep]
@ -60,23 +63,26 @@ const CustomMobileStepper = ({
{labelCanBeTranslated(label) ? <Translate word={label} /> : label}
</Typography>
{Boolean(errors[id]) && (
<Typography className={classes.error} variant='caption' color='error'>
{labelCanBeTranslated(label)
? <Translate word={errors[id]?.message} /> : errors[id]?.message}
<Typography className={classes.error} variant="caption" color="error">
{labelCanBeTranslated(label) ? (
<Translate word={errors[id]?.message} />
) : (
errors[id]?.message
)}
</Typography>
)}
</Box>
<MobileStepper
className={classes.stepper}
variant='progress'
position='static'
variant="progress"
position="static"
steps={totalSteps}
activeStep={activeStep}
LinearProgressProps={{ color: 'secondary' }}
backButton={
<Button
className={classes.button}
size='small'
size="small"
onClick={handleBack}
disabled={disabledBack}
>
@ -85,11 +91,12 @@ const CustomMobileStepper = ({
</Button>
}
nextButton={
<Button className={classes.button} size='small' onClick={handleNext}>
{activeStep === lastStep
? <Translate word={T.Finish} />
: <Translate word={T.Next} />
}
<Button className={classes.button} size="small" onClick={handleNext}>
{activeStep === lastStep ? (
<Translate word={T.Finish} />
) : (
<Translate word={T.Next} />
)}
<NextIcon />
</Button>
}
@ -101,11 +108,8 @@ const CustomMobileStepper = ({
CustomMobileStepper.propTypes = {
steps: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]).isRequired,
label: PropTypes.string.isRequired
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
label: PropTypes.string.isRequired,
})
),
totalSteps: PropTypes.number,
@ -115,8 +119,8 @@ CustomMobileStepper.propTypes = {
handleNext: PropTypes.func,
handleBack: PropTypes.func,
errors: PropTypes.shape({
message: PropTypes.string
})
message: PropTypes.string,
}),
}
CustomMobileStepper.defaultProps = {
@ -127,7 +131,7 @@ CustomMobileStepper.defaultProps = {
disabledBack: false,
handleNext: () => undefined,
handleBack: () => undefined,
errors: undefined
errors: undefined,
}
CustomMobileStepper.displayName = 'MobileStepper'

View File

@ -24,8 +24,8 @@ const ControlWrapper = styled('div')(({ theme }) => ({
gap: '1em',
[theme.breakpoints.down('lg')]: {
justifyContent: 'space-between',
alignItems: 'center'
}
alignItems: 'center',
},
}))
/**
@ -34,17 +34,17 @@ const ControlWrapper = styled('div')(({ theme }) => ({
* @returns {JSXElementConstructor} Skeleton loader component
*/
const SkeletonStepsForm = memo(() => {
const isMobile = useMediaQuery(theme => theme.breakpoints.down('lg'))
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('lg'))
return (
<div>
<Skeleton variant="rectangular" height={120} width='100%' />
<Skeleton variant="rectangular" height={120} width="100%" />
<ControlWrapper>
<Skeleton variant="rectangular" height={35} width={95} />
{isMobile && <Skeleton variant="rectangular" height={8} width='100%' />}
{isMobile && <Skeleton variant="rectangular" height={8} width="100%" />}
<Skeleton variant="rectangular" height={35} width={95} />
</ControlWrapper>
<Skeleton variant="rectangular" height={200} width='100%' />
<Skeleton variant="rectangular" height={200} width="100%" />
</div>
)
})

View File

@ -22,7 +22,9 @@ import Step from '@mui/material/Step'
import StepLabel from '@mui/material/StepLabel'
import StepButton from '@mui/material/StepButton'
import StepIcon, { stepIconClasses } from '@mui/material/StepIcon'
import StepConnector, { stepConnectorClasses } from '@mui/material/StepConnector'
import StepConnector, {
stepConnectorClasses,
} from '@mui/material/StepConnector'
import { styled } from '@mui/styles'
import { SubmitButton } from 'client/components/FormControl'
@ -34,43 +36,44 @@ const StepperStyled = styled(Stepper)(({ theme }) => ({
top: -15,
minHeight: 100,
background: alpha(theme.palette.background.paper, 0.95),
zIndex: theme.zIndex.mobileStepper
zIndex: theme.zIndex.mobileStepper,
}))
const ConnectorStyled = styled(StepConnector)(({ theme }) => ({
[`&.${stepConnectorClasses.alternativeLabel}`]: {
top: 10,
left: 'calc(-50% + 16px)',
right: 'calc(50% + 16px)'
right: 'calc(50% + 16px)',
},
[`&.${stepConnectorClasses.active}`]: {
[`& .${stepConnectorClasses.line}`]: {
borderColor: theme.palette.secondary[700]
}
borderColor: theme.palette.secondary[700],
},
},
[`&.${stepConnectorClasses.completed}`]: {
[`& .${stepConnectorClasses.line}`]: {
borderColor: theme.palette.secondary[700]
}
borderColor: theme.palette.secondary[700],
},
},
[`& .${stepConnectorClasses.line}`]: {
borderColor: theme.palette.mode === SCHEMES.DARK
? theme.palette.grey[600]
: theme.palette.grey[400],
borderColor:
theme.palette.mode === SCHEMES.DARK
? theme.palette.grey[600]
: theme.palette.grey[400],
borderTopWidth: 2,
borderRadius: 1
}
borderRadius: 1,
},
}))
const StepIconStyled = styled(StepIcon)(({ theme }) => ({
color: theme.palette.text.hint,
display: 'block',
[`&.${stepIconClasses.completed}, &.${stepIconClasses.active}`]: {
color: theme.palette.secondary[700]
color: theme.palette.secondary[700],
},
[`&.${stepIconClasses.error}`]: {
color: theme.palette.error.main
}
color: theme.palette.error.main,
},
}))
const CustomStepper = ({
@ -82,21 +85,28 @@ const CustomStepper = ({
handleNext,
handleBack,
errors,
isSubmitting
isSubmitting,
}) => (
<>
<StepperStyled nonLinear activeStep={activeStep} connector={<ConnectorStyled />}>
<StepperStyled
nonLinear
activeStep={activeStep}
connector={<ConnectorStyled />}
>
{steps?.map(({ id, label }, stepIdx) => (
<Step key={id} completed={activeStep > stepIdx}>
<StepButton
onClick={() => handleStep(stepIdx)}
disabled={activeStep + 1 < stepIdx}
optional={errors[id] && (
<Typography variant='caption' color='error'>
{labelCanBeTranslated(errors[id]?.message)
? Tr(errors[id]?.message) : errors[id]?.message}
</Typography>
)}
optional={
errors[id] && (
<Typography variant="caption" color="error">
{labelCanBeTranslated(errors[id]?.message)
? Tr(errors[id]?.message)
: errors[id]?.message}
</Typography>
)
}
>
<StepLabel
StepIconComponent={StepIconStyled}
@ -108,21 +118,21 @@ const CustomStepper = ({
</Step>
))}
</StepperStyled>
<Box marginY={2} textAlign='end'>
<Box marginY={2} textAlign="end">
<Button
data-cy='stepper-back-button'
data-cy="stepper-back-button"
disabled={disabledBack || isSubmitting}
onClick={handleBack}
size='small'
size="small"
>
<Translate word={T.Back} />
</Button>
<SubmitButton
color='secondary'
data-cy='stepper-next-button'
color="secondary"
data-cy="stepper-next-button"
isSubmitting={isSubmitting}
onClick={handleNext}
size='small'
size="small"
label={<Translate word={activeStep === lastStep ? T.Finish : T.Next} />}
/>
</Box>
@ -132,11 +142,8 @@ const CustomStepper = ({
CustomStepper.propTypes = {
steps: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]).isRequired,
label: PropTypes.string.isRequired
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
label: PropTypes.string.isRequired,
})
),
activeStep: PropTypes.number.isRequired,
@ -147,8 +154,8 @@ CustomStepper.propTypes = {
handleNext: PropTypes.func,
handleBack: PropTypes.func,
errors: PropTypes.shape({
message: PropTypes.string
})
message: PropTypes.string,
}),
}
CustomStepper.defaultProps = {
@ -160,7 +167,7 @@ CustomStepper.defaultProps = {
handleNext: () => undefined,
handleBack: () => undefined,
errors: undefined,
isSubmitting: false
isSubmitting: false,
}
CustomStepper.displayName = 'Stepper'

View File

@ -13,7 +13,13 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useState, useMemo, useCallback, useEffect, JSXElementConstructor } from 'react'
import {
useState,
useMemo,
useCallback,
useEffect,
JSXElementConstructor,
} from 'react'
import PropTypes from 'prop-types'
import { sprintf } from 'sprintf-js'
@ -42,8 +48,14 @@ const FIRST_STEP = 0
* @returns {JSXElementConstructor} Stepper form component
*/
const FormStepper = ({ steps = [], schema, onSubmit }) => {
const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs'))
const { control, watch, reset, formState: { errors }, setError } = useFormContext()
const isMobile = useMediaQuery((theme) => theme.breakpoints.only('xs'))
const {
control,
watch,
reset,
formState: { errors },
setError,
} = useFormContext()
const { isLoading } = useGeneral()
const [formData, setFormData] = useState(() => watch())
@ -57,12 +69,13 @@ const FormStepper = ({ steps = [], schema, onSubmit }) => {
reset({ ...formData }, { keepErrors: false })
}, [formData])
const validateSchema = async stepIdx => {
const validateSchema = async (stepIdx) => {
const { id, resolver, optionsValidate: options, ...step } = steps[stepIdx]
const stepData = watch(id)
const allData = { ...formData, [id]: stepData }
const stepSchema = typeof resolver === 'function' ? resolver(allData) : resolver
const stepSchema =
typeof resolver === 'function' ? resolver(allData) : resolver
await stepSchema.validate(stepData, { context: allData, ...options })
@ -74,7 +87,10 @@ const FormStepper = ({ steps = [], schema, onSubmit }) => {
const totalErrors = Object.keys(errorsByPath).length
totalErrors > 0
? setError(id, { type: 'manual', message: [T.ErrorsOcurred, totalErrors] })
? setError(id, {
type: 'manual',
message: [T.ErrorsOcurred, totalErrors],
})
: setError(id, rest)
inner?.forEach(({ path, type, errors: message }) => {
@ -88,7 +104,7 @@ const FormStepper = ({ steps = [], schema, onSubmit }) => {
})
}
const handleStep = stepToAdvance => {
const handleStep = (stepToAdvance) => {
const isBackAction = activeStep > stepToAdvance
isBackAction && handleBack(stepToAdvance)
@ -99,7 +115,8 @@ const FormStepper = ({ steps = [], schema, onSubmit }) => {
try {
const { id, data } = await validateSchema(stepIdx)
activeStep === stepIdx && setFormData(prev => ({ ...prev, [id]: data }))
activeStep === stepIdx &&
setFormData((prev) => ({ ...prev, [id]: data }))
stepIdx === stepsToValidate.length - 1 && setActiveStep(stepToAdvance)
} catch (validateError) {
@ -114,64 +131,76 @@ const FormStepper = ({ steps = [], schema, onSubmit }) => {
if (activeStep === lastStep) {
const submitData = { ...formData, [id]: data }
const schemaData = schema().cast(submitData, { context: submitData, isSubmit: true })
const schemaData = schema().cast(submitData, {
context: submitData,
isSubmit: true,
})
onSubmit(schemaData)
} else {
setFormData(prev => ({ ...prev, [id]: data }))
setActiveStep(prevActiveStep => prevActiveStep + 1)
setFormData((prev) => ({ ...prev, [id]: data }))
setActiveStep((prevActiveStep) => prevActiveStep + 1)
}
} catch (validateError) {
setErrors(validateError)
}
}
const handleBack = useCallback(stepToBack => {
if (activeStep < FIRST_STEP) return
const handleBack = useCallback(
(stepToBack) => {
if (activeStep < FIRST_STEP) return
const { id } = steps[activeStep]
const stepData = watch(id)
const { id } = steps[activeStep]
const stepData = watch(id)
setFormData(prev => ({ ...prev, [id]: stepData }))
setActiveStep(prevStep => Number.isInteger(stepToBack) ? stepToBack : (prevStep - 1))
}, [activeStep])
setFormData((prev) => ({ ...prev, [id]: stepData }))
setActiveStep((prevStep) =>
Number.isInteger(stepToBack) ? stepToBack : prevStep - 1
)
},
[activeStep]
)
const { id, content: Content } = useMemo(() => steps[activeStep], [
formData,
activeStep
])
const { id, content: Content } = useMemo(
() => steps[activeStep],
[formData, activeStep]
)
return (
<>
{/* STEPPER */}
{useMemo(() => isMobile ? (
<CustomMobileStepper
steps={steps}
totalSteps={totalSteps}
activeStep={activeStep}
lastStep={lastStep}
disabledBack={disabledBack}
isSubmitting={isLoading}
handleNext={handleNext}
handleBack={handleBack}
errors={errors}
/>
) : (
<CustomStepper
steps={steps}
activeStep={activeStep}
lastStep={lastStep}
disabledBack={disabledBack}
isSubmitting={isLoading}
handleStep={handleStep}
handleNext={handleNext}
handleBack={handleBack}
errors={errors}
/>
), [isLoading, isMobile, activeStep, errors[id]])}
{useMemo(
() =>
isMobile ? (
<CustomMobileStepper
steps={steps}
totalSteps={totalSteps}
activeStep={activeStep}
lastStep={lastStep}
disabledBack={disabledBack}
isSubmitting={isLoading}
handleNext={handleNext}
handleBack={handleBack}
errors={errors}
/>
) : (
<CustomStepper
steps={steps}
activeStep={activeStep}
lastStep={lastStep}
disabledBack={disabledBack}
isSubmitting={isLoading}
handleStep={handleStep}
handleNext={handleNext}
handleBack={handleBack}
errors={errors}
/>
),
[isLoading, isMobile, activeStep, errors[id]]
)}
{/* FORM CONTENT */}
{Content && <Content data={formData[id]} setFormData={setFormData} />}
{isDevelopment() && <DevTool control={control} placement='top-left' />}
{isDevelopment() && <DevTool control={control} placement="top-left" />}
</>
)
}
@ -182,25 +211,21 @@ FormStepper.propTypes = {
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
label: PropTypes.string.isRequired,
content: PropTypes.func.isRequired,
resolver: PropTypes.oneOfType([
PropTypes.func,
PropTypes.object
]).isRequired,
resolver: PropTypes.oneOfType([PropTypes.func, PropTypes.object])
.isRequired,
optionsValidate: PropTypes.shape({
strict: PropTypes.bool,
abortEarly: PropTypes.bool,
stripUnknown: PropTypes.bool,
recursive: PropTypes.bool,
context: PropTypes.object
})
context: PropTypes.object,
}),
})
).isRequired,
schema: PropTypes.func.isRequired,
onSubmit: PropTypes.func
onSubmit: PropTypes.func,
}
export {
SkeletonStepsForm
}
export { SkeletonStepsForm }
export default FormStepper

View File

@ -21,16 +21,19 @@ import { Grow, Menu, MenuItem, Typography, ListItemIcon } from '@mui/material'
import { NavArrowDown } from 'iconoir-react'
import { useDialog } from 'client/hooks'
import { DialogConfirmation, DialogForm, DialogPropTypes } from 'client/components/Dialogs'
import {
DialogConfirmation,
DialogForm,
DialogPropTypes,
} from 'client/components/Dialogs'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import SubmitButton, { SubmitButtonPropTypes } from 'client/components/FormControl/SubmitButton'
import SubmitButton, {
SubmitButtonPropTypes,
} from 'client/components/FormControl/SubmitButton'
import FormStepper from 'client/components/FormStepper'
import { Translate } from 'client/components/HOC'
const ButtonToTriggerForm = ({
buttonProps = {},
options = []
}) => {
const ButtonToTriggerForm = ({ buttonProps = {}, options = [] }) => {
const buttonId = buttonProps['data-cy'] ?? 'main-button'
const isGroupButton = options.length > 1
@ -38,12 +41,24 @@ const ButtonToTriggerForm = ({
const open = Boolean(anchorEl)
const { display, show, hide, values: Form } = useDialog()
const { onSubmit: handleSubmit, form, isConfirmDialog = false, dialogProps = {} } = Form ?? {}
const {
onSubmit: handleSubmit,
form,
isConfirmDialog = false,
dialogProps = {},
} = Form ?? {}
const formConfig = useMemo(() => form?.() ?? {}, [form])
const { steps, defaultValues, resolver, description, fields, transformBeforeSubmit } = formConfig
const {
steps,
defaultValues,
resolver,
description,
fields,
transformBeforeSubmit,
} = formConfig
const handleTriggerSubmit = async formData => {
const handleTriggerSubmit = async (formData) => {
try {
const data = transformBeforeSubmit?.(formData) ?? formData
await handleSubmit?.(data)
@ -52,12 +67,12 @@ const ButtonToTriggerForm = ({
}
}
const openDialogForm = formParams => {
const openDialogForm = (formParams) => {
show(formParams)
handleClose()
}
const handleToggle = evt => setAnchorEl(evt.currentTarget)
const handleToggle = (evt) => setAnchorEl(evt.currentTarget)
const handleClose = () => setAnchorEl(null)
return (
@ -70,9 +85,8 @@ const ButtonToTriggerForm = ({
aria-haspopup={isGroupButton ? 'true' : false}
disabled={!options.length}
endicon={isGroupButton ? <NavArrowDown /> : undefined}
onClick={evt => !isGroupButton
? openDialogForm(options[0])
: handleToggle(evt)
onClick={(evt) =>
!isGroupButton ? openDialogForm(options[0]) : handleToggle(evt)
}
{...buttonProps}
/>
@ -99,7 +113,7 @@ const ButtonToTriggerForm = ({
<Icon />
</ListItemIcon>
)}
<Typography variant='inherit' noWrap>
<Typography variant="inherit" noWrap>
<Translate word={name} />
</Typography>
</MenuItem>
@ -107,8 +121,8 @@ const ButtonToTriggerForm = ({
</Menu>
)}
{display && (
isConfirmDialog ? (
{display &&
(isConfirmDialog ? (
<DialogConfirmation
handleAccept={handleTriggerSubmit}
handleCancel={hide}
@ -130,12 +144,11 @@ const ButtonToTriggerForm = ({
) : (
<>
{description}
<FormWithSchema cy='form-dg' fields={fields} />
<FormWithSchema cy="form-dg" fields={fields} />
</>
)}
</DialogForm>
)
)}
))}
</>
)
}
@ -150,9 +163,9 @@ export const ButtonToTriggerFormPropTypes = {
name: PropTypes.string,
icon: PropTypes.any,
form: PropTypes.func,
onSubmit: PropTypes.func
onSubmit: PropTypes.func,
})
)
),
}
ButtonToTriggerForm.propTypes = ButtonToTriggerFormPropTypes

View File

@ -28,7 +28,7 @@ const NOT_DEPEND_ATTRIBUTES = [
'watcher',
'transform',
'getRowId',
'renderValue'
'renderValue',
]
const INPUT_CONTROLLER = {
@ -42,86 +42,97 @@ const INPUT_CONTROLLER = {
[INPUT_TYPES.FILE]: FC.FileController,
[INPUT_TYPES.TIME]: FC.TimeController,
[INPUT_TYPES.TABLE]: FC.TableController,
[INPUT_TYPES.TOGGLE]: FC.ToggleController
[INPUT_TYPES.TOGGLE]: FC.ToggleController,
}
const FormWithSchema = ({ id, cy, fields, rootProps, className, legend, legendTooltip }) => {
const FormWithSchema = ({
id,
cy,
fields,
rootProps,
className,
legend,
legendTooltip,
}) => {
const formContext = useFormContext()
const { control, watch } = formContext
const { sx: sxRoot, restOfRootProps } = rootProps ?? {}
const getFields = useMemo(() => typeof fields === 'function' ? fields() : fields, [])
const getFields = useMemo(
() => (typeof fields === 'function' ? fields() : fields),
[]
)
if (!getFields || getFields?.length === 0) return null
const addIdToName = name => name.startsWith('$')
? name.slice(1) // removes character '$' and returns
: id ? `${id}.${name}` : name // concat form ID if exists
const addIdToName = (name) =>
name.startsWith('$')
? name.slice(1) // removes character '$' and returns
: id
? `${id}.${name}`
: name // concat form ID if exists
return (
<FormControl
component='fieldset'
component="fieldset"
className={className}
sx={{ width: '100%', ...sxRoot }}
{...restOfRootProps}
>
{legend && (
<Legend title={legend} tooltip={legendTooltip} />
)}
<Grid container spacing={1} alignContent='flex-start'>
{getFields?.map?.(
({ dependOf, ...attributes }) => {
let valueOfDependField = null
let nameOfDependField = null
{legend && <Legend title={legend} tooltip={legendTooltip} />}
<Grid container spacing={1} alignContent="flex-start">
{getFields?.map?.(({ dependOf, ...attributes }) => {
let valueOfDependField = null
let nameOfDependField = null
if (dependOf) {
nameOfDependField = Array.isArray(dependOf)
? dependOf.map(addIdToName)
: addIdToName(dependOf)
if (dependOf) {
nameOfDependField = Array.isArray(dependOf)
? dependOf.map(addIdToName)
: addIdToName(dependOf)
valueOfDependField = watch(nameOfDependField)
}
const { name, type, htmlType, grid, ...fieldProps } = Object
.entries(attributes)
.reduce((field, attribute) => {
const [key, value] = attribute
const isNotDependAttribute = NOT_DEPEND_ATTRIBUTES.includes(key)
const finalValue = (
typeof value === 'function' &&
!isNotDependAttribute &&
!isValidElement(value())
) ? value(valueOfDependField, formContext) : value
return { ...field, [key]: finalValue }
}, {})
const dataCy = `${cy}-${name}`
const inputName = addIdToName(name)
const isHidden = htmlType === INPUT_TYPES.HIDDEN
if (isHidden) return null
return (
INPUT_CONTROLLER[type] && (
<Grid key={dataCy} item xs={12} md={6} {...grid}>
{createElement(INPUT_CONTROLLER[type], {
control,
cy: dataCy,
formContext,
dependencies: nameOfDependField,
name: inputName,
type: htmlType === false ? undefined : htmlType,
...fieldProps
})}
</Grid>
)
)
valueOfDependField = watch(nameOfDependField)
}
)}
const { name, type, htmlType, grid, ...fieldProps } = Object.entries(
attributes
).reduce((field, attribute) => {
const [key, value] = attribute
const isNotDependAttribute = NOT_DEPEND_ATTRIBUTES.includes(key)
const finalValue =
typeof value === 'function' &&
!isNotDependAttribute &&
!isValidElement(value())
? value(valueOfDependField, formContext)
: value
return { ...field, [key]: finalValue }
}, {})
const dataCy = `${cy}-${name}`
const inputName = addIdToName(name)
const isHidden = htmlType === INPUT_TYPES.HIDDEN
if (isHidden) return null
return (
INPUT_CONTROLLER[type] && (
<Grid key={dataCy} item xs={12} md={6} {...grid}>
{createElement(INPUT_CONTROLLER[type], {
control,
cy: dataCy,
formContext,
dependencies: nameOfDependField,
name: inputName,
type: htmlType === false ? undefined : htmlType,
...fieldProps,
})}
</Grid>
)
)
})}
</Grid>
</FormControl>
)
@ -132,12 +143,12 @@ FormWithSchema.propTypes = {
cy: PropTypes.string,
fields: PropTypes.oneOfType([
PropTypes.func,
PropTypes.arrayOf(PropTypes.object)
PropTypes.arrayOf(PropTypes.object),
]),
legend: PropTypes.string,
legendTooltip: PropTypes.string,
rootProps: PropTypes.object,
className: PropTypes.string
className: PropTypes.string,
}
export default FormWithSchema

View File

@ -20,33 +20,33 @@ import { styled, Typography } from '@mui/material'
import AdornmentWithTooltip from 'client/components/FormControl/Tooltip'
import { Tr, labelCanBeTranslated } from 'client/components/HOC'
const StyledLegend = styled(props => (
<Typography variant='subtitle1' component='legend' {...props} />
const StyledLegend = styled((props) => (
<Typography variant="subtitle1" component="legend" {...props} />
))(({ theme, tooltip }) => ({
marginBottom: '1em',
padding: '0em 1em 0.2em 0.5em',
borderBottom: `2px solid ${theme.palette.secondary.main}`,
...(!!tooltip && {
display: 'inline-flex',
alignItems: 'center'
})
alignItems: 'center',
}),
}))
const Legend = memo(({ title, tooltip }) => {
return (
<StyledLegend tooltip={tooltip}>
{labelCanBeTranslated(title) ? Tr(title) : title}
{!!tooltip && <AdornmentWithTooltip title={tooltip} />}
</StyledLegend>
)
}, (prev, next) =>
prev.title === next.title &&
prev.tooltip === next.tooltip
const Legend = memo(
({ title, tooltip }) => {
return (
<StyledLegend tooltip={tooltip}>
{labelCanBeTranslated(title) ? Tr(title) : title}
{!!tooltip && <AdornmentWithTooltip title={tooltip} />}
</StyledLegend>
)
},
(prev, next) => prev.title === next.title && prev.tooltip === next.tooltip
)
Legend.propTypes = {
title: PropTypes.string,
tooltip: PropTypes.string
tooltip: PropTypes.string,
}
Legend.displayName = 'FieldsetLegend'

View File

@ -16,7 +16,10 @@
import PropTypes from 'prop-types'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { FIELDS, SCHEMA } from 'client/components/Forms/MarketplaceApp/CreateForm/Steps/BasicConfiguration/schema'
import {
FIELDS,
SCHEMA,
} from 'client/components/Forms/MarketplaceApp/CreateForm/Steps/BasicConfiguration/schema'
import { Step } from 'client/utils'
import { T } from 'client/constants'
@ -42,12 +45,12 @@ const ConfigurationStep = () => ({
label: T.Configuration,
resolver: SCHEMA,
optionsValidate: { abortEarly: false },
content: Content
content: Content,
})
Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func
setFormData: PropTypes.func,
}
export default ConfigurationStep

View File

@ -17,19 +17,28 @@ import { string, boolean, object, ObjectSchema } from 'yup'
import { makeStyles } from '@mui/styles'
import { useSystem, useDatastore } from 'client/features/One'
import { ImagesTable, VmsTable, VmTemplatesTable } from 'client/components/Tables'
import { Field, arrayToOptions, getValidationFromFields, sentenceCase } from 'client/utils'
import {
ImagesTable,
VmsTable,
VmTemplatesTable,
} from 'client/components/Tables'
import {
Field,
arrayToOptions,
getValidationFromFields,
sentenceCase,
} from 'client/utils'
import { isMarketExportSupport } from 'client/models/Datastore'
import { T, INPUT_TYPES, STATES, RESOURCE_NAMES } from 'client/constants'
const TYPES = {
IMAGE: RESOURCE_NAMES.IMAGE.toUpperCase(),
VM: RESOURCE_NAMES.VM.toUpperCase(),
VM_TEMPLATE: RESOURCE_NAMES.VM_TEMPLATE.toUpperCase()
VM_TEMPLATE: RESOURCE_NAMES.VM_TEMPLATE.toUpperCase(),
}
const useTableStyles = makeStyles({
body: { gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr))' }
body: { gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr))' },
})
/** @type {Field} Type field */
@ -38,14 +47,14 @@ const TYPE = {
type: INPUT_TYPES.TOGGLE,
values: arrayToOptions(Object.values(TYPES), {
addEmpty: false,
getText: type => sentenceCase(type).toUpperCase()
getText: (type) => sentenceCase(type).toUpperCase(),
}),
validation: string()
.trim()
.required()
.uppercase()
.default(() => TYPES.IMAGE),
grid: { md: 12 }
grid: { md: 12 },
}
/** @type {Field} App name field */
@ -57,7 +66,7 @@ const NAME = {
.trim()
.required()
.default(() => undefined),
grid: { md: 12, lg: 6 }
grid: { md: 12, lg: 6 },
}
/** @type {Field} Import image/templates field */
@ -66,7 +75,7 @@ const IMPORT = {
label: T.DontAssociateApp,
type: INPUT_TYPES.SWITCH,
validation: boolean().default(() => false),
grid: { md: 12, lg: 6 }
grid: { md: 12, lg: 6 },
}
/** @type {Field} Resource table field */
@ -74,36 +83,40 @@ const RES_TABLE = {
name: 'id',
type: INPUT_TYPES.TABLE,
dependOf: 'type',
label: type => `Select the ${
sentenceCase(type) ?? 'resource'} to create the App`,
Table: type => ({
[TYPES.IMAGE]: ImagesTable,
[TYPES.VM]: VmsTable,
[TYPES.VM_TEMPLATE]: VmTemplatesTable
})[type],
label: (type) =>
`Select the ${sentenceCase(type) ?? 'resource'} to create the App`,
Table: (type) =>
({
[TYPES.IMAGE]: ImagesTable,
[TYPES.VM]: VmsTable,
[TYPES.VM_TEMPLATE]: VmTemplatesTable,
}[type]),
validation: string()
.trim()
.required()
.default(() => undefined),
grid: { md: 12 },
fieldProps: type => {
fieldProps: (type) => {
const { config: oneConfig } = useSystem()
const datastores = useDatastore()
const classes = useTableStyles()
return {
[TYPES.IMAGE]: {
filter: image => {
const datastore = datastores?.find(ds => ds?.ID === image?.DATASTORE_ID)
filter: (image) => {
const datastore = datastores?.find(
(ds) => ds?.ID === image?.DATASTORE_ID
)
return isMarketExportSupport(datastore, oneConfig)
}
},
},
[TYPES.VM]: {
initialState: { filters: [{ id: 'STATE', value: STATES.POWEROFF }] }
initialState: { filters: [{ id: 'STATE', value: STATES.POWEROFF }] },
},
[TYPES.VM_TEMPLATE]: { classes }
[TYPES.VM_TEMPLATE]: { classes },
}[type]
}
},
}
/** @type {Field[]} - List of fields */

View File

@ -29,7 +29,7 @@ const Content = ({ data }) => {
const { setValue } = useFormContext()
const { config: oneConfig } = useSystem()
const handleSelectedRows = rows => {
const handleSelectedRows = (rows) => {
const { original = {} } = rows?.[0] ?? {}
setValue(STEP_ID, original.ID !== undefined ? [original] : [])
@ -40,13 +40,15 @@ const Content = ({ data }) => {
singleSelect
onlyGlobalSearch
onlyGlobalSelectedRows
getRowId={market => String(market.NAME)}
filter={market =>
getRowId={(market) => String(market.NAME)}
filter={(market) =>
oneConfig?.FEDERATION?.ZONE_ID === market.ZONE_ID &&
oneConfig?.MARKET_MAD_CONF?.some(marketMad => (
marketMad?.APP_ACTIONS?.includes('create') &&
`${marketMad?.NAME}`.toUpperCase() === `${market?.MARKET_MAD}`.toUpperCase()
))
oneConfig?.MARKET_MAD_CONF?.some(
(marketMad) =>
marketMad?.APP_ACTIONS?.includes('create') &&
`${marketMad?.NAME}`.toUpperCase() ===
`${market?.MARKET_MAD}`.toUpperCase()
)
}
initialState={{ selectedRowIds: { [NAME]: true } }}
onSelectedRowsChange={handleSelectedRows}
@ -63,12 +65,12 @@ const MarketplaceStep = () => ({
id: STEP_ID,
label: T.SelectMarketplace,
resolver: SCHEMA,
content: Content
content: Content,
})
Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func
setFormData: PropTypes.func,
}
export default MarketplaceStep

View File

@ -13,22 +13,23 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import BasicConfiguration, { STEP_ID as BASIC_ID } from 'client/components/Forms/MarketplaceApp/CreateForm/Steps/BasicConfiguration'
import MarketplacesTable, { STEP_ID as MARKET_ID } from 'client/components/Forms/MarketplaceApp/CreateForm/Steps/MarketplacesTable'
import BasicConfiguration, {
STEP_ID as BASIC_ID,
} from 'client/components/Forms/MarketplaceApp/CreateForm/Steps/BasicConfiguration'
import MarketplacesTable, {
STEP_ID as MARKET_ID,
} from 'client/components/Forms/MarketplaceApp/CreateForm/Steps/MarketplacesTable'
import { createSteps } from 'client/utils'
const Steps = createSteps(
[BasicConfiguration, MarketplacesTable],
{
transformInitialValue: (initialValues, schema) => {
return schema.cast({ [BASIC_ID]: initialValues }, { stripUnknown: true })
},
transformBeforeSubmit: formData => {
const { [BASIC_ID]: configuration, [MARKET_ID]: [market] = [] } = formData
const Steps = createSteps([BasicConfiguration, MarketplacesTable], {
transformInitialValue: (initialValues, schema) => {
return schema.cast({ [BASIC_ID]: initialValues }, { stripUnknown: true })
},
transformBeforeSubmit: (formData) => {
const { [BASIC_ID]: configuration, [MARKET_ID]: [market] = [] } = formData
return { market: market?.ID, ...configuration }
}
}
)
return { market: market?.ID, ...configuration }
},
})
export default Steps

View File

@ -39,7 +39,7 @@ const CreateForm = ({ initialValues, onSubmit }) => {
const methods = useForm({
mode: 'onSubmit',
defaultValues,
resolver: yupResolver(resolver?.())
resolver: yupResolver(resolver?.()),
})
useEffect(() => {
@ -51,7 +51,7 @@ const CreateForm = ({ initialValues, onSubmit }) => {
<FormStepper
steps={steps}
schema={resolver}
onSubmit={data => onSubmit(transformBeforeSubmit?.(data) ?? data)}
onSubmit={(data) => onSubmit(transformBeforeSubmit?.(data) ?? data)}
/>
</FormProvider>
)
@ -59,7 +59,7 @@ const CreateForm = ({ initialValues, onSubmit }) => {
CreateForm.propTypes = {
initialValues: PropTypes.object,
onSubmit: PropTypes.func
onSubmit: PropTypes.func,
}
export default CreateForm

View File

@ -18,18 +18,17 @@ import PropTypes from 'prop-types'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { SCHEMA, FIELDS } from 'client/components/Forms/MarketplaceApp/ExportForm/Steps/BasicConfiguration/schema'
import {
SCHEMA,
FIELDS,
} from 'client/components/Forms/MarketplaceApp/ExportForm/Steps/BasicConfiguration/schema'
import { Step } from 'client/utils'
import { T } from 'client/constants'
export const STEP_ID = 'configuration'
const Content = () => (
<FormWithSchema
cy='export-app-configuration'
id={STEP_ID}
fields={FIELDS}
/>
<FormWithSchema cy="export-app-configuration" id={STEP_ID} fields={FIELDS} />
)
/**
@ -42,13 +41,13 @@ const ConfigurationStep = () => ({
label: T.Configuration,
resolver: SCHEMA,
optionsValidate: { abortEarly: false },
content: Content
content: Content,
})
Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
nics: PropTypes.array
nics: PropTypes.array,
}
export default ConfigurationStep

View File

@ -29,7 +29,7 @@ const NAME_FIELD = {
.trim()
.required()
.default(() => context.app.NAME)
})
}),
}
/** @type {Field} Template name field */
@ -43,7 +43,7 @@ const TEMPLATE_NAME_FIELD = {
.trim()
.required()
.default(() => context.app.NAME)
})
}),
}
/** @type {Field} Associate field */
@ -52,15 +52,11 @@ const ASSOCIATED_FIELD = {
label: T.DontAssociateApp,
type: INPUT_TYPES.SWITCH,
validation: boolean().yesOrNo(),
grid: { md: 12 }
grid: { md: 12 },
}
/** @type {Field[]} List of fields */
export const FIELDS = [
NAME_FIELD,
TEMPLATE_NAME_FIELD,
ASSOCIATED_FIELD
]
export const FIELDS = [NAME_FIELD, TEMPLATE_NAME_FIELD, ASSOCIATED_FIELD]
/** @type {ObjectSchema} Advanced options schema */
export const SCHEMA = object(getValidationFromFields(FIELDS))

View File

@ -30,10 +30,11 @@ const Content = ({ data, app }) => {
const isKernelType = useMemo(() => {
const appTemplate = String(decodeBase64(app?.TEMPLATE?.APPTEMPLATE64, ''))
return appTemplate.includes('TYPE="KERNEL"')
}, [])
const handleSelectedRows = rows => {
const handleSelectedRows = (rows) => {
const { original = {} } = rows?.[0] ?? {}
setValue(STEP_ID, original.ID !== undefined ? [original] : [])
@ -44,10 +45,10 @@ const Content = ({ data, app }) => {
singleSelect
onlyGlobalSearch
onlyGlobalSelectedRows
getRowId={row => String(row.NAME)}
getRowId={(row) => String(row.NAME)}
initialState={{
selectedRowIds: { [NAME]: true },
filters: [{ id: 'TYPE', value: isKernelType ? 'FILE' : 'IMAGE' }]
filters: [{ id: 'TYPE', value: isKernelType ? 'FILE' : 'IMAGE' }],
}}
onSelectedRowsChange={handleSelectedRows}
/>
@ -60,17 +61,17 @@ const Content = ({ data, app }) => {
* @param {object} app - Marketplace App resource
* @returns {Step} Datastore step
*/
const DatastoreStep = app => ({
const DatastoreStep = (app) => ({
id: STEP_ID,
label: T.SelectDatastore,
resolver: SCHEMA,
content: props => Content({ ...props, app })
content: (props) => Content({ ...props, app }),
})
Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
app: PropTypes.object
app: PropTypes.object,
}
export default DatastoreStep

View File

@ -13,26 +13,25 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import BasicConfiguration, { STEP_ID as BASIC_ID } from 'client/components/Forms/MarketplaceApp/ExportForm/Steps/BasicConfiguration'
import DatastoresTable, { STEP_ID as DATASTORE_ID } from 'client/components/Forms/MarketplaceApp/ExportForm/Steps/DatastoresTable'
import BasicConfiguration, {
STEP_ID as BASIC_ID,
} from 'client/components/Forms/MarketplaceApp/ExportForm/Steps/BasicConfiguration'
import DatastoresTable, {
STEP_ID as DATASTORE_ID,
} from 'client/components/Forms/MarketplaceApp/ExportForm/Steps/DatastoresTable'
import { createSteps } from 'client/utils'
const Steps = createSteps(
[BasicConfiguration, DatastoresTable],
{
transformInitialValue: (app, schema) => schema.cast({}, { context: { app } }),
transformBeforeSubmit: formData => {
const {
[BASIC_ID]: configuration,
[DATASTORE_ID]: [datastore] = []
} = formData
const Steps = createSteps([BasicConfiguration, DatastoresTable], {
transformInitialValue: (app, schema) => schema.cast({}, { context: { app } }),
transformBeforeSubmit: (formData) => {
const { [BASIC_ID]: configuration, [DATASTORE_ID]: [datastore] = [] } =
formData
return {
datastore: datastore?.ID,
...configuration
}
return {
datastore: datastore?.ID,
...configuration,
}
}
)
},
})
export default Steps

View File

@ -16,7 +16,4 @@
import CreateForm from 'client/components/Forms/MarketplaceApp/CreateForm'
import ExportForm from 'client/components/Forms/MarketplaceApp/ExportForm'
export {
CreateForm,
ExportForm
}
export { CreateForm, ExportForm }

View File

@ -17,7 +17,10 @@
import PropTypes from 'prop-types'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { FORM_FIELDS, STEP_FORM_SCHEMA } from 'client/components/Forms/Provider/CreateForm/Steps/BasicConfiguration/schema'
import {
FORM_FIELDS,
STEP_FORM_SCHEMA,
} from 'client/components/Forms/Provider/CreateForm/Steps/BasicConfiguration/schema'
import { T } from 'client/constants'
export const STEP_ID = 'configuration'
@ -25,7 +28,7 @@ export const STEP_ID = 'configuration'
const Content = ({ isUpdate }) => {
return (
<FormWithSchema
cy='form-provider'
cy="form-provider"
id={STEP_ID}
fields={FORM_FIELDS({ isUpdate })}
/>
@ -37,11 +40,11 @@ const BasicConfiguration = ({ isUpdate }) => ({
label: T.ProviderOverview,
resolver: () => STEP_FORM_SCHEMA({ isUpdate }),
optionsValidate: { abortEarly: false },
content: () => Content({ isUpdate })
content: () => Content({ isUpdate }),
})
Content.propTypes = {
isUpdate: PropTypes.bool
isUpdate: PropTypes.bool,
}
export * from 'client/components/Forms/Provider/CreateForm/Steps/BasicConfiguration/schema'

View File

@ -27,7 +27,7 @@ const NAME = {
.min(1, 'Name field is required')
.trim()
.required('Name field is required')
.default('')
.default(''),
}
const DESCRIPTION = {
@ -35,17 +35,11 @@ const DESCRIPTION = {
label: 'Description',
type: INPUT_TYPES.TEXT,
multiline: true,
validation: yup
.string()
.trim()
.default('')
validation: yup.string().trim().default(''),
}
export const FORM_FIELDS = ({ isUpdate }) => [
!isUpdate && NAME,
DESCRIPTION
].filter(Boolean)
export const FORM_FIELDS = ({ isUpdate }) =>
[!isUpdate && NAME, DESCRIPTION].filter(Boolean)
export const STEP_FORM_SCHEMA = ({ isUpdate }) => yup.object(
getValidationFromFields(FORM_FIELDS({ isUpdate }))
)
export const STEP_FORM_SCHEMA = ({ isUpdate }) =>
yup.object(getValidationFromFields(FORM_FIELDS({ isUpdate })))

View File

@ -26,7 +26,10 @@ import { getConnectionEditable } from 'client/models/ProviderTemplate'
import { sentenceCase } from 'client/utils'
import { T } from 'client/constants'
import { FORM_FIELDS, STEP_FORM_SCHEMA } from 'client/components/Forms/Provider/CreateForm/Steps/Connection/schema'
import {
FORM_FIELDS,
STEP_FORM_SCHEMA,
} from 'client/components/Forms/Provider/CreateForm/Steps/Connection/schema'
import { STEP_ID as TEMPLATE_ID } from 'client/components/Forms/Provider/CreateForm/Steps/Template'
export const STEP_ID = 'connection'
@ -42,27 +45,31 @@ const Content = ({ isUpdate }) => {
useEffect(() => {
const {
[TEMPLATE_ID]: templateSelected,
[STEP_ID]: currentConnection = {}
[STEP_ID]: currentConnection = {},
} = watch()
const template = templateSelected?.[0] ?? {}
fileCredentials = Boolean(providerConfig?.[template?.provider]?.file_credentials)
fileCredentials = Boolean(
providerConfig?.[template?.provider]?.file_credentials
)
connection = isUpdate
// when is updating, connections have the name as input label
? Object.keys(currentConnection)
.reduce((res, name) => ({ ...res, [name]: sentenceCase(name) }), {})
// set connections from template, to take values as input labels
: getConnectionEditable(template, providerConfig)
? // when is updating, connections have the name as input label
Object.keys(currentConnection).reduce(
(res, name) => ({ ...res, [name]: sentenceCase(name) }),
{}
)
: // set connections from template, to take values as input labels
getConnectionEditable(template, providerConfig)
setFields(FORM_FIELDS({ connection, fileCredentials }))
}, [])
return (fields?.length === 0) ? (
return fields?.length === 0 ? (
<EmptyCard title={"There aren't connections to fill"} />
) : (
<FormWithSchema cy='form-provider' fields={fields} id={STEP_ID} />
<FormWithSchema cy="form-provider" fields={fields} id={STEP_ID} />
)
}
@ -71,11 +78,11 @@ const Connection = ({ isUpdate }) => ({
label: T.ConfigureConnection,
resolver: () => STEP_FORM_SCHEMA({ connection, fileCredentials }),
optionsValidate: { abortEarly: false },
content: () => Content({ isUpdate })
content: () => Content({ isUpdate }),
})
Content.propTypes = {
isUpdate: PropTypes.bool
isUpdate: PropTypes.bool,
}
export * from 'client/components/Forms/Provider/CreateForm/Steps/Connection/schema'

View File

@ -24,13 +24,10 @@ const CREDENTIAL_INPUT = 'credentials'
export const FORM_FIELDS = ({ connection, fileCredentials }) =>
Object.entries(connection)?.map(([name, label]) => {
const isInputFile = fileCredentials && String(name).toLowerCase() === CREDENTIAL_INPUT
const isInputFile =
fileCredentials && String(name).toLowerCase() === CREDENTIAL_INPUT
let validation = yup
.string()
.trim()
.required()
.default(undefined)
let validation = yup.string().trim().required().default(undefined)
if (isInputFile) {
validation = validation.isBase64()
@ -43,21 +40,27 @@ export const FORM_FIELDS = ({ connection, fileCredentials }) =>
validation,
...(isInputFile && {
fieldProps: { accept: JSON_FORMAT },
validationBeforeTransform: [{
message: `Only the following formats are accepted: ${JSON_FORMAT}`,
test: value => value?.type !== JSON_FORMAT
}, {
message: `The file is too large. Max ${prettyBytes(MAX_SIZE_JSON, '')}`,
test: value => value?.size > MAX_SIZE_JSON
}],
transform: async file => {
validationBeforeTransform: [
{
message: `Only the following formats are accepted: ${JSON_FORMAT}`,
test: (value) => value?.type !== JSON_FORMAT,
},
{
message: `The file is too large. Max ${prettyBytes(
MAX_SIZE_JSON,
''
)}`,
test: (value) => value?.size > MAX_SIZE_JSON,
},
],
transform: async (file) => {
const json = await new Response(file ?? '{}').json()
return btoa(JSON.stringify(json))
}
})
},
}),
}
})
export const STEP_FORM_SCHEMA = props => yup.object(
getValidationFromFields(FORM_FIELDS(props))
)
export const STEP_FORM_SCHEMA = (props) =>
yup.object(getValidationFromFields(FORM_FIELDS(props)))

View File

@ -16,8 +16,14 @@
/* eslint-disable jsdoc/require-jsdoc */
import { useState, useEffect, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Divider, Select, Breadcrumbs, InputLabel, FormControl } from '@mui/material'
import { } from '@mui/material/Link'
import {
Divider,
Select,
Breadcrumbs,
InputLabel,
FormControl,
} from '@mui/material'
import {} from '@mui/material/Link'
import { NavArrowRight } from 'iconoir-react'
import Marked from 'marked'
@ -51,28 +57,43 @@ const Content = ({ data, setFormData }) => {
const { providerConfig } = useAuth()
const templateSelected = data?.[0]
const provisionTypes = useMemo(() => [
...new Set(
Object.values(providerConfig)
.map(provider => provider?.provision_type).flat()
)
], [])
const provisionTypes = useMemo(
() => [
...new Set(
Object.values(providerConfig)
.map((provider) => provider?.provision_type)
.flat()
),
],
[]
)
const [providerSelected, setProvider] = useState(() => templateSelected?.provider)
const [provisionSelected, setProvision] =
useState(() => templateSelected?.plain?.provision_type ?? provisionTypes[0])
const [providerSelected, setProvider] = useState(
() => templateSelected?.provider
)
const [provisionSelected, setProvision] = useState(
() => templateSelected?.plain?.provision_type ?? provisionTypes[0]
)
const [templatesByProvisionSelected, providerTypes, description] = useMemo(() => {
const templates = Object.values(provisionTemplates[provisionSelected]?.providers).flat()
const types = [...new Set(templates.map(({ provider }) => provider))]
const provisionDescription = provisionTemplates?.[provisionSelected]?.description
const [templatesByProvisionSelected, providerTypes, description] =
useMemo(() => {
const templates = Object.values(
provisionTemplates[provisionSelected]?.providers
).flat()
const types = [...new Set(templates.map(({ provider }) => provider))]
const provisionDescription =
provisionTemplates?.[provisionSelected]?.description
return [templates, types, provisionDescription]
}, [provisionSelected])
return [templates, types, provisionDescription]
}, [provisionSelected])
const templatesAvailable = useMemo(() => (
templatesByProvisionSelected.filter(({ provider }) => providerSelected === provider)
), [providerSelected])
const templatesAvailable = useMemo(
() =>
templatesByProvisionSelected.filter(
({ provider }) => providerSelected === provider
),
[providerSelected]
)
useEffect(() => {
// set first provision type
@ -88,16 +109,16 @@ const Content = ({ data, setFormData }) => {
key: STEP_ID,
list: data,
setList: setFormData,
getItemId: item => item.name
getItemId: (item) => item.name,
})
const handleChangeProvision = evt => {
const handleChangeProvision = (evt) => {
setProvision(evt.target.value)
setProvider(undefined)
templateSelected && handleClear()
}
const handleChangeProvider = evt => {
const handleChangeProvider = (evt) => {
setProvider(evt.target.value)
templateSelected && handleClear()
}
@ -109,7 +130,7 @@ const Content = ({ data, setFormData }) => {
// reset rest of form when change template
setFormData({
[CONFIGURATION_ID]: { name, description },
[CONNECTION_ID]: {}
[CONNECTION_ID]: {},
})
isSelected
@ -119,6 +140,7 @@ const Content = ({ data, setFormData }) => {
const RenderDescription = ({ description = '' }) => {
const html = Marked(sanitize`${description}`, { renderer })
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
@ -127,20 +149,20 @@ const Content = ({ data, setFormData }) => {
{/* -- SELECTORS -- */}
<Breadcrumbs separator={<NavArrowRight />}>
<FormControl>
<InputLabel color='secondary' shrink id='select-provision-type-label'>
<InputLabel color="secondary" shrink id="select-provision-type-label">
{'Provision type'}
</InputLabel>
<Select
color='secondary'
color="secondary"
inputProps={{ 'data-cy': 'select-provision-type' }}
labelId='select-provision-type-label'
labelId="select-provision-type-label"
native
style={{ marginTop: '1em', minWidth: '8em' }}
onChange={handleChangeProvision}
value={provisionSelected}
variant='outlined'
variant="outlined"
>
{provisionTypes.map(type => (
{provisionTypes.map((type) => (
<option key={type} value={type}>
{type}
</option>
@ -148,20 +170,20 @@ const Content = ({ data, setFormData }) => {
</Select>
</FormControl>
<FormControl>
<InputLabel color='secondary' shrink id='select-provider-type-label'>
<InputLabel color="secondary" shrink id="select-provider-type-label">
{'Provider type'}
</InputLabel>
<Select
color='secondary'
color="secondary"
inputProps={{ 'data-cy': 'select-provider-type' }}
labelId='select-provider-type-label'
labelId="select-provider-type-label"
native
style={{ marginTop: '1em', minWidth: '8em' }}
onChange={handleChangeProvider}
value={providerSelected}
variant='outlined'
variant="outlined"
>
{providerTypes.map(type => (
{providerTypes.map((type) => (
<option key={type} value={type}>
{providerConfig[type]?.name ?? type}
</option>
@ -171,18 +193,23 @@ const Content = ({ data, setFormData }) => {
</Breadcrumbs>
{/* -- DESCRIPTION -- */}
{useMemo(() => description && <RenderDescription description={description} />, [description])}
{useMemo(
() => description && <RenderDescription description={description} />,
[description]
)}
<Divider style={{ margin: '1rem 0' }} />
{/* -- LIST -- */}
<ListCards
keyProp='name'
keyProp="name"
list={templatesAvailable}
gridProps={{ 'data-cy': 'providers-templates' }}
CardComponent={ProvisionTemplateCard}
cardsProps={({ value = {} }) => {
const isSelected = data?.some(selected => selected.name === value.name)
const isSelected = data?.some(
(selected) => selected.name === value.name
)
const isValid = isValidProviderTemplate(value, providerConfig)
const image = providerConfig?.[value.provider]?.image
@ -191,7 +218,7 @@ const Content = ({ data, setFormData }) => {
isProvider: true,
isSelected,
isValid,
handleClick: () => handleClick(value, isSelected)
handleClick: () => handleClick(value, isSelected),
}
}}
/>
@ -203,12 +230,12 @@ const Template = () => ({
id: STEP_ID,
label: T.ProviderTemplate,
resolver: () => STEP_FORM_SCHEMA,
content: Content
content: Content,
})
Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func
setFormData: PropTypes.func,
}
export * from 'client/components/Forms/Provider/CreateForm/Steps/Template/schema'

View File

@ -13,47 +13,64 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import Template, { STEP_ID as TEMPLATE_ID } from 'client/components/Forms/Provider/CreateForm/Steps/Template'
import BasicConfiguration, { STEP_ID as BASIC_ID } from 'client/components/Forms/Provider/CreateForm/Steps/BasicConfiguration'
import Connection, { STEP_ID as CONNECTION_ID } from 'client/components/Forms/Provider/CreateForm/Steps/Connection'
import { getConnectionEditable, getConnectionFixed } from 'client/models/ProviderTemplate'
import Template, {
STEP_ID as TEMPLATE_ID,
} from 'client/components/Forms/Provider/CreateForm/Steps/Template'
import BasicConfiguration, {
STEP_ID as BASIC_ID,
} from 'client/components/Forms/Provider/CreateForm/Steps/BasicConfiguration'
import Connection, {
STEP_ID as CONNECTION_ID,
} from 'client/components/Forms/Provider/CreateForm/Steps/Connection'
import {
getConnectionEditable,
getConnectionFixed,
} from 'client/models/ProviderTemplate'
import { createSteps, deepmerge } from 'client/utils'
const Steps = createSteps(stepProps => {
const { isUpdate } = stepProps
const Steps = createSteps(
(stepProps) => {
const { isUpdate } = stepProps
return [
!isUpdate && Template,
BasicConfiguration,
Connection
].filter(Boolean)
}, {
transformInitialValue: ({ provider, connection, providerConfig } = {}) => {
const { description, ...currentBodyTemplate } = provider?.TEMPLATE?.PROVISION_BODY ?? {}
// overwrite decrypted connection
const fakeProviderTemplate = { ...currentBodyTemplate, connection }
const connectionEditable = getConnectionEditable(fakeProviderTemplate, providerConfig)
return {
[TEMPLATE_ID]: [fakeProviderTemplate],
[CONNECTION_ID]: connectionEditable,
[BASIC_ID]: { description }
}
return [!isUpdate && Template, BasicConfiguration, Connection].filter(
Boolean
)
},
transformBeforeSubmit: (formData, providerConfig) => {
const {
[TEMPLATE_ID]: [templateSelected] = [],
[CONNECTION_ID]: connection = {},
[BASIC_ID]: configuration = {}
} = formData ?? {}
{
transformInitialValue: ({ provider, connection, providerConfig } = {}) => {
const { description, ...currentBodyTemplate } =
provider?.TEMPLATE?.PROVISION_BODY ?? {}
const connectionFixed = getConnectionFixed(templateSelected, providerConfig)
const allConnections = { ...connection, ...connectionFixed }
const editedData = { ...configuration, connection: allConnections }
// overwrite decrypted connection
const fakeProviderTemplate = { ...currentBodyTemplate, connection }
const connectionEditable = getConnectionEditable(
fakeProviderTemplate,
providerConfig
)
return deepmerge(templateSelected, editedData)
return {
[TEMPLATE_ID]: [fakeProviderTemplate],
[CONNECTION_ID]: connectionEditable,
[BASIC_ID]: { description },
}
},
transformBeforeSubmit: (formData, providerConfig) => {
const {
[TEMPLATE_ID]: [templateSelected] = [],
[CONNECTION_ID]: connection = {},
[BASIC_ID]: configuration = {},
} = formData ?? {}
const connectionFixed = getConnectionFixed(
templateSelected,
providerConfig
)
const allConnections = { ...connection, ...connectionFixed }
const editedData = { ...configuration, connection: allConnections }
return deepmerge(templateSelected, editedData)
},
}
})
)
export default Steps

View File

@ -38,7 +38,7 @@ const CreateForm = ({ provider, providerConfig, connection, onSubmit }) => {
const methods = useForm({
mode: 'onSubmit',
defaultValues,
resolver: yupResolver(resolver())
resolver: yupResolver(resolver()),
})
return (
@ -46,7 +46,7 @@ const CreateForm = ({ provider, providerConfig, connection, onSubmit }) => {
<FormStepper
steps={steps}
schema={resolver}
onSubmit={data =>
onSubmit={(data) =>
onSubmit(transformBeforeSubmit?.(data, providerConfig) ?? data)
}
/>
@ -61,36 +61,39 @@ const PreFetchingForm = ({ providerId, onSubmit }) => {
const [provider, connection] = data ?? []
useEffect(() => {
providerId && fetchRequestAll([
getProvider(providerId),
getProviderConnection(providerId)
])
providerId &&
fetchRequestAll([
getProvider(providerId),
getProviderConnection(providerId),
])
}, [])
if (error) {
return <Redirect to={PATH.PROVIDERS.LIST} />
}
return (providerId && !data)
? <SkeletonStepsForm />
: <CreateForm
return providerId && !data ? (
<SkeletonStepsForm />
) : (
<CreateForm
provider={provider}
providerConfig={providerConfig}
connection={connection}
onSubmit={onSubmit}
/>
)
}
PreFetchingForm.propTypes = {
providerId: PropTypes.string,
onSubmit: PropTypes.func
onSubmit: PropTypes.func,
}
CreateForm.propTypes = {
provider: PropTypes.object,
connection: PropTypes.object,
providerConfig: PropTypes.object,
onSubmit: PropTypes.func
onSubmit: PropTypes.func,
}
export default PreFetchingForm

View File

@ -15,6 +15,4 @@
* ------------------------------------------------------------------------- */
import CreateForm from 'client/components/Forms/Provider/CreateForm'
export {
CreateForm
}
export { CreateForm }

View File

@ -20,7 +20,8 @@ import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { T } from 'client/constants'
import {
FORM_FIELDS, STEP_FORM_SCHEMA
FORM_FIELDS,
STEP_FORM_SCHEMA,
} from 'client/components/Forms/Provision/CreateForm/Steps/BasicConfiguration/schema'
export const STEP_ID = 'configuration'
@ -31,9 +32,11 @@ const BasicConfiguration = () => ({
resolver: () => STEP_FORM_SCHEMA,
optionsValidate: { abortEarly: false },
content: useCallback(
() => <FormWithSchema cy="form-provision" fields={FORM_FIELDS} id={STEP_ID} />,
() => (
<FormWithSchema cy="form-provision" fields={FORM_FIELDS} id={STEP_ID} />
),
[]
)
),
})
export default BasicConfiguration

View File

@ -26,7 +26,7 @@ const NAME = {
.min(1, 'Name field is required')
.trim()
.required('Name field is required')
.default('')
.default(''),
}
const DESCRIPTION = {
@ -34,14 +34,9 @@ const DESCRIPTION = {
label: 'Description',
type: INPUT_TYPES.TEXT,
multiline: true,
validation: yup
.string()
.trim()
.default('')
validation: yup.string().trim().default(''),
}
export const FORM_FIELDS = [NAME, DESCRIPTION]
export const STEP_FORM_SCHEMA = yup.object(
getValidationFromFields(FORM_FIELDS)
)
export const STEP_FORM_SCHEMA = yup.object(getValidationFromFields(FORM_FIELDS))

View File

@ -30,7 +30,8 @@ import { deepmerge } from 'client/utils'
import { STEP_ID as PROVIDER_ID } from 'client/components/Forms/Provision/CreateForm/Steps/Provider'
import { STEP_ID as TEMPLATE_ID } from 'client/components/Forms/Provision/CreateForm/Steps/Template'
import {
FORM_FIELDS, STEP_FORM_SCHEMA
FORM_FIELDS,
STEP_FORM_SCHEMA,
} from 'client/components/Forms/Provision/CreateForm/Steps/Inputs/schema'
export const STEP_ID = 'inputs'
@ -50,7 +51,8 @@ const Inputs = () => ({
const { watch, reset } = useFormContext()
useEffect(() => {
const { [PROVIDER_ID]: providerSelected = [], [STEP_ID]: currentInputs } = watch()
const { [PROVIDER_ID]: providerSelected = [], [STEP_ID]: currentInputs } =
watch()
if (!currentInputs) {
changeLoading(true) // disable finish button until provider is fetched
@ -68,10 +70,11 @@ const Inputs = () => ({
const templateInputs = provisionTemplateSelected?.[0]?.inputs ?? []
// MERGE INPUTS provision template + PROVISION_BODY.inputs (provider fetch)
inputs = templateInputs.map(templateInput => {
const providerInput = PROVISION_BODY.inputs?.find(
providerInput => providerInput.name === templateInput.name
) ?? {}
inputs = templateInputs.map((templateInput) => {
const providerInput =
PROVISION_BODY.inputs?.find(
(providerInput) => providerInput.name === templateInput.name
) ?? {}
return deepmerge(templateInput, providerInput)
})
@ -82,15 +85,15 @@ const Inputs = () => ({
}, [fetchData])
if (!fields) {
return <LinearProgress color='secondary' />
return <LinearProgress color="secondary" />
}
return (fields?.length === 0) ? (
return fields?.length === 0 ? (
<EmptyCard title={'✔️ There is not inputs to fill'} />
) : (
<FormWithSchema cy="form-provision" fields={fields} id={STEP_ID} />
)
}, [])
}, []),
})
export default Inputs

View File

@ -17,31 +17,32 @@
import * as yup from 'yup'
import { getValidationFromFields, schemaUserInput } from 'client/utils'
export const FORM_FIELDS = inputs =>
inputs?.map(({
name,
description,
type,
default: defaultValue,
min_value: min,
max_value: max,
options
}) => {
const optionsValue = options ?? `${min}..${max}`
return {
export const FORM_FIELDS = (inputs) =>
inputs?.map(
({
name,
label: `${description ?? name} *`,
...schemaUserInput({
mandatory: true,
name,
type,
options: optionsValue,
default: defaultValue
})
}
})
description,
type,
default: defaultValue,
min_value: min,
max_value: max,
options,
}) => {
const optionsValue = options ?? `${min}..${max}`
export const STEP_FORM_SCHEMA = inputs => yup.object(
getValidationFromFields(FORM_FIELDS(inputs))
)
return {
name,
label: `${description ?? name} *`,
...schemaUserInput({
mandatory: true,
name,
type,
options: optionsValue,
default: defaultValue,
}),
}
}
)
export const STEP_FORM_SCHEMA = (inputs) =>
yup.object(getValidationFromFields(FORM_FIELDS(inputs)))

View File

@ -40,24 +40,29 @@ const Provider = () => ({
const provisionTemplateSelected = useWatch({ name: TEMPLATE_ID })?.[0] ?? {}
const providersAvailable = useMemo(() =>
providers.filter(provider => {
const { TEMPLATE: { PLAIN = {} } } = provider ?? {}
const providersAvailable = useMemo(
() =>
providers.filter((provider) => {
const {
TEMPLATE: { PLAIN = {} },
} = provider ?? {}
return (
PLAIN.provider === provisionTemplateSelected.provider &&
PLAIN.provision_type === provisionTemplateSelected.provision_type
)
}), [])
return (
PLAIN.provider === provisionTemplateSelected.provider &&
PLAIN.provision_type === provisionTemplateSelected.provision_type
)
}),
[]
)
const {
handleSelect,
handleClear
} = useListForm({ key: STEP_ID, setList: setFormData })
const { handleSelect, handleClear } = useListForm({
key: STEP_ID,
setList: setFormData,
})
const handleClick = (provider, isSelected) => {
// reset inputs when selected provider changes
setFormData(prev => ({ ...prev, [INPUTS_ID]: undefined }))
setFormData((prev) => ({ ...prev, [INPUTS_ID]: undefined }))
isSelected ? handleClear() : handleSelect(provider)
}
@ -70,19 +75,19 @@ const Provider = () => ({
gridProps={{ 'data-cy': 'providers' }}
cardsProps={({ value = {} }) => {
const { ID, TEMPLATE } = value
const isSelected = data?.some(selected => selected.ID === ID)
const isSelected = data?.some((selected) => selected.ID === ID)
const image = providerConfig?.[TEMPLATE?.PLAIN?.provider]?.image
return {
image,
isProvider: true,
isSelected,
handleClick: () => handleClick(value, isSelected)
handleClick: () => handleClick(value, isSelected),
}
}}
/>
)
}, [])
}, []),
})
export default Provider

View File

@ -15,7 +15,13 @@
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useState, useCallback, useEffect, useMemo } from 'react'
import { Divider, Select, Breadcrumbs, InputLabel, FormControl } from '@mui/material'
import {
Divider,
Select,
Breadcrumbs,
InputLabel,
FormControl,
} from '@mui/material'
import { NavArrowRight } from 'iconoir-react'
import Marked from 'marked'
@ -54,28 +60,43 @@ const Template = () => ({
const { providerConfig } = useAuth()
const templateSelected = data?.[0]
const provisionTypes = useMemo(() => [
...new Set(
Object.values(providerConfig)
.map(provider => provider?.provision_type).flat()
)
], [])
const provisionTypes = useMemo(
() => [
...new Set(
Object.values(providerConfig)
.map((provider) => provider?.provision_type)
.flat()
),
],
[]
)
const [providerSelected, setProvider] = useState(() => templateSelected?.provider)
const [provisionSelected, setProvision] =
useState(() => templateSelected?.provision_type ?? provisionTypes[0])
const [providerSelected, setProvider] = useState(
() => templateSelected?.provider
)
const [provisionSelected, setProvision] = useState(
() => templateSelected?.provision_type ?? provisionTypes[0]
)
const [templatesByProvisionSelected, providerTypes, description] = useMemo(() => {
const templates = Object.values(provisionTemplates[provisionSelected]?.provisions).flat()
const types = [...new Set(templates.map(({ provider }) => provider))]
const provisionDescription = provisionTemplates?.[provisionSelected]?.description
const [templatesByProvisionSelected, providerTypes, description] =
useMemo(() => {
const templates = Object.values(
provisionTemplates[provisionSelected]?.provisions
).flat()
const types = [...new Set(templates.map(({ provider }) => provider))]
const provisionDescription =
provisionTemplates?.[provisionSelected]?.description
return [templates, types, provisionDescription]
}, [provisionSelected])
return [templates, types, provisionDescription]
}, [provisionSelected])
const templatesAvailable = useMemo(() => (
templatesByProvisionSelected.filter(({ provider }) => providerSelected === provider)
), [providerSelected])
const templatesAvailable = useMemo(
() =>
templatesByProvisionSelected.filter(
({ provider }) => providerSelected === provider
),
[providerSelected]
)
useEffect(() => {
// set first provision type
@ -87,19 +108,18 @@ const Template = () => ({
provisionSelected && !providerSelected && setProvider(providerTypes[0])
}, [provisionSelected])
const {
handleSelect,
handleUnselect,
handleClear
} = useListForm({ key: STEP_ID, setList: setFormData })
const { handleSelect, handleUnselect, handleClear } = useListForm({
key: STEP_ID,
setList: setFormData,
})
const handleChangeProvision = evt => {
const handleChangeProvision = (evt) => {
setProvision(evt.target.value)
setProvider(undefined)
templateSelected && handleClear()
}
const handleChangeProvider = evt => {
const handleChangeProvider = (evt) => {
setProvider(evt.target.value)
templateSelected && handleClear()
}
@ -108,22 +128,26 @@ const Template = () => ({
const { name, description, defaults, hosts } = template
// reset rest of form when change template
const providerName = defaults?.provision?.provider_name ?? hosts?.[0]?.provision.provider_name
const providerFromProvisionTemplate = providers?.filter(({ NAME }) => NAME === providerName) ?? []
const providerName =
defaults?.provision?.provider_name ??
hosts?.[0]?.provision.provider_name
const providerFromProvisionTemplate =
providers?.filter(({ NAME }) => NAME === providerName) ?? []
setFormData({
[PROVIDER_ID]: providerFromProvisionTemplate,
[CONFIGURATION_ID]: { name, description },
[INPUTS_ID]: undefined
[INPUTS_ID]: undefined,
})
isSelected
? handleUnselect(name, item => item.name === name)
? handleUnselect(name, (item) => item.name === name)
: handleSelect({ ...template, provision_type: provisionSelected })
}
const RenderDescription = ({ description = '' }) => {
const html = Marked(sanitize`${description}`, { renderer })
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
@ -132,20 +156,24 @@ const Template = () => ({
{/* -- SELECTORS -- */}
<Breadcrumbs separator={<NavArrowRight />}>
<FormControl>
<InputLabel color='secondary' shrink id='select-provision-type-label'>
<InputLabel
color="secondary"
shrink
id="select-provision-type-label"
>
{'Provision type'}
</InputLabel>
<Select
color='secondary'
color="secondary"
inputProps={{ 'data-cy': 'select-provision-type' }}
labelId='select-provision-type-label'
labelId="select-provision-type-label"
native
style={{ marginTop: '1em', minWidth: '8em' }}
onChange={handleChangeProvision}
value={provisionSelected}
variant='outlined'
variant="outlined"
>
{provisionTypes.map(type => (
{provisionTypes.map((type) => (
<option key={type} value={type}>
{type}
</option>
@ -153,20 +181,24 @@ const Template = () => ({
</Select>
</FormControl>
<FormControl>
<InputLabel color='secondary' shrink id='select-provider-type-label'>
<InputLabel
color="secondary"
shrink
id="select-provider-type-label"
>
{'Provider type'}
</InputLabel>
<Select
color='secondary'
color="secondary"
inputProps={{ 'data-cy': 'select-provider-type' }}
labelId='select-provider-type-label'
labelId="select-provider-type-label"
native
style={{ marginTop: '1em', minWidth: '8em' }}
onChange={handleChangeProvider}
value={providerSelected}
variant='outlined'
variant="outlined"
>
{providerTypes.map(type => (
{providerTypes.map((type) => (
<option key={type} value={type}>
{providerConfig[type]?.name ?? type}
</option>
@ -176,31 +208,36 @@ const Template = () => ({
</Breadcrumbs>
{/* -- DESCRIPTION -- */}
{useMemo(() => description && <RenderDescription description={description} />, [description])}
{useMemo(
() => description && <RenderDescription description={description} />,
[description]
)}
<Divider style={{ margin: '1rem 0' }} />
{/* -- LIST -- */}
<ListCards
keyProp='name'
keyProp="name"
list={templatesAvailable}
gridProps={{ 'data-cy': 'provisions-templates' }}
CardComponent={ProvisionTemplateCard}
cardsProps={({ value = {} }) => {
const isSelected = data?.some(selected => selected.name === value.name)
const isSelected = data?.some(
(selected) => selected.name === value.name
)
const isValid = isValidProvisionTemplate(value)
return {
image: value?.image,
isSelected,
isValid,
handleClick: () => handleClick(value, isSelected)
handleClick: () => handleClick(value, isSelected),
}
}}
/>
</>
)
}, [])
}, []),
})
export default Template

View File

@ -20,37 +20,40 @@ import BasicConfiguration from 'client/components/Forms/Provision/CreateForm/Ste
import Inputs from 'client/components/Forms/Provision/CreateForm/Steps/Inputs'
import { set, createSteps, cloneObject } from 'client/utils'
const Steps = createSteps(
[Template, Provider, BasicConfiguration, Inputs],
{
transformBeforeSubmit: formData => {
const { template, provider, configuration, inputs } = formData
const { name, description } = configuration
const providerName = provider?.[0]?.NAME
const Steps = createSteps([Template, Provider, BasicConfiguration, Inputs], {
transformBeforeSubmit: (formData) => {
const { template, provider, configuration, inputs } = formData
const { name, description } = configuration
const providerName = provider?.[0]?.NAME
// clone object from redux store
const provisionTemplateSelected = cloneObject(template?.[0] ?? {})
// clone object from redux store
const provisionTemplateSelected = cloneObject(template?.[0] ?? {})
// update provider name if changed during form
if (provisionTemplateSelected.defaults?.provision?.provider_name) {
set(provisionTemplateSelected, 'defaults.provision.provider_name', providerName)
} else if (provisionTemplateSelected.hosts?.length > 0) {
provisionTemplateSelected.hosts.forEach(host => {
set(host, 'provision.provider_name', providerName)
})
}
const resolvedInputs = provisionTemplateSelected?.inputs
?.map(input => ({ ...input, value: `${inputs[input?.name]}` }))
return {
...provisionTemplateSelected,
name,
description,
inputs: resolvedInputs
}
// update provider name if changed during form
if (provisionTemplateSelected.defaults?.provision?.provider_name) {
set(
provisionTemplateSelected,
'defaults.provision.provider_name',
providerName
)
} else if (provisionTemplateSelected.hosts?.length > 0) {
provisionTemplateSelected.hosts.forEach((host) => {
set(host, 'provision.provider_name', providerName)
})
}
}
)
const resolvedInputs = provisionTemplateSelected?.inputs?.map((input) => ({
...input,
value: `${inputs[input?.name]}`,
}))
return {
...provisionTemplateSelected,
name,
description,
inputs: resolvedInputs,
}
},
})
export default Steps

View File

@ -28,7 +28,7 @@ const CreateForm = ({ onSubmit }) => {
const methods = useForm({
mode: 'onSubmit',
defaultValues,
resolver: yupResolver(resolver())
resolver: yupResolver(resolver()),
})
return (
@ -36,7 +36,7 @@ const CreateForm = ({ onSubmit }) => {
<FormStepper
steps={steps}
schema={resolver}
onSubmit={data => onSubmit(transformBeforeSubmit?.(data) ?? data)}
onSubmit={(data) => onSubmit(transformBeforeSubmit?.(data) ?? data)}
/>
</FormProvider>
)
@ -44,7 +44,7 @@ const CreateForm = ({ onSubmit }) => {
CreateForm.propTypes = {
initialValues: PropTypes.object,
onSubmit: PropTypes.func
onSubmit: PropTypes.func,
}
export default CreateForm

View File

@ -14,7 +14,10 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { createForm } from 'client/utils'
import { SCHEMA, FIELDS } from 'client/components/Forms/Provision/DeleteForm/schema'
import {
SCHEMA,
FIELDS,
} from 'client/components/Forms/Provision/DeleteForm/schema'
const DeleteForm = createForm(SCHEMA, FIELDS)

View File

@ -25,11 +25,12 @@ const CLEANUP = {
tooltip: `
Force to terminate VMs running on provisioned Hosts
and delete all images in the datastores.`,
validation: yup.boolean().notRequired().default(() => false)
validation: yup
.boolean()
.notRequired()
.default(() => false),
}
export const FIELDS = [
CLEANUP
]
export const FIELDS = [CLEANUP]
export const SCHEMA = yup.object(getValidationFromFields(FIELDS))

View File

@ -16,7 +16,4 @@
import CreateForm from 'client/components/Forms/Provision/CreateForm'
import DeleteForm from 'client/components/Forms/Provision/DeleteForm'
export {
CreateForm,
DeleteForm
}
export { CreateForm, DeleteForm }

View File

@ -26,11 +26,7 @@ export const TARGET = {
Device to map image disk.
If set, it will overwrite the default device mapping.`,
type: INPUT_TYPES.TEXT,
validation: yup
.string()
.trim()
.notRequired()
.default(undefined)
validation: yup.string().trim().notRequired().default(undefined),
}
export const READONLY = {
@ -40,13 +36,13 @@ export const READONLY = {
type: INPUT_TYPES.SELECT,
values: [
{ text: T.Yes, value: 'YES' },
{ text: T.No, value: 'NO' }
{ text: T.No, value: 'NO' },
],
validation: yup
.string()
.trim()
.notRequired()
.default(() => 'NO')
.default(() => 'NO'),
}
export const DEV_PREFIX = {
@ -59,13 +55,9 @@ export const DEV_PREFIX = {
{ text: 'Virtio', value: 'vd' },
{ text: 'CSI/SATA', value: 'sd' },
{ text: 'Parallel ATA (IDE)', value: 'hd' },
{ text: 'Custom', value: 'custom' }
{ text: 'Custom', value: 'custom' },
],
validation: yup
.string()
.trim()
.notRequired()
.default(undefined)
validation: yup.string().trim().notRequired().default(undefined),
}
export const VCENTER_ADAPTER_TYPE = {
@ -78,13 +70,9 @@ export const VCENTER_ADAPTER_TYPE = {
{ text: 'lsiLogic', value: 'lsiLogic' },
{ text: 'ide', value: 'ide' },
{ text: 'busLogic', value: 'busLogic' },
{ text: 'Custom', value: 'custom' }
{ text: 'Custom', value: 'custom' },
],
validation: yup
.string()
.trim()
.notRequired()
.default(undefined)
validation: yup.string().trim().notRequired().default(undefined),
}
export const VCENTER_DISK_TYPE = {
@ -97,13 +85,9 @@ export const VCENTER_DISK_TYPE = {
{ text: 'Thin', value: 'thin' },
{ text: 'Thick', value: 'thick' },
{ text: 'Eager Zeroed Thick', value: 'eagerZeroedThick' },
{ text: 'Custom', value: 'custom' }
{ text: 'Custom', value: 'custom' },
],
validation: yup
.string()
.trim()
.notRequired()
.default(undefined)
validation: yup.string().trim().notRequired().default(undefined),
}
export const CACHE = {
@ -117,13 +101,9 @@ export const CACHE = {
{ text: 'Writethrough', value: 'writethrough' },
{ text: 'Writeback', value: 'writeback' },
{ text: 'Directsync', value: 'directsync' },
{ text: 'Unsafe', value: 'unsafe' }
{ text: 'Unsafe', value: 'unsafe' },
],
validation: yup
.string()
.trim()
.notRequired()
.default(undefined)
validation: yup.string().trim().notRequired().default(undefined),
}
export const IO = {
@ -134,13 +114,9 @@ export const IO = {
values: [
{ text: '', value: '' },
{ text: 'Threads', value: 'threads' },
{ text: 'Native', value: 'native' }
{ text: 'Native', value: 'native' },
],
validation: yup
.string()
.trim()
.notRequired()
.default(undefined)
validation: yup.string().trim().notRequired().default(undefined),
}
export const DISCARD = {
@ -151,11 +127,7 @@ export const DISCARD = {
values: [
{ text: '', value: '' },
{ text: 'Ignore', value: 'ignore' },
{ text: 'Unmap', value: 'unmap' }
{ text: 'Unmap', value: 'unmap' },
],
validation: yup
.string()
.trim()
.notRequired()
.default(undefined)
validation: yup.string().trim().notRequired().default(undefined),
}

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