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

F #5833: Add initial to oneflow in Sunstone (#2069)

This commit is contained in:
Sergio Betanzos 2022-05-23 19:39:57 +02:00 committed by GitHub
parent 6c3fe14e77
commit bcb12e7104
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 2610 additions and 242 deletions

View File

@ -18,7 +18,9 @@ import {
ModernTv as VmsIcons,
Shuffle as VRoutersIcons,
Archive as TemplatesIcon,
GoogleDocs as TemplateIcon,
EmptyPage as TemplateIcon,
Packages as ServicesIcon,
MultiplePagesEmpty as ServiceTemplateIcon,
Box as StorageIcon,
Db as DatastoreIcon,
BoxIso as ImageIcon,
@ -52,6 +54,14 @@ const VirtualRouters = loadable(
{ ssr: false }
)
const Services = loadable(() => import('client/containers/Services'), {
ssr: false,
})
const ServiceDetail = loadable(
() => import('client/containers/Services/Detail'),
{ ssr: false }
)
const VmTemplates = loadable(() => import('client/containers/VmTemplates'), {
ssr: false,
})
@ -70,6 +80,17 @@ const VMTemplateDetail = loadable(
// const VrTemplates = loadable(() => import('client/containers/VrTemplates'), { ssr: false })
// const VmGroups = loadable(() => import('client/containers/VmGroups'), { ssr: false })
const ServiceTemplates = loadable(
() => import('client/containers/ServiceTemplates'),
{ ssr: false }
)
// const DeployServiceTemplates = loadable(() => import('client/containers/ServiceTemplates/Instantiate'), { ssr: false })
// const CreateServiceTemplates = loadable(() => import('client/containers/ServiceTemplates/Create'), { ssr: false })
const ServiceTemplateDetail = loadable(
() => import('client/containers/ServiceTemplates/Detail'),
{ ssr: false }
)
const Datastores = loadable(() => import('client/containers/Datastores'), {
ssr: false,
})
@ -137,6 +158,10 @@ export const PATH = {
VROUTERS: {
LIST: `/${RESOURCE_NAMES.V_ROUTER}`,
},
SERVICES: {
LIST: `/${RESOURCE_NAMES.SERVICE}`,
DETAIL: `/${RESOURCE_NAMES.SERVICE}/:id`,
},
},
TEMPLATE: {
VMS: {
@ -145,6 +170,12 @@ export const PATH = {
CREATE: `/${RESOURCE_NAMES.VM_TEMPLATE}/create`,
DETAIL: `/${RESOURCE_NAMES.VM_TEMPLATE}/:id`,
},
SERVICES: {
LIST: `/${RESOURCE_NAMES.SERVICE_TEMPLATE}`,
DETAIL: `/${RESOURCE_NAMES.SERVICE_TEMPLATE}/:id`,
DEPLOY: `/${RESOURCE_NAMES.SERVICE_TEMPLATE}/deploy/`,
CREATE: `/${RESOURCE_NAMES.SERVICE_TEMPLATE}/create`,
},
},
STORAGE: {
DATASTORES: {
@ -231,6 +262,19 @@ const ENDPOINTS = [
icon: VRoutersIcons,
Component: VirtualRouters,
},
{
title: T.Services,
path: PATH.INSTANCE.SERVICES.LIST,
sidebar: true,
icon: ServicesIcon,
Component: Services,
},
{
title: T.Service,
description: (params) => `#${params?.id}`,
path: PATH.INSTANCE.SERVICES.DETAIL,
Component: ServiceDetail,
},
],
},
{
@ -265,6 +309,36 @@ const ENDPOINTS = [
path: PATH.TEMPLATE.VMS.DETAIL,
Component: VMTemplateDetail,
},
{
title: T.ServiceTemplates,
path: PATH.TEMPLATE.SERVICES.LIST,
sidebar: true,
icon: ServiceTemplateIcon,
Component: ServiceTemplates,
},
/* {
title: T.DeployServiceTemplate,
description: (_, state) =>
state?.ID !== undefined && `#${state.ID} ${state.NAME}`,
path: PATH.TEMPLATE.SERVICES.DEPLOY,
Component: DeployServiceTemplates,
},
{
title: (_, state) =>
state?.ID !== undefined
? T.UpdateServiceTemplate
: T.CreateServiceTemplate,
description: (_, state) =>
state?.ID !== undefined && `#${state.ID} ${state.NAME}`,
path: PATH.TEMPLATE.SERVICES.CREATE,
Component: CreateServiceTemplates,
}, */
{
title: T.ServiceTemplate,
description: (params) => `#${params?.id}`,
path: PATH.TEMPLATE.SERVICES.DETAIL,
Component: ServiceTemplateDetail,
},
],
},
{

View File

@ -0,0 +1,108 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement, memo, useMemo } from 'react'
import PropTypes from 'prop-types'
import { WarningCircledOutline as WarningIcon } from 'iconoir-react'
import { Typography } from '@mui/material'
import { useViews } from 'client/features/Auth'
import MultipleTags from 'client/components/MultipleTags'
import Timer from 'client/components/Timer'
import { StatusCircle } from 'client/components/Status'
import { rowStyles } from 'client/components/Tables/styles'
import {
timeFromMilliseconds,
getUniqueLabels,
getColorFromString,
} from 'client/models/Helper'
import { getState } from 'client/models/Service'
import { T, Service, ACTIONS, RESOURCE_NAMES } from 'client/constants'
const ServiceCard = memo(
/**
* @param {object} props - Props
* @param {Service} props.service - Service resource
* @param {object} props.rootProps - Props to root component
* @param {function(string):Promise} [props.onDeleteLabel] - Callback to delete label
* @param {ReactElement} [props.actions] - Actions
* @returns {ReactElement} - Card
*/
({ service, rootProps, actions, onDeleteLabel }) => {
const classes = rowStyles()
const { [RESOURCE_NAMES.SERVICE]: serviceView } = useViews()
const enableEditLabels =
serviceView?.actions?.[ACTIONS.EDIT_LABELS] === true && !!onDeleteLabel
const {
ID,
NAME,
TEMPLATE: { BODY: { description, labels, start_time: startTime } = {} },
} = service
const { color: stateColor, name: stateName } = getState(service)
const time = useMemo(() => timeFromMilliseconds(+startTime), [startTime])
const uniqueLabels = useMemo(
() =>
getUniqueLabels(labels).map((label) => ({
text: label,
stateColor: getColorFromString(label),
onDelete: enableEditLabels && onDeleteLabel,
})),
[labels, enableEditLabels, onDeleteLabel]
)
return (
<div {...rootProps} data-cy={`service-template-${ID}`}>
<div className={classes.main}>
<div className={classes.title}>
<StatusCircle color={stateColor} tooltip={stateName} />
<Typography noWrap component="span" title={description}>
{NAME}
</Typography>
<span className={classes.labels}>
<WarningIcon title={description} />
<MultipleTags tags={uniqueLabels} />
</span>
</div>
<div className={classes.caption}>
<span data-cy="id">{`#${ID}`}</span>
<span title={time.toFormat('ff')}>
<Timer translateWord={T.RegisteredAt} initial={time} />
</span>
</div>
</div>
{actions && <div className={classes.actions}>{actions}</div>}
</div>
)
}
)
ServiceCard.propTypes = {
service: PropTypes.object,
rootProps: PropTypes.shape({
className: PropTypes.string,
}),
onDeleteLabel: PropTypes.func,
actions: PropTypes.any,
}
ServiceCard.displayName = 'ServiceCard'
export default ServiceCard

View File

@ -0,0 +1,127 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement, memo, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Network, Package } from 'iconoir-react'
import { Typography } from '@mui/material'
import { useViews } from 'client/features/Auth'
import MultipleTags from 'client/components/MultipleTags'
import Timer from 'client/components/Timer'
import { Tr } from 'client/components/HOC'
import { rowStyles } from 'client/components/Tables/styles'
import {
timeFromMilliseconds,
getUniqueLabels,
getColorFromString,
} from 'client/models/Helper'
import { T, ServiceTemplate, ACTIONS, RESOURCE_NAMES } from 'client/constants'
const ServiceTemplateCard = memo(
/**
* @param {object} props - Props
* @param {ServiceTemplate} props.template - Service Template resource
* @param {object} props.rootProps - Props to root component
* @param {function(string):Promise} [props.onDeleteLabel] - Callback to delete label
* @param {ReactElement} [props.actions] - Actions
* @returns {ReactElement} - Card
*/
({ template, rootProps, actions, onDeleteLabel }) => {
const classes = rowStyles()
const { [RESOURCE_NAMES.SERVICE_TEMPLATE]: serviceView } = useViews()
const enableEditLabels =
serviceView?.actions?.[ACTIONS.EDIT_LABELS] === true && !!onDeleteLabel
const {
ID,
NAME,
TEMPLATE: {
BODY: {
description,
labels,
networks,
roles,
registration_time: regTime,
} = {},
},
} = template
const numberOfRoles = useMemo(() => roles?.length ?? 0, [roles])
const numberOfNetworks = useMemo(
() => Object.keys(networks)?.length ?? 0,
[networks]
)
const time = useMemo(() => timeFromMilliseconds(+regTime), [regTime])
const uniqueLabels = useMemo(
() =>
getUniqueLabels(labels).map((label) => ({
text: label,
stateColor: getColorFromString(label),
onDelete: enableEditLabels && onDeleteLabel,
})),
[labels, enableEditLabels, onDeleteLabel]
)
return (
<div {...rootProps} data-cy={`service-template-${ID}`}>
<div className={classes.main}>
<div className={classes.title}>
<Typography noWrap component="span" title={description}>
{NAME}
</Typography>
<span className={classes.labels}>
<MultipleTags tags={uniqueLabels} />
</span>
</div>
<div className={classes.caption}>
<span data-cy="id">{`#${ID}`}</span>
<span title={time.toFormat('ff')}>
<Timer translateWord={T.RegisteredAt} initial={time} />
</span>
<span title={`${Tr(T.Networks)}: ${numberOfNetworks}`}>
<Network width={20} height={20} />
<span data-cy="total-networks">{numberOfNetworks}</span>
</span>
<span title={`${Tr(T.Roles)}: ${numberOfRoles}`}>
<Package width={20} height={20} />
<span data-cy="total-roles">{numberOfRoles}</span>
</span>
</div>
</div>
{actions && <div className={classes.actions}>{actions}</div>}
</div>
)
}
)
ServiceTemplateCard.propTypes = {
template: PropTypes.object,
rootProps: PropTypes.shape({
className: PropTypes.string,
}),
onDeleteLabel: PropTypes.func,
actions: PropTypes.any,
}
ServiceTemplateCard.displayName = 'ServiceTemplateCard'
export default ServiceTemplateCard

View File

@ -32,6 +32,8 @@ import ProvisionTemplateCard from 'client/components/Cards/ProvisionTemplateCard
import ScheduleActionCard from 'client/components/Cards/ScheduleActionCard'
import SecurityGroupCard from 'client/components/Cards/SecurityGroupCard'
import SelectCard from 'client/components/Cards/SelectCard'
import ServiceCard from 'client/components/Cards/ServiceCard'
import ServiceTemplateCard from 'client/components/Cards/ServiceTemplateCard'
import SnapshotCard from 'client/components/Cards/SnapshotCard'
import TierCard from 'client/components/Cards/TierCard'
import VirtualMachineCard from 'client/components/Cards/VirtualMachineCard'
@ -58,6 +60,8 @@ export {
ScheduleActionCard,
SecurityGroupCard,
SelectCard,
ServiceCard,
ServiceTemplateCard,
SnapshotCard,
TierCard,
VirtualMachineCard,

View File

@ -16,7 +16,7 @@
import { JSXElementConstructor } from 'react'
import PropTypes from 'prop-types'
import { Tooltip } from '@mui/material'
import { Box, Tooltip } from '@mui/material'
import makeStyles from '@mui/styles/makeStyles'
import { TypographyWithPoint } from 'client/components/Typography'
@ -77,10 +77,6 @@ const SingleBar = ({ legend, data, total = 0 }) => {
{data?.map((value, idx) => {
const label = legend[idx]?.name
const color = legend[idx]?.color
const style = {
backgroundColor: color,
'&:hover': { backgroundColor: addOpacityToColor(color, 0.6) },
}
return (
<Tooltip
@ -89,7 +85,12 @@ const SingleBar = ({ legend, data, total = 0 }) => {
placement="top"
title={`${label}: ${value}`}
>
<div style={style}></div>
<Box
sx={{
bgcolor: color,
'&:hover': { bgcolor: addOpacityToColor(color, 0.6) },
}}
/>
</Tooltip>
)
})}

View File

@ -67,6 +67,7 @@ const EnhancedTable = ({
classes = {},
rootProps = {},
searchProps = {},
noDataMessage,
}) => {
const styles = EnhancedTableStyles()
@ -208,12 +209,15 @@ const EnhancedTable = ({
<div className={clsx(styles.body, classes.body)}>
{/* NO DATA MESSAGE */}
{!isLoading && !isUninitialized && page?.length === 0 && (
<span className={styles.noDataMessage}>
<InfoEmpty />
<Translate word={T.NoDataAvailable} />
</span>
)}
{!isLoading &&
!isUninitialized &&
page?.length === 0 &&
(noDataMessage || (
<span className={styles.noDataMessage}>
<InfoEmpty />
<Translate word={T.NoDataAvailable} />
</span>
))}
{/* DATALIST PER PAGE */}
{page.map((row) => {
@ -282,6 +286,11 @@ EnhancedTable.propTypes = {
RowComponent: PropTypes.any,
showPageCount: PropTypes.bool,
singleSelect: PropTypes.bool,
noDataMessage: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node,
PropTypes.bool,
]),
}
export * from 'client/components/Tables/Enhanced/Utils'

View File

@ -0,0 +1,35 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { Column } from 'react-table'
import { T } from 'client/constants'
/** @type {Column[]} Service Template columns */
const COLUMNS = [
{ Header: T.ID, id: 'id', accessor: 'ID', sortType: 'number' },
{ Header: T.Name, id: 'name', accessor: 'NAME' },
{ Header: T.Owner, id: 'owner', accessor: 'UNAME' },
{ Header: T.Group, id: 'group', accessor: 'GNAME' },
{
Header: T.RegistrationTime,
id: 'time',
accessor: 'TEMPLATE.BODY.registration_time',
},
]
COLUMNS.noFilterIds = ['id', 'name', 'time']
export default COLUMNS

View File

@ -0,0 +1,81 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useMemo, ReactElement } from 'react'
import { Alert } from '@mui/material'
import { useViews } from 'client/features/Auth'
import { useGetServiceTemplatesQuery } from 'client/features/OneApi/serviceTemplate'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import ServiceTemplateColumns from 'client/components/Tables/ServiceTemplates/columns'
import ServiceTemplateRow from 'client/components/Tables/ServiceTemplates/row'
import { Translate } from 'client/components/HOC'
import { T, RESOURCE_NAMES } from 'client/constants'
const DEFAULT_DATA_CY = 'service-templates'
/**
* @param {object} props - Props
* @returns {ReactElement} Service Templates table
*/
const ServiceTemplatesTable = (props) => {
const { rootProps = {}, searchProps = {}, ...rest } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useViews()
const {
data = [],
isFetching,
refetch,
error,
} = useGetServiceTemplatesQuery()
const columns = useMemo(
() =>
createColumns({
filters: getResourceView(RESOURCE_NAMES.SERVICE_TEMPLATE)?.filters,
columns: ServiceTemplateColumns,
}),
[view]
)
return (
<EnhancedTable
columns={columns}
data={useMemo(() => data, [data])}
rootProps={rootProps}
searchProps={searchProps}
refetch={refetch}
isLoading={isFetching}
getRowId={(row) => String(row.ID)}
RowComponent={ServiceTemplateRow}
noDataMessage={
error?.status === 500 && (
<Alert severity="error" variant="outlined">
<Translate word={T.CannotConnectOneFlow} />
</Alert>
)
}
{...rest}
/>
)
}
ServiceTemplatesTable.propTypes = { ...EnhancedTable.propTypes }
ServiceTemplatesTable.displayName = 'ServiceTemplatesTable'
export default ServiceTemplatesTable

View File

@ -0,0 +1,70 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo, useMemo, useCallback } from 'react'
import PropTypes from 'prop-types'
import serviceTemplateApi, {
useUpdateServiceTemplateMutation,
} from 'client/features/OneApi/serviceTemplate'
import { ServiceTemplateCard } from 'client/components/Cards'
const Row = memo(
({ original, value, ...props }) => {
const [update] = useUpdateServiceTemplateMutation()
const state =
serviceTemplateApi.endpoints.getServiceTemplates.useQueryState(
undefined,
{
selectFromResult: ({ data = [] }) =>
data.find((template) => +template.ID === +original.ID),
}
)
const memoTemplate = useMemo(() => state ?? original, [state, original])
const handleDeleteLabel = useCallback(
(label) => {
const currentLabels = memoTemplate.TEMPLATE.BODY.labels?.split(',')
const labels = currentLabels.filter((l) => l !== label).join(',')
update({ id: memoTemplate.ID, template: { labels }, append: true })
},
[memoTemplate.TEMPLATE.BODY?.labels, update]
)
return (
<ServiceTemplateCard
template={memoTemplate}
rootProps={props}
onDeleteLabel={handleDeleteLabel}
/>
)
},
(prev, next) => prev.className === next.className
)
Row.propTypes = {
original: PropTypes.object,
value: PropTypes.object,
isSelected: PropTypes.bool,
className: PropTypes.string,
handleClick: PropTypes.func,
}
Row.displayName = 'ServiceTemplateRow'
export default Row

View File

@ -0,0 +1,41 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { Column } from 'react-table'
import { T } from 'client/constants'
/** @type {Column[]} Service columns */
const COLUMNS = [
{ Header: T.ID, id: 'id', accessor: 'ID', sortType: 'number' },
{ Header: T.Name, id: 'name', accessor: 'NAME' },
{ Header: T.Owner, id: 'owner', accessor: 'UNAME' },
{ Header: T.Group, id: 'group', accessor: 'GNAME' },
{ Header: T.State, id: 'state', accessor: 'TEMPLATE.BODY.state' },
{
Header: T.Description,
id: 'description',
accessor: 'TEMPLATE.BODY.description',
},
{
Header: T.StartTime,
id: 'time',
accessor: 'TEMPLATE.BODY.start_time',
},
]
COLUMNS.noFilterIds = ['id', 'name', 'description', 'time']
export default COLUMNS

View File

@ -0,0 +1,76 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useMemo, ReactElement } from 'react'
import { Alert } from '@mui/material'
import { useViews } from 'client/features/Auth'
import { useGetServicesQuery } from 'client/features/OneApi/service'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import ServiceColumns from 'client/components/Tables/Services/columns'
import ServiceRow from 'client/components/Tables/Services/row'
import { Translate } from 'client/components/HOC'
import { T, RESOURCE_NAMES } from 'client/constants'
const DEFAULT_DATA_CY = 'services'
/**
* @param {object} props - Props
* @returns {ReactElement} Service table
*/
const ServicesTable = (props) => {
const { rootProps = {}, searchProps = {}, ...rest } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useViews()
const { data = [], isFetching, refetch, error } = useGetServicesQuery()
const columns = useMemo(
() =>
createColumns({
filters: getResourceView(RESOURCE_NAMES.SERVICE)?.filters,
columns: ServiceColumns,
}),
[view]
)
return (
<EnhancedTable
columns={columns}
data={useMemo(() => data, [data])}
rootProps={rootProps}
searchProps={searchProps}
refetch={refetch}
isLoading={isFetching}
getRowId={(row) => String(row.ID)}
RowComponent={ServiceRow}
noDataMessage={
error?.status === 500 && (
<Alert severity="error" variant="outlined">
<Translate word={T.CannotConnectOneFlow} />
</Alert>
)
}
{...rest}
/>
)
}
ServicesTable.propTypes = { ...EnhancedTable.propTypes }
ServicesTable.displayName = 'ServicesTable'
export default ServicesTable

View File

@ -0,0 +1,46 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo, useMemo } from 'react'
import PropTypes from 'prop-types'
import serviceApi from 'client/features/OneApi/service'
import { ServiceCard } from 'client/components/Cards'
const Row = memo(
({ original, value, ...props }) => {
const state = serviceApi.endpoints.getServices.useQueryState(undefined, {
selectFromResult: ({ data = [] }) =>
data.find((service) => +service.ID === +original.ID),
})
const memoService = useMemo(() => state ?? original, [state, original])
return <ServiceCard service={memoService} rootProps={props} />
},
(prev, next) => prev.className === next.className
)
Row.propTypes = {
original: PropTypes.object,
value: PropTypes.object,
isSelected: PropTypes.bool,
className: PropTypes.string,
handleClick: PropTypes.func,
}
Row.displayName = 'ServiceRow'
export default Row

View File

@ -23,6 +23,8 @@ import ImagesTable from 'client/components/Tables/Images'
import MarketplaceAppsTable from 'client/components/Tables/MarketplaceApps'
import MarketplacesTable from 'client/components/Tables/Marketplaces'
import SecurityGroupsTable from 'client/components/Tables/SecurityGroups'
import ServicesTable from 'client/components/Tables/Services'
import ServiceTemplatesTable from 'client/components/Tables/ServiceTemplates'
import SkeletonTable from 'client/components/Tables/Skeleton'
import UsersTable from 'client/components/Tables/Users'
import VirtualizedTable from 'client/components/Tables/Virtualized'
@ -46,6 +48,8 @@ export {
MarketplaceAppsTable,
MarketplacesTable,
SecurityGroupsTable,
ServicesTable,
ServiceTemplatesTable,
UsersTable,
VmsTable,
VmTemplatesTable,

View File

@ -0,0 +1,50 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import PropTypes from 'prop-types'
import { Stack } from '@mui/material'
import { useGetServiceQuery } from 'client/features/OneApi/service'
// import ScheduleActionCard from 'client/components/Cards/ScheduleActionCard'
/**
* Renders the list of schedule actions from a Service.
*
* @param {object} props - Props
* @param {string} props.id - Service id
* @param {object|boolean} props.tabProps - Tab properties
* @param {object} [props.tabProps.actions] - Actions from user view yaml
* @returns {ReactElement} Schedule actions tab
*/
const SchedulingTab = ({ id, tabProps: { actions } = {} }) => {
const { data: service = {} } = useGetServiceQuery({ id })
return (
<>
<Stack gap="1em" py="0.8em">
{service?.NAME}
{/* TODO: scheduler actions & form */}
</Stack>
</>
)
}
SchedulingTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
export default SchedulingTab

View File

@ -0,0 +1,92 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import PropTypes from 'prop-types'
import { Stack } from '@mui/material'
import { useGetServiceQuery } from 'client/features/OneApi/service'
import { Permissions, Ownership } from 'client/components/Tabs/Common'
import Information from 'client/components/Tabs/Service/Info/information'
import { getActionsAvailable } from 'client/models/Helper'
/**
* Renders mainly information tab.
*
* @param {object} props - Props
* @param {object} props.tabProps - Tab information
* @param {string} props.id - Template id
* @returns {ReactElement} Information tab
*/
const ServiceInfoTab = ({ tabProps = {}, id }) => {
const {
information_panel: informationPanel,
permissions_panel: permissionsPanel,
ownership_panel: ownershipPanel,
} = tabProps
const { data: service = {} } = useGetServiceQuery({ id })
const { UNAME, UID, GNAME, GID, PERMISSIONS = {} } = service
const getActions = (actions) => getActionsAvailable(actions)
return (
<Stack
display="grid"
gap="1em"
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
padding={{ sm: '0.8em' }}
>
{informationPanel?.enabled && (
<Information
actions={getActions(informationPanel?.actions)}
service={service}
/>
)}
{permissionsPanel?.enabled && (
<Permissions
actions={getActions(permissionsPanel?.actions)}
ownerUse={PERMISSIONS.OWNER_U}
ownerManage={PERMISSIONS.OWNER_M}
ownerAdmin={PERMISSIONS.OWNER_A}
groupUse={PERMISSIONS.GROUP_U}
groupManage={PERMISSIONS.GROUP_M}
groupAdmin={PERMISSIONS.GROUP_A}
otherUse={PERMISSIONS.OTHER_U}
otherManage={PERMISSIONS.OTHER_M}
otherAdmin={PERMISSIONS.OTHER_A}
/>
)}
{ownershipPanel?.enabled && (
<Ownership
actions={getActions(ownershipPanel?.actions)}
userId={UID}
userName={UNAME}
groupId={GID}
groupName={GNAME}
/>
)}
</Stack>
)
}
ServiceInfoTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
ServiceInfoTab.displayName = 'ServiceInfoTab'
export default ServiceInfoTab

View File

@ -0,0 +1,106 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import PropTypes from 'prop-types'
import { Stack } from '@mui/material'
import { List } from 'client/components/Tabs/Common'
import { StatusCircle, StatusChip } from 'client/components/Status'
import { getState } from 'client/models/Service'
import { timeToString, booleanToString } from 'client/models/Helper'
import { T, Service } from 'client/constants'
/**
* Renders mainly information tab.
*
* @param {object} props - Props
* @param {Service} props.service - Service
* @param {string[]} props.actions - Available actions to information tab
* @returns {ReactElement} Information tab
*/
const InformationPanel = ({ service = {}, actions }) => {
const {
ID,
NAME,
TEMPLATE: {
BODY: {
deployment,
shutdown_action: shutdownAction,
registration_time: regTime,
ready_status_gate: readyStatusGate,
automatic_deletion: autoDelete,
} = {},
},
} = service || {}
const { name: stateName, color: stateColor } = getState(service)
const info = [
{ name: T.ID, value: ID, dataCy: 'id' },
{ name: T.Name, value: NAME, dataCy: 'name' },
{
name: T.State,
value: (
<Stack direction="row" alignItems="center" gap={1}>
<StatusCircle color={stateColor} />
<StatusChip dataCy="state" text={stateName} stateColor={stateColor} />
</Stack>
),
},
{
name: T.StartTime,
value: timeToString(regTime),
dataCy: 'time',
},
{
name: T.Strategy,
value: deployment,
dataCy: 'deployment',
},
{
name: T.ShutdownAction,
value: shutdownAction,
dataCy: 'shutdown-action',
},
{
name: T.ReadyStatusGate,
value: booleanToString(readyStatusGate),
dataCy: 'ready-status-gate',
},
{
name: T.AutomaticDeletion,
value: booleanToString(autoDelete),
dataCy: 'auto-delete',
},
].filter(Boolean)
return (
<List
title={T.Information}
list={info}
containerProps={{ sx: { gridRow: 'span 2' } }}
/>
)
}
InformationPanel.displayName = 'InformationPanel'
InformationPanel.propTypes = {
service: PropTypes.object,
actions: PropTypes.arrayOf(PropTypes.string),
}
export default InformationPanel

View File

@ -0,0 +1,61 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import PropTypes from 'prop-types'
import { Stack, Typography } from '@mui/material'
import { useGetServiceQuery } from 'client/features/OneApi/service'
import { timeFromMilliseconds } from 'client/models/Helper'
import { Service, SERVICE_LOG_SEVERITY } from 'client/constants'
/**
* Renders log tab.
*
* @param {object} props - Props
* @param {string} props.id - Service id
* @returns {ReactElement} Log tab
*/
const LogTab = ({ id }) => {
const { data: service = {} } = useGetServiceQuery({ id })
/** @type {Service} */
const { TEMPLATE: { BODY: { log = [] } = {} } = {} } = service
return (
<Stack gap="0.5em" p="1em" bgcolor="background.default">
{log?.map(({ severity, message, timestamp } = {}) => {
const time = timeFromMilliseconds(+timestamp)
const isError = severity === SERVICE_LOG_SEVERITY.ERROR
return (
<Typography
key={`message-${timestamp}`}
noWrap
variant="body2"
color={isError ? 'error' : 'textPrimary'}
>
{`${time.toFormat('ff')} [${severity}] ${message}`}
</Typography>
)
})}
</Stack>
)
}
LogTab.propTypes = { tabProps: PropTypes.object, id: PropTypes.string }
LogTab.displayName = 'RolesTab'
export default LogTab

View File

@ -0,0 +1,118 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement, memo, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Link as RouterLink, generatePath } from 'react-router-dom'
import { Box, Typography, Link, CircularProgress } from '@mui/material'
import { useGetServiceQuery } from 'client/features/OneApi/service'
import { useGetTemplatesQuery } from 'client/features/OneApi/vmTemplate'
import { Translate } from 'client/components/HOC'
import { T, ServiceTemplateRole } from 'client/constants'
import { PATH } from 'client/apps/sunstone/routesOne'
const COLUMNS = [T.Name, T.Cardinality, T.VMTemplate, T.Parents]
/**
* Renders template tab.
*
* @param {object} props - Props
* @param {string} props.id - Service Template id
* @returns {ReactElement} Roles tab
*/
const RolesTab = ({ id }) => {
const { data: template = {} } = useGetServiceQuery({ id })
const roles = template?.TEMPLATE?.BODY?.roles || []
return (
<Box
display="grid"
gridTemplateColumns="repeat(4, 1fr)"
padding="1em"
bgcolor="background.default"
>
{COLUMNS.map((col) => (
<Typography key={col} noWrap variant="subtitle1" padding="0.5em">
<Translate word={col} />
</Typography>
))}
{roles.map((role, idx) => (
<Box
key={`role-${role.name ?? idx}`}
display="contents"
padding="0.5em"
// hover except for the circular progress component
sx={{ '&:hover > *:not(span)': { bgcolor: 'action.hover' } }}
>
<RoleComponent role={role} />
</Box>
))}
</Box>
)
}
RolesTab.propTypes = { tabProps: PropTypes.object, id: PropTypes.string }
RolesTab.displayName = 'RolesTab'
const RoleComponent = memo(({ role }) => {
/** @type {ServiceTemplateRole} */
const { name, cardinality, vm_template: templateId, parents } = role
const { data: template, isLoading } = useGetTemplatesQuery(undefined, {
selectFromResult: ({ data = [], ...restOfQuery }) => ({
data: data.find((item) => +item.ID === +templateId),
...restOfQuery,
}),
})
const linkToVmTemplate = useMemo(
() => generatePath(PATH.TEMPLATE.VMS.DETAIL, { id: templateId }),
[templateId]
)
const commonProps = { noWrap: true, variant: 'subtitle2', padding: '0.5em' }
return (
<>
<Typography {...commonProps} data-cy="name">
{name}
</Typography>
<Typography {...commonProps} data-cy="cardinality">
{cardinality}
</Typography>
{isLoading ? (
<CircularProgress color="secondary" size={20} />
) : (
<Link
{...commonProps}
color="secondary"
component={RouterLink}
to={linkToVmTemplate}
>
{`#${template?.ID} ${template?.NAME}`}
</Link>
)}
<Typography {...commonProps} data-cy="parents">
{parents?.join?.()}
</Typography>
</>
)
})
RoleComponent.propTypes = { role: PropTypes.object }
RoleComponent.displayName = 'RoleComponent'
export default RolesTab

View File

@ -0,0 +1,68 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Alert, LinearProgress } from '@mui/material'
import { useViews } from 'client/features/Auth'
import { useGetServiceQuery } from 'client/features/OneApi/service'
import { getAvailableInfoTabs } from 'client/models/Helper'
import { RESOURCE_NAMES } from 'client/constants'
import Tabs from 'client/components/Tabs'
import Info from 'client/components/Tabs/Service/Info'
import Roles from 'client/components/Tabs/Service/Roles'
import Log from 'client/components/Tabs/Service/Log'
import Actions from 'client/components/Tabs/Service/Actions'
const getTabComponent = (tabName) =>
({
info: Info,
roles: Roles,
log: Log,
schedulerAction: Actions,
}[tabName])
const ServiceTabs = memo(({ id }) => {
const { view, getResourceView } = useViews()
const { isLoading, isError, error } = useGetServiceQuery({ id })
const tabsAvailable = useMemo(() => {
const resource = RESOURCE_NAMES.SERVICE
const infoTabs = getResourceView(resource)?.['info-tabs'] ?? {}
return getAvailableInfoTabs(infoTabs, getTabComponent, id)
}, [view])
if (isError) {
return (
<Alert severity="error" variant="outlined">
{error.data}
</Alert>
)
}
return isLoading ? (
<LinearProgress color="secondary" sx={{ width: '100%' }} />
) : (
<Tabs addBorder tabs={tabsAvailable ?? []} />
)
})
ServiceTabs.propTypes = { id: PropTypes.string.isRequired }
ServiceTabs.displayName = 'ServiceTabs'
export default ServiceTabs

View File

@ -0,0 +1,119 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import PropTypes from 'prop-types'
import { Stack } from '@mui/material'
import {
useGetServiceTemplateQuery,
useChangeServiceTemplatePermissionsMutation,
useChangeServiceTemplateOwnershipMutation,
} from 'client/features/OneApi/serviceTemplate'
import { Permissions, Ownership } from 'client/components/Tabs/Common'
import Information from 'client/components/Tabs/ServiceTemplate/Info/information'
import { getActionsAvailable, permissionsToOctal } from 'client/models/Helper'
import { toSnakeCase } from 'client/utils'
/**
* Renders mainly information tab.
*
* @param {object} props - Props
* @param {object} props.tabProps - Tab information
* @param {string} props.id - Template id
* @returns {ReactElement} Information tab
*/
const ServiceTemplateInfoTab = ({ tabProps = {}, id }) => {
const {
information_panel: informationPanel,
permissions_panel: permissionsPanel,
ownership_panel: ownershipPanel,
} = tabProps
const [changePermissions] = useChangeServiceTemplatePermissionsMutation()
const [changeOwnership] = useChangeServiceTemplateOwnershipMutation()
const { data: template = {} } = useGetServiceTemplateQuery({ id })
const { UNAME, UID, GNAME, GID, PERMISSIONS = {} } = template
const handleChangeOwnership = async (newOwnership) => {
await changeOwnership({ id, ...newOwnership })
}
const handleChangePermission = async (newPermission) => {
const [key, value] = Object.entries(newPermission)[0]
// transform key to snake case concatenated by the first letter of permission type
// example: 'OWNER_ADMIN' -> 'OWNER_A'
const [member, permission] = toSnakeCase(key).toUpperCase().split('_')
const fullPermissionName = `${member}_${permission[0]}`
const newPermissions = { ...PERMISSIONS, [fullPermissionName]: value }
const octet = permissionsToOctal(newPermissions)
await changePermissions({ id, octet })
}
const getActions = (actions) => getActionsAvailable(actions)
return (
<Stack
display="grid"
gap="1em"
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
padding={{ sm: '0.8em' }}
>
{informationPanel?.enabled && (
<Information
actions={getActions(informationPanel?.actions)}
template={template}
/>
)}
{permissionsPanel?.enabled && (
<Permissions
actions={getActions(permissionsPanel?.actions)}
handleEdit={handleChangePermission}
ownerUse={PERMISSIONS.OWNER_U}
ownerManage={PERMISSIONS.OWNER_M}
ownerAdmin={PERMISSIONS.OWNER_A}
groupUse={PERMISSIONS.GROUP_U}
groupManage={PERMISSIONS.GROUP_M}
groupAdmin={PERMISSIONS.GROUP_A}
otherUse={PERMISSIONS.OTHER_U}
otherManage={PERMISSIONS.OTHER_M}
otherAdmin={PERMISSIONS.OTHER_A}
/>
)}
{ownershipPanel?.enabled && (
<Ownership
actions={getActions(ownershipPanel?.actions)}
handleEdit={handleChangeOwnership}
userId={UID}
userName={UNAME}
groupId={GID}
groupName={GNAME}
/>
)}
</Stack>
)
}
ServiceTemplateInfoTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
ServiceTemplateInfoTab.displayName = 'ServiceTemplateInfoTab'
export default ServiceTemplateInfoTab

View File

@ -0,0 +1,100 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import PropTypes from 'prop-types'
import { List } from 'client/components/Tabs/Common'
import { useRenameServiceTemplateMutation } from 'client/features/OneApi/serviceTemplate'
import { timeToString, booleanToString } from 'client/models/Helper'
import { T, VM_TEMPLATE_ACTIONS, ServiceTemplate } from 'client/constants'
/**
* Renders mainly information tab.
*
* @param {object} props - Props
* @param {ServiceTemplate} props.template - Service Template
* @param {string[]} props.actions - Available actions to information tab
* @returns {ReactElement} Information tab
*/
const InformationPanel = ({ template = {}, actions }) => {
const [renameTemplate] = useRenameServiceTemplateMutation()
const {
ID,
NAME,
TEMPLATE: {
BODY: {
description,
registration_time: regTime,
ready_status_gate: readyStatusGate,
automatic_deletion: autoDelete,
} = {},
},
} = template || {}
const handleRename = async (_, newName) => {
await renameTemplate({ id: ID, name: newName })
}
const info = [
{ name: T.ID, value: ID, dataCy: 'id' },
{
name: T.Name,
value: NAME,
canEdit: actions?.includes?.(VM_TEMPLATE_ACTIONS.RENAME),
handleEdit: handleRename,
dataCy: 'name',
},
{
name: T.Description,
value: description,
dataCy: 'description',
},
{
name: T.StartTime,
value: timeToString(regTime),
dataCy: 'time',
},
{
name: T.ReadyStatusGate,
value: booleanToString(readyStatusGate),
dataCy: 'ready-status-gate',
},
{
name: T.AutomaticDeletion,
value: booleanToString(autoDelete),
dataCy: 'auto-delete',
},
].filter(Boolean)
return (
<List
title={T.Information}
list={info}
containerProps={{ sx: { gridRow: 'span 2' } }}
/>
)
}
InformationPanel.displayName = 'InformationPanel'
InformationPanel.propTypes = {
actions: PropTypes.arrayOf(PropTypes.string),
template: PropTypes.object,
}
export default InformationPanel

View File

@ -0,0 +1,118 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement, memo, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Link as RouterLink, generatePath } from 'react-router-dom'
import { Box, Typography, Link, CircularProgress } from '@mui/material'
import { useGetServiceTemplateQuery } from 'client/features/OneApi/serviceTemplate'
import { useGetTemplatesQuery } from 'client/features/OneApi/vmTemplate'
import { Translate } from 'client/components/HOC'
import { T, Role } from 'client/constants'
import { PATH } from 'client/apps/sunstone/routesOne'
const COLUMNS = [T.Name, T.Cardinality, T.VMTemplate, T.Parents]
/**
* Renders roles tab.
*
* @param {object} props - Props
* @param {string} props.id - Service Template id
* @returns {ReactElement} Roles tab
*/
const RolesTab = ({ id }) => {
const { data: template = {} } = useGetServiceTemplateQuery({ id })
const roles = template?.TEMPLATE?.BODY?.roles || []
return (
<Box
display="grid"
gridTemplateColumns="repeat(4, 1fr)"
padding="1em"
bgcolor="background.default"
>
{COLUMNS.map((col) => (
<Typography key={col} noWrap variant="subtitle1" padding="0.5em">
<Translate word={col} />
</Typography>
))}
{roles.map((role, idx) => (
<Box
key={`role-${role.name ?? idx}`}
display="contents"
padding="0.5em"
// hover except for the circular progress component
sx={{ '&:hover > *:not(span)': { bgcolor: 'action.hover' } }}
>
<RoleComponent role={role} />
</Box>
))}
</Box>
)
}
RolesTab.propTypes = { tabProps: PropTypes.object, id: PropTypes.string }
RolesTab.displayName = 'RolesTab'
const RoleComponent = memo(({ role }) => {
/** @type {Role} */
const { name, cardinality, vm_template: templateId, parents } = role
const { data: template, isLoading } = useGetTemplatesQuery(undefined, {
selectFromResult: ({ data = [], ...restOfQuery }) => ({
data: data.find((item) => +item.ID === +templateId),
...restOfQuery,
}),
})
const linkToVmTemplate = useMemo(
() => generatePath(PATH.TEMPLATE.VMS.DETAIL, { id: templateId }),
[templateId]
)
const commonProps = { noWrap: true, variant: 'subtitle2', padding: '0.5em' }
return (
<>
<Typography {...commonProps} data-cy="name">
{name}
</Typography>
<Typography {...commonProps} data-cy="cardinality">
{cardinality}
</Typography>
{isLoading ? (
<CircularProgress color="secondary" size={20} />
) : (
<Link
{...commonProps}
color="secondary"
component={RouterLink}
to={linkToVmTemplate}
>
{`#${template?.ID} ${template?.NAME}`}
</Link>
)}
<Typography {...commonProps} data-cy="parents">
{parents?.join?.()}
</Typography>
</>
)
})
RoleComponent.propTypes = { role: PropTypes.object }
RoleComponent.displayName = 'RoleComponent'
export default RolesTab

View File

@ -0,0 +1,55 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import PropTypes from 'prop-types'
import { Box, Accordion, AccordionDetails } from '@mui/material'
import { useGetServiceTemplateQuery } from 'client/features/OneApi/serviceTemplate'
/**
* Renders template tab.
*
* @param {object} props - Props
* @param {string} props.id - Service Template id
* @returns {ReactElement} Template tab
*/
const TemplateTab = ({ id }) => {
const { data: template = {} } = useGetServiceTemplateQuery({ id })
return (
<Accordion variant="outlined" expanded>
<AccordionDetails>
<Box component="pre">
<Box
component="code"
sx={{ whiteSpace: 'break-spaces', wordBreak: 'break-all' }}
>
{JSON.stringify(template?.TEMPLATE?.BODY ?? {}, null, 2)}
</Box>
</Box>
</AccordionDetails>
</Accordion>
)
}
TemplateTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
TemplateTab.displayName = 'TemplateTab'
export default TemplateTab

View File

@ -0,0 +1,66 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Alert, LinearProgress } from '@mui/material'
import { useViews } from 'client/features/Auth'
import { useGetServiceTemplateQuery } from 'client/features/OneApi/serviceTemplate'
import { getAvailableInfoTabs } from 'client/models/Helper'
import { RESOURCE_NAMES } from 'client/constants'
import Tabs from 'client/components/Tabs'
import Info from 'client/components/Tabs/ServiceTemplate/Info'
import Roles from 'client/components/Tabs/ServiceTemplate/Roles'
import Template from 'client/components/Tabs/ServiceTemplate/Template'
const getTabComponent = (tabName) =>
({
info: Info,
roles: Roles,
template: Template,
}[tabName])
const ServiceTemplateTabs = memo(({ id }) => {
const { view, getResourceView } = useViews()
const { isLoading, isError, error } = useGetServiceTemplateQuery({ id })
const tabsAvailable = useMemo(() => {
const resource = RESOURCE_NAMES.SERVICE_TEMPLATE
const infoTabs = getResourceView(resource)?.['info-tabs'] ?? {}
return getAvailableInfoTabs(infoTabs, getTabComponent, id)
}, [view])
if (isError) {
return (
<Alert severity="error" variant="outlined">
{error.data}
</Alert>
)
}
return isLoading ? (
<LinearProgress color="secondary" sx={{ width: '100%' }} />
) : (
<Tabs addBorder tabs={tabsAvailable ?? []} />
)
})
ServiceTemplateTabs.propTypes = { id: PropTypes.string.isRequired }
ServiceTemplateTabs.displayName = 'ServiceTemplateTabs'
export default ServiceTemplateTabs

View File

@ -16,7 +16,112 @@
import * as STATES from 'client/constants/states'
import COLOR from 'client/constants/color'
export const APPLICATION_STATES = [
/**
* @typedef {'CHANGE'|'CARDINALITY'|'PERCENTAGE_CHANGE'} AdjustmentType
*/
/**
* @typedef ElasticityPolicy
* @property {AdjustmentType} type - Type of adjustment
* @property {string} adjust - Adjustment type
* @property {string} [min_adjust_type] - Minimum adjustment type
* @property {string} [cooldown] - Cooldown period duration after a scale operation, in seconds
* @property {string} [period] - Duration, in seconds, of each period in period_number
* @property {string} [period_number] - Number of periods that the expression must be true before the elasticity is triggered
* @property {string} expression - Expression to trigger the elasticity
* @property {string} [last_eval] - Last time the policy was evaluated
* @property {string} [true_evals] - Number of times the policy was evaluated to true
* @property {string} [expression_evaluated] - Expression evaluated to true
*/
/**
* @typedef ScheduledPolicy
* @property {AdjustmentType} type - Type of adjustment
* @property {string} adjust - Adjustment type
* @property {string} [min_adjust_step] - Optional parameter for PERCENTAGE_CHANGE adjustment type.
* If present, the policy will change the cardinality by at least the number of VMs set in this attribute.
* @property {string} [recurrence] - Time for recurring adjustments. Time is specified with the Unix cron syntax
* @property {string} [start_time] - Exact time for the adjustment
* @property {string} [cooldown] - Cooldown period duration after a scale operation, in seconds
* @property {string} [last_eval] - Last time the policy was evaluated
*/
/**
* @typedef Node
* @property {string} deploy_id - Deployment id
* @property {object} vm_info - VM information
* @property {object} vm_info.VM - Virtual machine object
* @property {string} vm_info.VM.ID - VM id
* @property {string} vm_info.VM.NAME - VM name
* @property {string} vm_info.VM.UID - Owner id
* @property {string} vm_info.VM.UNAME - Owner name
* @property {string} vm_info.VM.GID - Group id
* @property {string} vm_info.VM.GNAME - Group name
*/
/**
* @typedef Role
* @property {string} name - Name
* @property {string} cardinality - Cardinality
* @property {string[]} [parents] - Names of the roles that must be deployed before this one
* @property {string} [last_vmname] - ??
* @property {string} state - Role state (see @see ROLE_STATES for more info)
* @property {string} vm_template - OpenNebula VM Template ID
* @property {string} [vm_template_contents] - Contents to be used into VM template
* @property {'shutdown'|'shutdown-hard'} [shutdown_action] - VM shutdown action
* @property {string} [min_vms] - Minimum number of VMs for elasticity adjustments
* @property {string} [max_vms] - Maximum number of VMs for elasticity adjustments
* @property {string} [cooldown] - Cooldown period duration after a scale operation, in seconds.
* If it is not set, the default set in `oneflow-server.conf` will be used.
* @property {boolean} [on_hold] - VM role is on hold (not deployed)
* @property {ElasticityPolicy[]} [elasticity_policies] - Elasticity Policies
* @property {ElasticityPolicy[]} [scheduled_policies] - Scheduled Policies
* @property {Node[]} nodes - Nodes information (see @see Node for more info)
*/
/**
* @typedef ServiceLogItem
* @property {string} message - Log message
* @property {SERVICE_LOG_SEVERITY} severity - Severity (see @see SERVICE_LOG_SEVERITY for more info)
* @property {string} timestamp - Timestamp
*/
/**
* @typedef Service
* @property {string} ID - Id
* @property {string} NAME - Name
* @property {string} UID - User id
* @property {string} UNAME - User name
* @property {string} GID - Group id
* @property {string} GNAME - Group name
* @property {Permissions} [PERMISSIONS] - Permissions
* @property {object} TEMPLATE - Template
* @property {object} TEMPLATE.BODY - Body in JSON format
* @property {string} TEMPLATE.BODY.name - Template name
* @property {string} TEMPLATE.BODY.description - Template description
* @property {string} TEMPLATE.BODY.state - Service state
* @property {object} [TEMPLATE.BODY.custom_attrs] - Hash of custom attributes to use in the service
* @property {object} [TEMPLATE.BODY.custom_attrs_values] - ??
* @property {'straight'|'none'} [TEMPLATE.BODY.deployment] - Deployment strategy of the service:
* - 'none' - all roles are deployed at the same time
* - 'straight' - each role is deployed when all its parents are RUNNING
* @property {ServiceLogItem[]} [TEMPLATE.BODY.log] - Log items
* @property {object} [TEMPLATE.BODY.networks] - Network to print an special user inputs on instantiation form
* @property {object[]} [TEMPLATE.BODY.networks_values] - Network values to include on roles
* @property {boolean} [TEMPLATE.BODY.ready_status_gate] - If ready_status_gate is set to true,
* a VM will only be considered to be in running state the following points are true:
*
* - VM is in running state for OpenNebula. Which specifically means that LCM_STATE == 3 and STATE >= 3
* - The VM has READY=YES in the user template, this can be reported by the VM using OneGate
* @property {'terminate'|'terminate-hard'|'shutdown'|'shutdown-hard'} [TEMPLATE.BODY.shutdown_action] - VM shutdown action
* @property {Role[]} TEMPLATE.BODY.roles - Roles information (see @see Role for more info)
* @property {string} [TEMPLATE.BODY.registration_time] - Registration time
* @property {boolean} [TEMPLATE.BODY.automatic_deletion] - Automatic deletion
* @property {boolean} [TEMPLATE.BODY.on_hold] - VMs of the service are on hold (not deployed)
*/
/** @type {STATES.StateInfo[]} Service states */
export const SERVICE_STATES = [
{
// 0
name: STATES.PENDING,
@ -29,19 +134,19 @@ export const APPLICATION_STATES = [
// 1
name: STATES.DEPLOYING,
color: COLOR.info.main,
meaning: 'Some Tiers are being deployed',
meaning: 'Some roles are being deployed',
},
{
// 2
name: STATES.RUNNING,
color: COLOR.success.main,
meaning: 'All Tiers are deployed successfully',
meaning: 'All roles are deployed successfully',
},
{
// 3
name: STATES.UNDEPLOYING,
color: COLOR.error.light,
meaning: 'Some Tiers are being undeployed',
meaning: 'Some roles are being undeployed',
},
{
// 4
@ -52,10 +157,10 @@ export const APPLICATION_STATES = [
{
// 5
name: STATES.DONE,
color: COLOR.error.dark,
color: COLOR.debug.light,
meaning: `
The Applications will stay in this state after
a successful undeployment. It can be deleted`,
a successful undeploying. It can be deleted`,
},
{
// 6
@ -72,8 +177,8 @@ export const APPLICATION_STATES = [
{
// 8
name: STATES.SCALING,
color: COLOR.error.light,
meaning: 'A Tier is scaling up or down',
color: COLOR.info.main,
meaning: 'A roles is scaling up or down',
},
{
// 9
@ -84,26 +189,26 @@ export const APPLICATION_STATES = [
{
// 10
name: STATES.COOLDOWN,
color: COLOR.error.light,
meaning: 'A Tier is in the cooldown period after a scaling operation',
color: COLOR.info.main,
meaning: 'A roles is in the cooldown period after a scaling operation',
},
{
// 11
name: STATES.DEPLOYING_NETS,
color: COLOR.info.main,
meaning: '',
meaning: 'Service networks are being deployed, they are in LOCK state',
},
{
// 12
name: STATES.UNDEPLOYING_NETS,
color: COLOR.error.light,
meaning: '',
meaning: 'An error occurred while undeploying the Service networks',
},
{
// 13
name: STATES.FAILED_DEPLOYING_NETS,
color: COLOR.error.dark,
meaning: '',
meaning: 'An error occurred while deploying the Service networks',
},
{
// 14
@ -111,66 +216,146 @@ export const APPLICATION_STATES = [
color: COLOR.error.dark,
meaning: '',
},
{
// 15
name: STATES.HOLD,
color: COLOR.info.main,
meaning: 'All roles are in hold state',
},
]
export const TIER_STATES = [
/** @type {STATES.StateInfo[]} Role states */
export const ROLE_STATES = [
{
// 0
name: STATES.PENDING,
color: '',
meaning: 'The Tier is waiting to be deployed',
color: COLOR.info.light,
meaning: 'The role is waiting to be deployed',
},
{
// 1
name: STATES.DEPLOYING,
color: '',
color: COLOR.info.main,
meaning: `
The VMs are being created, and will be
monitored until all of them are running`,
},
{
// 2
name: STATES.RUNNING,
color: '',
color: COLOR.success.main,
meaning: 'All the VMs are running',
},
{
name: STATES.WARNING,
color: '',
meaning: 'A VM was found in a failure state',
},
{
name: STATES.SCALING,
color: '',
meaning: 'The Tier is waiting for VMs to be deployed or to be shutdown',
},
{
name: STATES.COOLDOWN,
color: '',
meaning: 'The Tier is in the cooldown period after a scaling operation',
},
{
// 3
name: STATES.UNDEPLOYING,
color: '',
color: COLOR.error.light,
meaning: `
The VMs are being shutdown. The Tier will stay in
The VMs are being shutdown. The role will stay in
this state until all VMs are done`,
},
{
// 4
name: STATES.WARNING,
color: COLOR.error.light,
meaning: 'A VM was found in a failure state',
},
{
// 5
name: STATES.DONE,
color: '',
color: COLOR.debug.light,
meaning: 'All the VMs are done',
},
{
name: STATES.FAILED_DEPLOYING,
color: '',
meaning: 'An error occurred while deploying the VMs',
},
{
// 6
name: STATES.FAILED_UNDEPLOYING,
color: '',
color: COLOR.error.dark,
meaning: 'An error occurred while undeploying the VMs',
},
{
// 7
name: STATES.FAILED_DEPLOYING,
color: COLOR.error.dark,
meaning: 'An error occurred while deploying the VMs',
},
{
// 8
name: STATES.SCALING,
color: COLOR.info.main,
meaning: 'The role is waiting for VMs to be deployed or to be shutdown',
},
{
// 9
name: STATES.FAILED_SCALING,
color: '',
meaning: 'An error occurred while scaling the Tier',
color: COLOR.error.dark,
meaning: 'An error occurred while scaling the role',
},
{
// 10
name: STATES.COOLDOWN,
color: COLOR.info.main,
meaning: 'The role is in the cooldown period after a scaling operation',
},
{
// 11
name: STATES.HOLD,
color: COLOR.info.main,
meaning:
'The VMs are HOLD and will not be scheduled until them are released',
},
]
/** @enum {string} Role actions */
export const ROLE_ACTIONS = {
CREATE_DIALOG: 'create_dialog',
HOLD: 'hold',
POWEROFF_HARD: 'poweroff_hard',
POWEROFF: 'poweroff',
REBOOT_HARD: 'reboot_hard',
REBOOT: 'reboot',
RELEASE: 'release',
RESUME: 'resume',
STOP: 'stop',
SUSPEND: 'suspend',
TERMINATE_HARD: 'terminate_hard',
TERMINATE: 'terminate',
UNDEPLOY_HARD: 'undeploy_hard',
UNDEPLOY: 'undeploy',
SNAPSHOT_DISK_CREATE: 'snapshot_disk_create',
SNAPSHOT_DISK_RENAME: 'snapshot_disk_rename',
SNAPSHOT_DISK_REVERT: 'snapshot_disk_revert',
SNAPSHOT_DISK_DELETE: 'snapshot_disk_delete',
SNAPSHOT_CREATE: 'snapshot_create',
SNAPSHOT_REVERT: 'snapshot_revert',
SNAPSHOT_DELETE: 'snapshot_delete',
}
/** @type {string[]} Actions that can be scheduled */
export const ROLE_ACTIONS_WITH_SCHEDULE = [
ROLE_ACTIONS.HOLD,
ROLE_ACTIONS.POWEROFF_HARD,
ROLE_ACTIONS.POWEROFF,
ROLE_ACTIONS.REBOOT_HARD,
ROLE_ACTIONS.REBOOT,
ROLE_ACTIONS.RELEASE,
ROLE_ACTIONS.RESUME,
ROLE_ACTIONS.SNAPSHOT_CREATE,
ROLE_ACTIONS.SNAPSHOT_DELETE,
ROLE_ACTIONS.SNAPSHOT_DISK_CREATE,
ROLE_ACTIONS.SNAPSHOT_DISK_DELETE,
ROLE_ACTIONS.SNAPSHOT_DISK_REVERT,
ROLE_ACTIONS.SNAPSHOT_REVERT,
ROLE_ACTIONS.STOP,
ROLE_ACTIONS.SUSPEND,
ROLE_ACTIONS.TERMINATE_HARD,
ROLE_ACTIONS.TERMINATE,
ROLE_ACTIONS.UNDEPLOY_HARD,
ROLE_ACTIONS.UNDEPLOY,
]
/** @enum {string} Log severity levels for service logs */
export const SERVICE_LOG_SEVERITY = {
DEBUG: 'D',
INFO: 'I',
ERROR: 'E',
}

View File

@ -162,6 +162,8 @@ export const RESOURCE_NAMES = {
VM: 'vm',
VN_TEMPLATE: 'network-template',
VNET: 'virtual-network',
SERVICE: 'service',
SERVICE_TEMPLATE: 'service-template',
ZONE: 'zone',
}

View File

@ -60,6 +60,7 @@ module.exports = {
CreateProvider: 'Create Provider',
CreateProvision: 'Create Provision',
CreateVmTemplate: 'Create VM Template',
CreateServiceTemplate: 'Create Service Template',
CurrentGroup: 'Current group: %s',
CurrentOwner: 'Current owner: %s',
Delete: 'Delete',
@ -70,6 +71,7 @@ module.exports = {
DeleteSomething: 'Delete: %s',
DeleteTemplate: 'Delete Template',
Deploy: 'Deploy',
DeployServiceTemplate: 'Deploy Service Template',
Detach: 'Detach',
DetachSomething: 'Detach: %s',
Disable: 'Disable',
@ -158,6 +160,7 @@ module.exports = {
UpdateProvider: 'Update Provider',
UpdateScheduleAction: 'Update schedule action: %s',
UpdateVmTemplate: 'Update VM Template',
UpdateServiceTemplate: 'Update Service Template',
/* questions */
Yes: 'Yes',
@ -343,6 +346,10 @@ module.exports = {
Templates: 'Templates',
VMTemplate: 'VM Template',
VMTemplates: 'VM Templates',
Service: 'Service',
Services: 'Services',
ServiceTemplate: 'Service Template',
ServiceTemplates: 'Service Templates',
/* sections - flow */
ApplicationsTemplates: 'Applications templates',
@ -406,11 +413,6 @@ module.exports = {
Monitoring: 'Monitoring',
EdgeCluster: 'Edge Cluster',
/* flow schema */
Strategy: 'Strategy',
ShutdownAction: 'Shutdown action',
ReadyStatusGate: 'Ready status gate',
/* VM schema */
/* VM schema - remote access */
Vnc: 'VNC',
@ -573,7 +575,6 @@ module.exports = {
EnableHotResize: 'Enable hot resize',
/* VM Template schema - VM Group */
AssociateToVMGroup: 'Associate VM to a VM Group',
Role: 'Role',
/* VM Template schema - vCenter */
vCenterTemplateRef: 'vCenter Template reference',
vCenterClusterRef: 'vCenter Cluster reference',
@ -759,6 +760,17 @@ module.exports = {
The VM Template(s), along with any image referenced by it, will
be unshared with the group's users. Permission changed: GROUP USE`,
/* Service Template schema */
/* Service Template schema - general */
Strategy: 'Strategy',
ShutdownAction: 'Shutdown action',
ReadyStatusGate: 'Ready status gate',
AutomaticDeletion: 'Automatic deletion',
Role: 'Role',
Roles: 'Roles',
Cardinality: 'Cardinality',
Parents: 'Parents',
/* Virtual Network schema - network */
Driver: 'Driver',
IP: 'IP',

View File

@ -0,0 +1,36 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import { useParams, Redirect } from 'react-router-dom'
import ServiceTemplateTabs from 'client/components/Tabs/ServiceTemplate'
/**
* Displays the detail information about a Service Template.
*
* @returns {ReactElement} Service Template detail component.
*/
function ServiceTemplateDetail() {
const { id } = useParams()
if (Number.isNaN(+id)) {
return <Redirect to="/" />
}
return <ServiceTemplateTabs id={id} />
}
export default ServiceTemplateDetail

View File

@ -0,0 +1,131 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement, useState, memo } from 'react'
import PropTypes from 'prop-types'
import { BookmarkEmpty } from 'iconoir-react'
import { Typography, Box, Stack, Chip, IconButton } from '@mui/material'
import { Row } from 'react-table'
import serviceTemplateApi from 'client/features/OneApi/serviceTemplate'
import { ServiceTemplatesTable } from 'client/components/Tables'
import ServiceTemplateTabs from 'client/components/Tabs/ServiceTemplate'
import SplitPane from 'client/components/SplitPane'
import MultipleTags from 'client/components/MultipleTags'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
/**
* Displays a list of Service Templates with a split pane between
* the list and selected row(s).
*
* @returns {ReactElement} Service Templates list and selected row(s)
*/
function ServiceTemplates() {
const [selectedRows, onSelectedRowsChange] = useState(() => [])
const hasSelectedRows = selectedRows?.length > 0
const moreThanOneSelected = selectedRows?.length > 1
const gridTemplateRows = hasSelectedRows ? '1fr auto 1fr' : '1fr'
return (
<SplitPane gridTemplateRows={gridTemplateRows}>
{({ getGridProps, GutterComponent }) => (
<Box {...getGridProps()}>
<ServiceTemplatesTable onSelectedRowsChange={onSelectedRowsChange} />
{hasSelectedRows && (
<>
<GutterComponent direction="row" track={1} />
{moreThanOneSelected ? (
<GroupedTags tags={selectedRows} />
) : (
<InfoTabs
id={selectedRows[0]?.original?.ID}
gotoPage={selectedRows[0]?.gotoPage}
/>
)}
</>
)}
</Box>
)}
</SplitPane>
)
}
/**
* Displays details of a Service Template.
*
* @param {string} id - Service Template id to display
* @param {Function} [gotoPage] - Function to navigate to a page of a Service Template
* @returns {ReactElement} Service Template details
*/
const InfoTabs = memo(({ id, gotoPage }) => {
const template =
serviceTemplateApi.endpoints.getServiceTemplates.useQueryState(undefined, {
selectFromResult: ({ data = [] }) =>
data.find((item) => +item.ID === +id),
})
return (
<Stack overflow="auto">
<Stack direction="row" alignItems="center" gap={1} mb={1}>
<Typography color="text.primary" noWrap>
{`#${id} | ${template.NAME}`}
</Typography>
{gotoPage && (
<IconButton title={Tr(T.LocateOnTable)} onClick={gotoPage}>
<BookmarkEmpty />
</IconButton>
)}
</Stack>
<ServiceTemplateTabs id={id} />
</Stack>
)
})
InfoTabs.propTypes = {
id: PropTypes.string.isRequired,
gotoPage: PropTypes.func,
}
InfoTabs.displayName = 'InfoTabs'
/**
* Displays a list of tags that represent the selected rows.
*
* @param {Row[]} tags - Row(s) to display as tags
* @returns {ReactElement} List of tags
*/
const GroupedTags = memo(({ tags = [] }) => (
<Stack direction="row" flexWrap="wrap" gap={1} alignContent="flex-start">
<MultipleTags
limitTags={10}
tags={tags?.map(({ original, id, toggleRowSelected, gotoPage }) => (
<Chip
key={id}
label={original?.NAME ?? id}
onClick={gotoPage}
onDelete={() => toggleRowSelected(false)}
/>
))}
/>
</Stack>
))
GroupedTags.propTypes = { tags: PropTypes.array }
GroupedTags.displayName = 'GroupedTags'
export default ServiceTemplates

View File

@ -0,0 +1,36 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import { useParams, Redirect } from 'react-router-dom'
import ServiceTabs from 'client/components/Tabs/Service'
/**
* Displays the detail information about a Service.
*
* @returns {ReactElement} Service detail component.
*/
function ServiceDetail() {
const { id } = useParams()
if (Number.isNaN(+id)) {
return <Redirect to="/" />
}
return <ServiceTabs id={id} />
}
export default ServiceDetail

View File

@ -0,0 +1,129 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement, useState, memo } from 'react'
import PropTypes from 'prop-types'
import { BookmarkEmpty } from 'iconoir-react'
import { Typography, Box, Stack, Chip, IconButton } from '@mui/material'
import { Row } from 'react-table'
import serviceApi from 'client/features/OneApi/service'
import { ServicesTable } from 'client/components/Tables'
import ServiceTabs from 'client/components/Tabs/Service'
import SplitPane from 'client/components/SplitPane'
import MultipleTags from 'client/components/MultipleTags'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
/**
* Displays a list of Service with a split pane between
* the list and selected row(s).
*
* @returns {ReactElement} Service list and selected row(s)
*/
function Services() {
const [selectedRows, onSelectedRowsChange] = useState(() => [])
const hasSelectedRows = selectedRows?.length > 0
const moreThanOneSelected = selectedRows?.length > 1
const gridTemplateRows = hasSelectedRows ? '1fr auto 1fr' : '1fr'
return (
<SplitPane gridTemplateRows={gridTemplateRows}>
{({ getGridProps, GutterComponent }) => (
<Box {...getGridProps()}>
<ServicesTable onSelectedRowsChange={onSelectedRowsChange} />
{hasSelectedRows && (
<>
<GutterComponent direction="row" track={1} />
{moreThanOneSelected ? (
<GroupedTags tags={selectedRows} />
) : (
<InfoTabs
id={selectedRows[0]?.original?.ID}
gotoPage={selectedRows[0]?.gotoPage}
/>
)}
</>
)}
</Box>
)}
</SplitPane>
)
}
/**
* Displays details of a Service.
*
* @param {string} id - Service id to display
* @param {Function} [gotoPage] - Function to navigate to a page of a Service
* @returns {ReactElement} Service details
*/
const InfoTabs = memo(({ id, gotoPage }) => {
const template = serviceApi.endpoints.getServices.useQueryState(undefined, {
selectFromResult: ({ data = [] }) => data.find((item) => +item.ID === +id),
})
return (
<Stack overflow="auto">
<Stack direction="row" alignItems="center" gap={1} mb={1}>
<Typography color="text.primary" noWrap>
{`#${id} | ${template.NAME}`}
</Typography>
{gotoPage && (
<IconButton title={Tr(T.LocateOnTable)} onClick={gotoPage}>
<BookmarkEmpty />
</IconButton>
)}
</Stack>
<ServiceTabs id={id} />
</Stack>
)
})
InfoTabs.propTypes = {
id: PropTypes.string.isRequired,
gotoPage: PropTypes.func,
}
InfoTabs.displayName = 'InfoTabs'
/**
* Displays a list of tags that represent the selected rows.
*
* @param {Row[]} tags - Row(s) to display as tags
* @returns {ReactElement} List of tags
*/
const GroupedTags = memo(({ tags = [] }) => (
<Stack direction="row" flexWrap="wrap" gap={1} alignContent="flex-start">
<MultipleTags
limitTags={10}
tags={tags?.map(({ original, id, toggleRowSelected, gotoPage }) => (
<Chip
key={id}
label={original?.NAME ?? id}
onClick={gotoPage}
onDelete={() => toggleRowSelected(false)}
/>
))}
/>
</Stack>
))
GroupedTags.propTypes = { tags: PropTypes.array }
GroupedTags.displayName = 'GroupedTags'
export default Services

View File

@ -20,6 +20,16 @@ import groupApi from 'client/features/OneApi/group'
import { LockLevel, Permission, User, Group } from 'client/constants'
import { xmlToJson } from 'client/models/Helper'
/**
* Checks if the parameters are valid to update the pool store.
*
* @param {Draft} draft - The draft to check
* @param {string} resourceId - The resource ID
* @returns {boolean} - True if the parameters are valid, false otherwise
*/
const isUpdateOnPool = (draft, resourceId) =>
Array.isArray(draft) && resourceId !== undefined
/**
* Update the pool of resources with the new data.
*
@ -31,7 +41,7 @@ import { xmlToJson } from 'client/models/Helper'
export const updateResourceOnPool =
({ id: resourceId, resourceFromQuery }) =>
(draft) => {
if (resourceId !== undefined && Array.isArray(draft)) return
if (!isUpdateOnPool(draft, resourceId)) return
const index = draft.findIndex(({ ID }) => +ID === +resourceId)
index !== -1 && (draft[index] = resourceFromQuery)
@ -47,7 +57,7 @@ export const updateResourceOnPool =
export const removeResourceOnPool =
({ id: resourceId }) =>
(draft) => {
if (resourceId !== undefined && Array.isArray(draft)) return
if (!isUpdateOnPool(draft, resourceId)) return
draft.filter(({ ID }) => +ID !== +resourceId)
}
@ -63,13 +73,13 @@ export const removeResourceOnPool =
export const updateNameOnResource =
({ id: resourceId, name: newName }) =>
(draft) => {
const updatePool = resourceId !== undefined && Array.isArray(draft)
const updatePool = isUpdateOnPool(draft, resourceId)
const resource = updatePool
? draft.find(({ ID }) => +ID === +resourceId)
: draft
if ((updatePool && !resource) || newName !== undefined) return
if ((updatePool && !resource) || newName === undefined) return
resource.NAME = newName
}
@ -79,13 +89,13 @@ export const updateNameOnResource =
*
* @param {string} params - The parameters from query
* @param {string} [params.id] - The id of the resource
* @param {LockLevel} [params.level] - The new lock level
* @param {LockLevel} [params.level] - The new lock level. By default, the lock level is 4.
* @returns {function(Draft):ThunkAction} - Dispatches the action
*/
export const updateLockLevelOnResource =
({ id: resourceId, level = '4' }) =>
(draft) => {
const updatePool = resourceId !== undefined && Array.isArray(draft)
const updatePool = isUpdateOnPool(draft, resourceId)
const resource = updatePool
? draft.find(({ ID }) => +ID === +resourceId)
@ -107,7 +117,7 @@ export const updateLockLevelOnResource =
export const removeLockLevelOnResource =
({ id: resourceId }) =>
(draft) => {
const updatePool = resourceId !== undefined && Array.isArray(draft)
const updatePool = isUpdateOnPool(draft, resourceId)
const resource = updatePool
? draft.find(({ ID }) => +ID === +resourceId)
@ -137,7 +147,7 @@ export const removeLockLevelOnResource =
export const updatePermissionOnResource =
({ id: resourceId, ...permissions }) =>
(draft) => {
const updatePool = resourceId !== undefined && Array.isArray(draft)
const updatePool = isUpdateOnPool(draft, resourceId)
const resource = updatePool
? draft.find(({ ID }) => +ID === +resourceId)
@ -206,7 +216,7 @@ export const updateOwnershipOnResource = (
const { user, group } = selectOwnershipFromState(state, { userId, groupId })
return (draft) => {
const updatePool = resourceId !== undefined && Array.isArray(draft)
const updatePool = isUpdateOnPool(draft, resourceId)
const resource = updatePool
? draft.find(({ ID }) => +ID === +resourceId)
@ -232,6 +242,9 @@ export const updateOwnershipOnResource = (
* - Update type:
* ``0``: Replace the whole template.
* ``1``: Merge new template with the existing one.
* @param {boolean} [params.append]
* - ``true``: Merge new template with the existing one.
* - ``false``: Replace the whole template.
* @param {string} [userTemplateAttribute] - The attribute name of the user template. By default is `USER_TEMPLATE`.
* @returns {function(Draft):ThunkAction} - Dispatches the action
*/
@ -241,7 +254,7 @@ export const updateUserTemplateOnResource =
userTemplateAttribute = 'USER_TEMPLATE'
) =>
(draft) => {
const updatePool = resourceId !== undefined && Array.isArray(draft)
const updatePool = isUpdateOnPool(draft, resourceId)
const newTemplateJson = xmlToJson(xml)
const resource = updatePool
@ -255,3 +268,32 @@ export const updateUserTemplateOnResource =
? newTemplateJson
: { ...resource[userTemplateAttribute], ...newTemplateJson }
}
/**
* Update the template body of a document in the store.
*
* @param {object} params - Request params
* @param {number|string} params.id - The id of the resource
* @param {object} params.template - The new template contents on JSON format
* @param {boolean} [params.append]
* - ``true``: Merge new template with the existing one.
* - ``false``: Replace the whole template.
*
* By default, ``true``.
* @returns {function(Draft):ThunkAction} - Dispatches the action
*/
export const updateTemplateOnDocument =
({ id: resourceId, template, append = true }) =>
(draft) => {
const updatePool = isUpdateOnPool(draft, resourceId)
const resource = updatePool
? draft.find(({ ID }) => +ID === +resourceId)
: draft
if (updatePool && !resource) return
resource.TEMPLATE.BODY = append
? { ...resource.TEMPLATE.BODY, ...template }
: template
}

View File

@ -21,26 +21,26 @@ import { requestConfig, generateKey } from 'client/utils'
import http from 'client/utils/rest'
const ONE_RESOURCES = {
ACL: 'Acl',
APP: 'App',
CLUSTER: 'Cluster',
DATASTORE: 'Datastore',
FILE: 'File',
GROUP: 'Group',
HOST: 'Host',
IMAGE: 'Image',
MARKETPLACE: 'Marketplace',
SECURITYGROUP: 'SecurityGroup',
SYSTEM: 'System',
TEMPLATE: 'Template',
USER: 'User',
VDC: 'Vdc',
VM: 'Vm',
VMGROUP: 'VmGroup',
VNET: 'VNetwork',
VNTEMPLATE: 'NetworkTemplate',
VROUTER: 'VirtualRouter',
ZONE: 'Zone',
ACL: 'ACL',
APP: 'APP',
CLUSTER: 'CLUSTER',
DATASTORE: 'DATASTORE',
FILE: 'FILE',
GROUP: 'GROUP',
HOST: 'HOST',
IMAGE: 'IMAGE',
MARKETPLACE: 'MARKET',
SECURITYGROUP: 'SECGROUP',
SYSTEM: 'SYSTEM',
TEMPLATE: 'TEMPLATE',
USER: 'USER',
VDC: 'VDC',
VM: 'VM',
VMGROUP: 'VMGROUP',
VNET: 'VNET',
VNTEMPLATE: 'VNTEMPLATE',
VROUTER: 'VROUTER',
ZONE: 'ZONE',
}
const ONE_RESOURCES_POOL = Object.entries(ONE_RESOURCES).reduce(
@ -49,10 +49,10 @@ const ONE_RESOURCES_POOL = Object.entries(ONE_RESOURCES).reduce(
)
const DOCUMENT = {
SERVICE: 'applicationService',
SERVICE_TEMPLATE: 'applicationServiceTemplate',
PROVISION: 'provision',
PROVIDER: 'provider',
SERVICE: 'SERVICE',
SERVICE_TEMPLATE: 'SERVICE_TEMPLATE',
PROVISION: 'PROVISION',
PROVIDER: 'PROVIDER',
}
const DOCUMENT_POOL = Object.entries(DOCUMENT).reduce(
@ -61,19 +61,19 @@ const DOCUMENT_POOL = Object.entries(DOCUMENT).reduce(
)
const PROVISION_CONFIG = {
PROVISION_DEFAULTS: 'provisionDefaults',
PROVIDER_CONFIG: 'providerConfig',
PROVISION_DEFAULTS: 'PROVISION_DEFAULTS',
PROVIDER_CONFIG: 'PROVIDER_CONFIG',
}
const PROVISION_RESOURCES = {
CLUSTER: 'provisionCluster',
DATASTORE: 'provisionDatastore',
HOST: 'provisionHost',
TEMPLATE: 'provisionVmTemplate',
IMAGE: 'provisionImage',
NETWORK: 'provisionVNetwork',
VNTEMPLATE: 'provisionNetworkTemplate',
FLOWTEMPLATE: 'provisionFlowTemplate',
CLUSTER: 'PROVISION_CLUSTER',
DATASTORE: 'PROVISION_DATASTORE',
HOST: 'PROVISION_HOST',
TEMPLATE: 'PROVISION_VMTEMPLATE',
IMAGE: 'PROVISION_IMAGE',
NETWORK: 'PROVISION_VNET',
VNTEMPLATE: 'PROVISION_VNTEMPLATE',
FLOWTEMPLATE: 'PROVISION_FLOWTEMPLATE',
}
const oneApi = createApi({

View File

@ -14,7 +14,12 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { Actions, Commands } from 'server/routes/api/oneflow/service/routes'
import { oneApi, DOCUMENT, DOCUMENT_POOL } from 'client/features/OneApi'
import {
updateResourceOnPool,
removeResourceOnPool,
} from 'client/features/OneApi/common'
import { Service } from 'client/constants'
const { SERVICE } = DOCUMENT
@ -39,7 +44,10 @@ const serviceApi = oneApi.injectEndpoints({
providesTags: (services) =>
services
? [
services.map(({ ID }) => ({ type: SERVICE_POOL, id: `${ID}` })),
...services.map(({ ID }) => ({
type: SERVICE_POOL,
id: `${ID}`,
})),
SERVICE_POOL,
]
: [SERVICE_POOL],
@ -61,21 +69,27 @@ const serviceApi = oneApi.injectEndpoints({
},
transformResponse: (data) => data?.DOCUMENT ?? {},
providesTags: (_, __, { id }) => [{ type: SERVICE, id }],
async onQueryStarted({ id }, { dispatch, queryFulfilled }) {
async onQueryStarted(id, { dispatch, queryFulfilled }) {
try {
const { data: queryService } = await queryFulfilled
const { data: resourceFromQuery } = await queryFulfilled
dispatch(
serviceApi.util.updateQueryData(
'getServices',
undefined,
(draft) => {
const index = draft.findIndex(({ ID }) => +ID === +id)
index !== -1 && (draft[index] = queryService)
}
updateResourceOnPool({ id, resourceFromQuery })
)
)
} catch {}
} catch {
// if the query fails, we want to remove the resource from the pool
dispatch(
serviceApi.util.updateQueryData(
'getServices',
undefined,
removeResourceOnPool({ id })
)
)
}
},
}),
}),

View File

@ -14,7 +14,15 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { Actions, Commands } from 'server/routes/api/oneflow/template/routes'
import { oneApi, DOCUMENT, DOCUMENT_POOL } from 'client/features/OneApi'
import {
updateResourceOnPool,
removeResourceOnPool,
updateNameOnResource,
updateOwnershipOnResource,
updateTemplateOnDocument,
} from 'client/features/OneApi/common'
import { ServiceTemplate } from 'client/constants'
const { SERVICE_TEMPLATE } = DOCUMENT
@ -39,7 +47,7 @@ const serviceTemplateApi = oneApi.injectEndpoints({
providesTags: (serviceTemplates) =>
serviceTemplates
? [
serviceTemplates.map(({ ID }) => ({
...serviceTemplates.map(({ ID }) => ({
type: SERVICE_TEMPLATE_POOL,
id: `${ID}`,
})),
@ -64,21 +72,27 @@ const serviceTemplateApi = oneApi.injectEndpoints({
},
transformResponse: (data) => data?.DOCUMENT ?? {},
providesTags: (_, __, { id }) => [{ type: SERVICE_TEMPLATE, id }],
async onQueryStarted({ id }, { dispatch, queryFulfilled }) {
async onQueryStarted(id, { dispatch, queryFulfilled }) {
try {
const { data: queryService } = await queryFulfilled
const { data: resourceFromQuery } = await queryFulfilled
dispatch(
serviceTemplateApi.util.updateQueryData(
'getServiceTemplates',
undefined,
(draft) => {
const index = draft.findIndex(({ ID }) => +ID === +id)
index !== -1 && (draft[index] = queryService)
}
updateResourceOnPool({ id, resourceFromQuery })
)
)
} catch {}
} catch {
// if the query fails, we want to remove the resource from the pool
dispatch(
serviceTemplateApi.util.updateQueryData(
'getServiceTemplates',
undefined,
removeResourceOnPool({ id })
)
)
}
},
}),
createServiceTemplate: builder.mutation({
@ -96,7 +110,7 @@ const serviceTemplateApi = oneApi.injectEndpoints({
return { params, command }
},
providesTags: [SERVICE_TEMPLATE_POOL],
invalidatesTags: [SERVICE_TEMPLATE_POOL],
}),
updateServiceTemplate: builder.mutation({
/**
@ -104,17 +118,51 @@ const serviceTemplateApi = oneApi.injectEndpoints({
*
* @param {object} params - Request params
* @param {string} params.id - Service template id
* @param {object} [params.template] - Service template data
* @param {object} params.template - The new template contents
* @param {boolean} [params.append]
* - ``true``: Merge new template with the existing one.
* - ``false``: Replace the whole template.
*
* By default, ``true``.
* @returns {number} Service template id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.SERVICE_TEMPLATE_UPDATE
query: ({ template = {}, append = true, ...params }) => {
params.action = {
perform: 'update',
params: { template_json: JSON.stringify(template), append },
}
const name = Actions.SERVICE_TEMPLATE_ACTION
const command = { name, ...Commands[name] }
return { params, command }
},
providesTags: (_, __, { id }) => [{ type: SERVICE_TEMPLATE, id }],
invalidatesTags: (_, __, { id }) => [{ type: SERVICE_TEMPLATE, id }],
async onQueryStarted(params, { dispatch, queryFulfilled }) {
try {
const patchVmTemplate = dispatch(
serviceTemplateApi.util.updateQueryData(
'getServiceTemplates',
{ id: params.id },
updateTemplateOnDocument(params)
)
)
const patchVmTemplates = dispatch(
serviceTemplateApi.util.updateQueryData(
'getServiceTemplates',
undefined,
updateTemplateOnDocument(params)
)
)
queryFulfilled.catch(() => {
patchVmTemplate.undo()
patchVmTemplates.undo()
})
} catch {}
},
}),
removeServiceTemplate: builder.mutation({
/**
@ -131,9 +179,9 @@ const serviceTemplateApi = oneApi.injectEndpoints({
return { params, command }
},
providesTags: [SERVICE_TEMPLATE_POOL],
invalidatesTags: [SERVICE_TEMPLATE_POOL],
}),
instantiateServiceTemplate: builder.mutation({
deployServiceTemplate: builder.mutation({
/**
* Perform instantiate action on the service template.
*
@ -159,7 +207,121 @@ const serviceTemplateApi = oneApi.injectEndpoints({
return { params, command }
},
providesTags: [SERVICE_POOL],
invalidatesTags: [SERVICE_POOL],
}),
changeServiceTemplatePermissions: builder.mutation({
/**
* Changes the permission bits of a Service template.
* If set any permission to -1, it's not changed.
*
* @param {object} params - Request parameters
* @param {string} params.id - Service Template id
* @param {string} params.octet - Permissions in octal format
* @returns {number} Service Template id
* @throws Fails when response isn't code 200
*/
query: ({ octet, ...params }) => {
params.action = { perform: 'chmod', params: { octet } }
const name = Actions.SERVICE_TEMPLATE_ACTION
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: SERVICE_TEMPLATE, id }],
}),
changeServiceTemplateOwnership: builder.mutation({
/**
* Changes the ownership bits of a Service template.
* If set to `-1`, the user or group aren't changed.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - Service Template id
* @param {number|'-1'} params.user - The user id
* @param {number|'-1'} params.group - The group id
* @returns {number} Service Template id
* @throws Fails when response isn't code 200
*/
query: ({ user = '-1', group = '-1', ...params }) => {
params.action = {
perform: 'chown',
params: { owner_id: user, group_id: group },
}
const name = Actions.SERVICE_TEMPLATE_ACTION
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: SERVICE_TEMPLATE, id }],
async onQueryStarted(params, { getState, dispatch, queryFulfilled }) {
try {
const patchServiceTemplate = dispatch(
serviceTemplateApi.util.updateQueryData(
'getServiceTemplate',
{ id: params.id },
updateOwnershipOnResource(getState(), params)
)
)
const patchServiceTemplates = dispatch(
serviceTemplateApi.util.updateQueryData(
'getServiceTemplates',
undefined,
updateOwnershipOnResource(getState(), params)
)
)
queryFulfilled.catch(() => {
patchServiceTemplate.undo()
patchServiceTemplates.undo()
})
} catch {}
},
}),
renameServiceTemplate: builder.mutation({
/**
* Renames a Service template.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - Service Template id
* @param {string} params.name - The new name
* @returns {number} Service Template id
* @throws Fails when response isn't code 200
*/
query: ({ name, ...params }) => {
params.action = { perform: 'rename', params: { name } }
const cName = Actions.SERVICE_TEMPLATE_ACTION
const command = { name: cName, ...Commands[cName] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: SERVICE_TEMPLATE, id }],
async onQueryStarted(params, { dispatch, queryFulfilled }) {
try {
const patchServiceTemplate = dispatch(
serviceTemplateApi.util.updateQueryData(
'getServiceTemplate',
{ id: params.id },
updateNameOnResource(params)
)
)
const patchServiceTemplates = dispatch(
serviceTemplateApi.util.updateQueryData(
'getServiceTemplates',
undefined,
updateNameOnResource(params)
)
)
queryFulfilled.catch(() => {
patchServiceTemplate.undo()
patchServiceTemplates.undo()
})
} catch {}
},
}),
}),
})
@ -175,7 +337,10 @@ export const {
useCreateServiceTemplateMutation,
useUpdateServiceTemplateMutation,
useRemoveServiceTemplateMutation,
useInstantiateServiceTemplateMutation,
useDeployServiceTemplateMutation,
useChangeServiceTemplatePermissionsMutation,
useChangeServiceTemplateOwnershipMutation,
useRenameServiceTemplateMutation,
} = serviceTemplateApi
export default serviceTemplateApi

View File

@ -108,7 +108,7 @@ const vmApi = oneApi.injectEndpoints({
},
transformResponse: (data) => data?.VM ?? {},
providesTags: (_, __, { id }) => [{ type: VM, id }],
async onQueryStarted(id, { dispatch, queryFulfilled }) {
async onQueryStarted({ id }, { dispatch, queryFulfilled }) {
try {
const { data: resourceFromQuery } = await queryFulfilled

View File

@ -14,13 +14,17 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { prettyBytes } from 'client/utils'
import { DATASTORE_STATES, DATASTORE_TYPES, STATES } from 'client/constants'
import {
Datastore,
DATASTORE_STATES,
DATASTORE_TYPES,
STATES,
} from 'client/constants'
/**
* Returns the datastore type name.
*
* @param {object} datastore - Datastore
* @param {number} datastore.TYPE - Datastore type
* @param {Datastore} datastore - Datastore
* @returns {DATASTORE_TYPES} - Datastore type object
*/
export const getType = ({ TYPE } = {}) => DATASTORE_TYPES[TYPE]
@ -28,8 +32,7 @@ export const getType = ({ TYPE } = {}) => DATASTORE_TYPES[TYPE]
/**
* Returns information about datastore state.
*
* @param {object} datastore - Datastore
* @param {number} datastore.STATE - Datastore state ID
* @param {Datastore} datastore - Datastore
* @returns {STATES.StateInfo} - Datastore state object
*/
export const getState = ({ STATE = 0 } = {}) => DATASTORE_STATES[STATE]
@ -37,7 +40,7 @@ export const getState = ({ STATE = 0 } = {}) => DATASTORE_STATES[STATE]
/**
* Return the TM_MAD_SYSTEM attribute.
*
* @param {object} datastore - Datastore
* @param {Datastore} datastore - Datastore
* @returns {string[]} - The list of deploy modes available
*/
export const getDeployMode = (datastore = {}) => {
@ -52,9 +55,7 @@ export const getDeployMode = (datastore = {}) => {
/**
* Returns information about datastore capacity.
*
* @param {object} datastore - Datastore
* @param {number} datastore.TOTAL_MB - Total capacity in MB
* @param {number} datastore.USED_MB - Used capacity in MB
* @param {Datastore} datastore - Datastore
* @returns {{
* percentOfUsed: number,
* percentLabel: string
@ -74,8 +75,7 @@ export const getCapacityInfo = ({ TOTAL_MB, USED_MB } = {}) => {
/**
* Returns `true` if Datastore allows to export to Marketplace.
*
* @param {object} props - Datastore ob
* @param {object} props.NAME - Name
* @param {Datastore} datastore - Datastore
* @param {object} oneConfig - One config from redux
* @returns {boolean} - Datastore supports to export
*/

View File

@ -24,6 +24,7 @@ import {
import { camelCase } from 'client/utils'
import {
T,
Permission,
UserInputObject,
USER_INPUT_TYPES,
SERVER_CONFIG,
@ -208,9 +209,9 @@ export const levelLockToString = (level) =>
* Returns the permission numeric code.
*
* @param {string[]} category - Array with Use, Manage and Access permissions.
* @param {('YES'|'NO')} category.0 - `true` if use permission is allowed
* @param {('YES'|'NO')} category.1 - `true` if manage permission is allowed
* @param {('YES'|'NO')} category.2 - `true` if access permission is allowed
* @param {Permission} category.0 - `true` or `1` if use permission is allowed
* @param {Permission} category.1 - `true` or `1` if manage permission is allowed
* @param {Permission} category.2 - `true` or `1` if access permission is allowed
* @returns {number} Permission code number.
*/
const getCategoryValue = ([u, m, a]) =>
@ -222,15 +223,15 @@ const getCategoryValue = ([u, m, a]) =>
* Transform the permission from OpenNebula template to octal format.
*
* @param {object} permissions - Permissions object.
* @param {('YES'|'NO')} permissions.OWNER_U - Owner use permission.
* @param {('YES'|'NO')} permissions.OWNER_M - Owner manage permission.
* @param {('YES'|'NO')} permissions.OWNER_A - Owner access permission.
* @param {('YES'|'NO')} permissions.GROUP_U - Group use permission.
* @param {('YES'|'NO')} permissions.GROUP_M - Group manage permission.
* @param {('YES'|'NO')} permissions.GROUP_A - Group access permission.
* @param {('YES'|'NO')} permissions.OTHER_U - Other use permission.
* @param {('YES'|'NO')} permissions.OTHER_M - Other manage permission.
* @param {('YES'|'NO')} permissions.OTHER_A - Other access permission.
* @param {Permission} permissions.OWNER_U - Owner use
* @param {Permission} permissions.OWNER_M - Owner manage
* @param {Permission} permissions.OWNER_A - Owner access
* @param {Permission} permissions.GROUP_U - Group use
* @param {Permission} permissions.GROUP_M - Group manage
* @param {Permission} permissions.GROUP_A - Group access
* @param {Permission} permissions.OTHER_U - Other use
* @param {Permission} permissions.OTHER_M - Other manage
* @param {Permission} permissions.OTHER_A - Other access
* @returns {string} - Permissions in octal format.
*/
export const permissionsToOctal = (permissions) => {

View File

@ -0,0 +1,26 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { Service, SERVICE_STATES, STATES } from 'client/constants'
/**
* Returns information about Service state.
*
* @param {Service} service - Service
* @returns {STATES.StateInfo} - Service state object
*/
export const getState = ({ TEMPLATE = {} } = {}) =>
SERVICE_STATES[TEMPLATE?.BODY?.state]

View File

@ -48,9 +48,21 @@ export const sentenceCase = (input) => {
*
* @param {string} input - String to transform
* @returns {string} string
* @example //=> "testString"
* @example // "test-string" => "testString"
* @example // "test_string" => "testString"
*/
export const camelCase = (input) =>
input
.toLowerCase()
.replace(/([-_\s][a-z])/gi, ($1) => $1.toUpperCase().replace(/[-_\s]/g, ''))
/**
* Transform into a snake case string.
*
* @param {string} input - String to transform
* @returns {string} string
* @example // "test-string" => "test_string"
* @example // "testString" => "test_string"
* @example // "TESTString" => "test_string"
*/
export const toSnakeCase = (input) => sentenceCase(input).replace(/\s/g, '_')

View File

@ -14,31 +14,6 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
const action = {
id: '/Action',
type: 'object',
properties: {
action: {
type: 'object',
properties: {
perform: {
type: 'string',
required: true,
},
params: {
type: 'object',
properties: {
merge_template: {
type: 'object',
required: false,
},
},
},
},
},
},
}
const role = {
id: '/Role',
type: 'object',
@ -216,10 +191,6 @@ const service = {
},
}
const schemas = {
action,
role,
service,
}
const schemas = { role, service }
module.exports = schemas

View File

@ -47,7 +47,7 @@ module.exports = {
Actions,
Commands: {
[SERVICE_SHOW]: {
path: `${basepath}/:id`,
path: `${basepath}/:id?`,
httpMethod: GET,
auth: true,
params: {

View File

@ -15,7 +15,7 @@
* ------------------------------------------------------------------------- */
const { Validator } = require('jsonschema')
const { role, service, action } = require('server/routes/api/oneflow/schemas')
const { role, service } = require('server/routes/api/oneflow/schemas')
const {
oneFlowConnection,
returnSchemaError,
@ -262,32 +262,22 @@ const serviceTemplateAction = (
userData = {}
) => {
const { user, password } = userData
if (params && params.id && params.template && user && password) {
const v = new Validator()
const template = parsePostData(params.template)
const valSchema = v.validate(template, action)
if (valSchema.valid) {
const config = {
method: POST,
path: '/service_template/{0}/action',
user,
password,
request: params.id,
post: template,
}
oneFlowConnection(
config,
(data) => success(next, res, data),
(data) => error(next, res, data)
)
} else {
res.locals.httpCode = httpResponse(
internalServerError,
'',
`invalid schema ${returnSchemaError(valSchema.errors)}`
)
next()
if (params && params.id && params.action && user && password) {
const config = {
method: POST,
path: '/service_template/{0}/action',
user,
password,
request: params.id,
post: { action: parsePostData(params.action) },
}
oneFlowConnection(
config,
(data) => success(next, res, data),
(data) => error(next, res, data)
)
} else {
res.locals.httpCode = httpResponse(
methodNotAllowed,

View File

@ -58,7 +58,7 @@ module.exports = {
id: {
from: resource,
},
template: {
action: {
from: postBody,
},
},

View File

@ -62,6 +62,7 @@ const oneFlowConnection = (
const optionMethod = method || GET
const optionPath = path || '/'
const optionAuth = btoa(`${user || ''}:${password || ''}`)
const options = {
method: optionMethod,
baseURL: appConfig.oneflow_server || defaultOneFlowServer,
@ -69,39 +70,25 @@ const oneFlowConnection = (
headers: {
Authorization: `Basic ${optionAuth}`,
},
validateStatus: (status) => status,
validateStatus: (status) => status >= 200 && status < 400,
}
if (post) {
options.data = post
}
if (post) options.data = post
axios(options)
.then((response) => {
if (response && response.statusText) {
if (response.status >= 200 && response.status < 400) {
if (response.data) {
return response.data
}
if (
response.config.method &&
response.config.method.toUpperCase() === DELETE
) {
return Array.isArray(request)
? parseToNumber(request[0])
: parseToNumber(request)
}
} else if (response.data) {
throw Error(response.data)
}
if (!response.statusText) throw Error(response.statusText)
if (`${response.config.method}`.toUpperCase() === DELETE) {
return Array.isArray(request)
? parseToNumber(request[0])
: parseToNumber(request)
}
throw Error(response.statusText)
})
.then((data) => {
success(data)
})
.catch((e) => {
error(e)
return response.data
})
.then(success)
.catch(error)
}
const functionRoutes = {