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:
parent
62c0d1fc51
commit
b34b0eb642
@ -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 />
|
||||
|
@ -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'
|
||||
|
@ -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 }
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -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 />
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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 }
|
||||
|
@ -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 }
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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 }
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -44,5 +44,5 @@ export {
|
||||
SelectCard,
|
||||
TierCard,
|
||||
VirtualMachineCard,
|
||||
WavesCard
|
||||
WavesCard,
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -16,7 +16,4 @@
|
||||
import CircleChart from 'client/components/Charts/CircleChart'
|
||||
import SingleBar from 'client/components/Charts/SingleBar'
|
||||
|
||||
export {
|
||||
CircleChart,
|
||||
SingleBar
|
||||
}
|
||||
export { CircleChart, SingleBar }
|
||||
|
@ -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}`
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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 }
|
||||
|
@ -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'
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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 */
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 }
|
||||
|
@ -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'
|
||||
|
@ -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 })))
|
||||
|
@ -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'
|
||||
|
@ -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)))
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -15,6 +15,4 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import CreateForm from 'client/components/Forms/Provider/CreateForm'
|
||||
|
||||
export {
|
||||
CreateForm
|
||||
}
|
||||
export { CreateForm }
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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)))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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 }
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user