diff --git a/src/fireedge/src/client/apps/sunstone/routesOne.js b/src/fireedge/src/client/apps/sunstone/routesOne.js index 603338767ae..8d96526d1f8 100644 --- a/src/fireedge/src/client/apps/sunstone/routesOne.js +++ b/src/fireedge/src/client/apps/sunstone/routesOne.js @@ -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, + }, ], }, { diff --git a/src/fireedge/src/client/components/Cards/ServiceCard.js b/src/fireedge/src/client/components/Cards/ServiceCard.js new file mode 100644 index 00000000000..235ee9df37f --- /dev/null +++ b/src/fireedge/src/client/components/Cards/ServiceCard.js @@ -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 ( +
+
+
+ + + {NAME} + + + + + +
+
+ {`#${ID}`} + + + +
+
+ {actions &&
{actions}
} +
+ ) + } +) + +ServiceCard.propTypes = { + service: PropTypes.object, + rootProps: PropTypes.shape({ + className: PropTypes.string, + }), + onDeleteLabel: PropTypes.func, + actions: PropTypes.any, +} + +ServiceCard.displayName = 'ServiceCard' + +export default ServiceCard diff --git a/src/fireedge/src/client/components/Cards/ServiceTemplateCard.js b/src/fireedge/src/client/components/Cards/ServiceTemplateCard.js new file mode 100644 index 00000000000..fbedf928ad1 --- /dev/null +++ b/src/fireedge/src/client/components/Cards/ServiceTemplateCard.js @@ -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 ( +
+
+
+ + {NAME} + + + + +
+
+ {`#${ID}`} + + + + + + {numberOfNetworks} + + + + {numberOfRoles} + +
+
+ {actions &&
{actions}
} +
+ ) + } +) + +ServiceTemplateCard.propTypes = { + template: PropTypes.object, + rootProps: PropTypes.shape({ + className: PropTypes.string, + }), + onDeleteLabel: PropTypes.func, + actions: PropTypes.any, +} + +ServiceTemplateCard.displayName = 'ServiceTemplateCard' + +export default ServiceTemplateCard diff --git a/src/fireedge/src/client/components/Cards/index.js b/src/fireedge/src/client/components/Cards/index.js index 5926326285a..29752ff131a 100644 --- a/src/fireedge/src/client/components/Cards/index.js +++ b/src/fireedge/src/client/components/Cards/index.js @@ -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, diff --git a/src/fireedge/src/client/components/Charts/SingleBar.js b/src/fireedge/src/client/components/Charts/SingleBar.js index 1a9f669682c..2c92859f4ea 100644 --- a/src/fireedge/src/client/components/Charts/SingleBar.js +++ b/src/fireedge/src/client/components/Charts/SingleBar.js @@ -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 ( { placement="top" title={`${label}: ${value}`} > -
+
) })} diff --git a/src/fireedge/src/client/components/Tables/Enhanced/index.js b/src/fireedge/src/client/components/Tables/Enhanced/index.js index 1dfc409846a..06637f07738 100644 --- a/src/fireedge/src/client/components/Tables/Enhanced/index.js +++ b/src/fireedge/src/client/components/Tables/Enhanced/index.js @@ -67,6 +67,7 @@ const EnhancedTable = ({ classes = {}, rootProps = {}, searchProps = {}, + noDataMessage, }) => { const styles = EnhancedTableStyles() @@ -208,12 +209,15 @@ const EnhancedTable = ({
{/* NO DATA MESSAGE */} - {!isLoading && !isUninitialized && page?.length === 0 && ( - - - - - )} + {!isLoading && + !isUninitialized && + page?.length === 0 && + (noDataMessage || ( + + + + + ))} {/* 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' diff --git a/src/fireedge/src/client/components/Tables/ServiceTemplates/columns.js b/src/fireedge/src/client/components/Tables/ServiceTemplates/columns.js new file mode 100644 index 00000000000..e26b2a318db --- /dev/null +++ b/src/fireedge/src/client/components/Tables/ServiceTemplates/columns.js @@ -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 diff --git a/src/fireedge/src/client/components/Tables/ServiceTemplates/index.js b/src/fireedge/src/client/components/Tables/ServiceTemplates/index.js new file mode 100644 index 00000000000..4b2bdaaca70 --- /dev/null +++ b/src/fireedge/src/client/components/Tables/ServiceTemplates/index.js @@ -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 ( + data, [data])} + rootProps={rootProps} + searchProps={searchProps} + refetch={refetch} + isLoading={isFetching} + getRowId={(row) => String(row.ID)} + RowComponent={ServiceTemplateRow} + noDataMessage={ + error?.status === 500 && ( + + + + ) + } + {...rest} + /> + ) +} + +ServiceTemplatesTable.propTypes = { ...EnhancedTable.propTypes } +ServiceTemplatesTable.displayName = 'ServiceTemplatesTable' + +export default ServiceTemplatesTable diff --git a/src/fireedge/src/client/components/Tables/ServiceTemplates/row.js b/src/fireedge/src/client/components/Tables/ServiceTemplates/row.js new file mode 100644 index 00000000000..9d1e2ad7a18 --- /dev/null +++ b/src/fireedge/src/client/components/Tables/ServiceTemplates/row.js @@ -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 ( + + ) + }, + (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 diff --git a/src/fireedge/src/client/components/Tables/Services/columns.js b/src/fireedge/src/client/components/Tables/Services/columns.js new file mode 100644 index 00000000000..49d612cce5a --- /dev/null +++ b/src/fireedge/src/client/components/Tables/Services/columns.js @@ -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 diff --git a/src/fireedge/src/client/components/Tables/Services/index.js b/src/fireedge/src/client/components/Tables/Services/index.js new file mode 100644 index 00000000000..f5553e162b6 --- /dev/null +++ b/src/fireedge/src/client/components/Tables/Services/index.js @@ -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 ( + data, [data])} + rootProps={rootProps} + searchProps={searchProps} + refetch={refetch} + isLoading={isFetching} + getRowId={(row) => String(row.ID)} + RowComponent={ServiceRow} + noDataMessage={ + error?.status === 500 && ( + + + + ) + } + {...rest} + /> + ) +} + +ServicesTable.propTypes = { ...EnhancedTable.propTypes } +ServicesTable.displayName = 'ServicesTable' + +export default ServicesTable diff --git a/src/fireedge/src/client/components/Tables/Services/row.js b/src/fireedge/src/client/components/Tables/Services/row.js new file mode 100644 index 00000000000..f52099f0173 --- /dev/null +++ b/src/fireedge/src/client/components/Tables/Services/row.js @@ -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 + }, + (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 diff --git a/src/fireedge/src/client/components/Tables/index.js b/src/fireedge/src/client/components/Tables/index.js index 5537d8fd7be..064407bedb3 100644 --- a/src/fireedge/src/client/components/Tables/index.js +++ b/src/fireedge/src/client/components/Tables/index.js @@ -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, diff --git a/src/fireedge/src/client/components/Tabs/Service/Actions.js b/src/fireedge/src/client/components/Tabs/Service/Actions.js new file mode 100644 index 00000000000..5770b81fa5a --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Service/Actions.js @@ -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 ( + <> + + {service?.NAME} + {/* TODO: scheduler actions & form */} + + + ) +} + +SchedulingTab.propTypes = { + tabProps: PropTypes.object, + id: PropTypes.string, +} + +export default SchedulingTab diff --git a/src/fireedge/src/client/components/Tabs/Service/Info/index.js b/src/fireedge/src/client/components/Tabs/Service/Info/index.js new file mode 100644 index 00000000000..3da6965f51f --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Service/Info/index.js @@ -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 ( + + {informationPanel?.enabled && ( + + )} + {permissionsPanel?.enabled && ( + + )} + {ownershipPanel?.enabled && ( + + )} + + ) +} + +ServiceInfoTab.propTypes = { + tabProps: PropTypes.object, + id: PropTypes.string, +} + +ServiceInfoTab.displayName = 'ServiceInfoTab' + +export default ServiceInfoTab diff --git a/src/fireedge/src/client/components/Tabs/Service/Info/information.js b/src/fireedge/src/client/components/Tabs/Service/Info/information.js new file mode 100644 index 00000000000..74f3f967ed9 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Service/Info/information.js @@ -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: ( + + + + + ), + }, + { + 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 ( + + ) +} + +InformationPanel.displayName = 'InformationPanel' + +InformationPanel.propTypes = { + service: PropTypes.object, + actions: PropTypes.arrayOf(PropTypes.string), +} + +export default InformationPanel diff --git a/src/fireedge/src/client/components/Tabs/Service/Log.js b/src/fireedge/src/client/components/Tabs/Service/Log.js new file mode 100644 index 00000000000..9f0328cdd8b --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Service/Log.js @@ -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 ( + + {log?.map(({ severity, message, timestamp } = {}) => { + const time = timeFromMilliseconds(+timestamp) + const isError = severity === SERVICE_LOG_SEVERITY.ERROR + + return ( + + {`${time.toFormat('ff')} [${severity}] ${message}`} + + ) + })} + + ) +} + +LogTab.propTypes = { tabProps: PropTypes.object, id: PropTypes.string } +LogTab.displayName = 'RolesTab' + +export default LogTab diff --git a/src/fireedge/src/client/components/Tabs/Service/Roles.js b/src/fireedge/src/client/components/Tabs/Service/Roles.js new file mode 100644 index 00000000000..a11986f2ab9 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Service/Roles.js @@ -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 ( + + {COLUMNS.map((col) => ( + + + + ))} + {roles.map((role, idx) => ( + *:not(span)': { bgcolor: 'action.hover' } }} + > + + + ))} + + ) +} + +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 ( + <> + + {name} + + + {cardinality} + + {isLoading ? ( + + ) : ( + + {`#${template?.ID} ${template?.NAME}`} + + )} + + {parents?.join?.()} + + + ) +}) + +RoleComponent.propTypes = { role: PropTypes.object } +RoleComponent.displayName = 'RoleComponent' + +export default RolesTab diff --git a/src/fireedge/src/client/components/Tabs/Service/index.js b/src/fireedge/src/client/components/Tabs/Service/index.js new file mode 100644 index 00000000000..82d95b1a828 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Service/index.js @@ -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 ( + + {error.data} + + ) + } + + return isLoading ? ( + + ) : ( + + ) +}) + +ServiceTabs.propTypes = { id: PropTypes.string.isRequired } +ServiceTabs.displayName = 'ServiceTabs' + +export default ServiceTabs diff --git a/src/fireedge/src/client/components/Tabs/ServiceTemplate/Info/index.js b/src/fireedge/src/client/components/Tabs/ServiceTemplate/Info/index.js new file mode 100644 index 00000000000..ed60e983c01 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/ServiceTemplate/Info/index.js @@ -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 ( + + {informationPanel?.enabled && ( + + )} + {permissionsPanel?.enabled && ( + + )} + {ownershipPanel?.enabled && ( + + )} + + ) +} + +ServiceTemplateInfoTab.propTypes = { + tabProps: PropTypes.object, + id: PropTypes.string, +} + +ServiceTemplateInfoTab.displayName = 'ServiceTemplateInfoTab' + +export default ServiceTemplateInfoTab diff --git a/src/fireedge/src/client/components/Tabs/ServiceTemplate/Info/information.js b/src/fireedge/src/client/components/Tabs/ServiceTemplate/Info/information.js new file mode 100644 index 00000000000..76fbd071497 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/ServiceTemplate/Info/information.js @@ -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 ( + + ) +} + +InformationPanel.displayName = 'InformationPanel' + +InformationPanel.propTypes = { + actions: PropTypes.arrayOf(PropTypes.string), + template: PropTypes.object, +} + +export default InformationPanel diff --git a/src/fireedge/src/client/components/Tabs/ServiceTemplate/Roles.js b/src/fireedge/src/client/components/Tabs/ServiceTemplate/Roles.js new file mode 100644 index 00000000000..59758260771 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/ServiceTemplate/Roles.js @@ -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 ( + + {COLUMNS.map((col) => ( + + + + ))} + {roles.map((role, idx) => ( + *:not(span)': { bgcolor: 'action.hover' } }} + > + + + ))} + + ) +} + +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 ( + <> + + {name} + + + {cardinality} + + {isLoading ? ( + + ) : ( + + {`#${template?.ID} ${template?.NAME}`} + + )} + + {parents?.join?.()} + + + ) +}) + +RoleComponent.propTypes = { role: PropTypes.object } +RoleComponent.displayName = 'RoleComponent' + +export default RolesTab diff --git a/src/fireedge/src/client/components/Tabs/ServiceTemplate/Template.js b/src/fireedge/src/client/components/Tabs/ServiceTemplate/Template.js new file mode 100644 index 00000000000..dc5f76ab02e --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/ServiceTemplate/Template.js @@ -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 ( + + + + + {JSON.stringify(template?.TEMPLATE?.BODY ?? {}, null, 2)} + + + + + ) +} + +TemplateTab.propTypes = { + tabProps: PropTypes.object, + id: PropTypes.string, +} + +TemplateTab.displayName = 'TemplateTab' + +export default TemplateTab diff --git a/src/fireedge/src/client/components/Tabs/ServiceTemplate/index.js b/src/fireedge/src/client/components/Tabs/ServiceTemplate/index.js new file mode 100644 index 00000000000..4e9cb643644 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/ServiceTemplate/index.js @@ -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 ( + + {error.data} + + ) + } + + return isLoading ? ( + + ) : ( + + ) +}) + +ServiceTemplateTabs.propTypes = { id: PropTypes.string.isRequired } +ServiceTemplateTabs.displayName = 'ServiceTemplateTabs' + +export default ServiceTemplateTabs diff --git a/src/fireedge/src/client/constants/flow.js b/src/fireedge/src/client/constants/flow.js index 8bee91834e1..4f3b03b53dc 100644 --- a/src/fireedge/src/client/constants/flow.js +++ b/src/fireedge/src/client/constants/flow.js @@ -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', }, { + // 6 + name: STATES.FAILED_UNDEPLOYING, + color: COLOR.error.dark, + meaning: 'An error occurred while undeploying the VMs', + }, + { + // 7 name: STATES.FAILED_DEPLOYING, - color: '', + color: COLOR.error.dark, meaning: 'An error occurred while deploying the VMs', }, { - name: STATES.FAILED_UNDEPLOYING, - color: '', - meaning: 'An error occurred while undeploying 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', +} diff --git a/src/fireedge/src/client/constants/index.js b/src/fireedge/src/client/constants/index.js index 58cea46aa9b..c3053298317 100644 --- a/src/fireedge/src/client/constants/index.js +++ b/src/fireedge/src/client/constants/index.js @@ -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', } diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js index ad75c177c23..d5485b780c7 100644 --- a/src/fireedge/src/client/constants/translates.js +++ b/src/fireedge/src/client/constants/translates.js @@ -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', diff --git a/src/fireedge/src/client/containers/ServiceTemplates/Detail.js b/src/fireedge/src/client/containers/ServiceTemplates/Detail.js new file mode 100644 index 00000000000..476eeb7c3bc --- /dev/null +++ b/src/fireedge/src/client/containers/ServiceTemplates/Detail.js @@ -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 + } + + return +} + +export default ServiceTemplateDetail diff --git a/src/fireedge/src/client/containers/ServiceTemplates/index.js b/src/fireedge/src/client/containers/ServiceTemplates/index.js new file mode 100644 index 00000000000..6393cbdca9a --- /dev/null +++ b/src/fireedge/src/client/containers/ServiceTemplates/index.js @@ -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 ( + + {({ getGridProps, GutterComponent }) => ( + + + + {hasSelectedRows && ( + <> + + {moreThanOneSelected ? ( + + ) : ( + + )} + + )} + + )} + + ) +} + +/** + * 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 ( + + + + {`#${id} | ${template.NAME}`} + + {gotoPage && ( + + + + )} + + + + ) +}) + +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 = [] }) => ( + + ( + toggleRowSelected(false)} + /> + ))} + /> + +)) + +GroupedTags.propTypes = { tags: PropTypes.array } +GroupedTags.displayName = 'GroupedTags' + +export default ServiceTemplates diff --git a/src/fireedge/src/client/containers/Services/Detail.js b/src/fireedge/src/client/containers/Services/Detail.js new file mode 100644 index 00000000000..55336097fb8 --- /dev/null +++ b/src/fireedge/src/client/containers/Services/Detail.js @@ -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 + } + + return +} + +export default ServiceDetail diff --git a/src/fireedge/src/client/containers/Services/index.js b/src/fireedge/src/client/containers/Services/index.js new file mode 100644 index 00000000000..fb73f74de65 --- /dev/null +++ b/src/fireedge/src/client/containers/Services/index.js @@ -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 ( + + {({ getGridProps, GutterComponent }) => ( + + + + {hasSelectedRows && ( + <> + + {moreThanOneSelected ? ( + + ) : ( + + )} + + )} + + )} + + ) +} + +/** + * 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 ( + + + + {`#${id} | ${template.NAME}`} + + {gotoPage && ( + + + + )} + + + + ) +}) + +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 = [] }) => ( + + ( + toggleRowSelected(false)} + /> + ))} + /> + +)) + +GroupedTags.propTypes = { tags: PropTypes.array } +GroupedTags.displayName = 'GroupedTags' + +export default Services diff --git a/src/fireedge/src/client/features/OneApi/common.js b/src/fireedge/src/client/features/OneApi/common.js index 4ff6fd92131..100cd2cb6bd 100644 --- a/src/fireedge/src/client/features/OneApi/common.js +++ b/src/fireedge/src/client/features/OneApi/common.js @@ -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 + } diff --git a/src/fireedge/src/client/features/OneApi/index.js b/src/fireedge/src/client/features/OneApi/index.js index 08fa55863a6..058194a3122 100644 --- a/src/fireedge/src/client/features/OneApi/index.js +++ b/src/fireedge/src/client/features/OneApi/index.js @@ -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({ diff --git a/src/fireedge/src/client/features/OneApi/service.js b/src/fireedge/src/client/features/OneApi/service.js index cc980531e99..6cfb1450158 100644 --- a/src/fireedge/src/client/features/OneApi/service.js +++ b/src/fireedge/src/client/features/OneApi/service.js @@ -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 { + // if the query fails, we want to remove the resource from the pool + dispatch( + serviceApi.util.updateQueryData( + 'getServices', + undefined, + removeResourceOnPool({ id }) ) ) - } catch {} + } }, }), }), diff --git a/src/fireedge/src/client/features/OneApi/serviceTemplate.js b/src/fireedge/src/client/features/OneApi/serviceTemplate.js index aa1e4f042ab..7bc90b6c4dc 100644 --- a/src/fireedge/src/client/features/OneApi/serviceTemplate.js +++ b/src/fireedge/src/client/features/OneApi/serviceTemplate.js @@ -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 diff --git a/src/fireedge/src/client/features/OneApi/vm.js b/src/fireedge/src/client/features/OneApi/vm.js index 9654409806d..8d14e60237f 100644 --- a/src/fireedge/src/client/features/OneApi/vm.js +++ b/src/fireedge/src/client/features/OneApi/vm.js @@ -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 diff --git a/src/fireedge/src/client/models/Datastore.js b/src/fireedge/src/client/models/Datastore.js index 70f4d11bda3..3ba9e3f52dd 100644 --- a/src/fireedge/src/client/models/Datastore.js +++ b/src/fireedge/src/client/models/Datastore.js @@ -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 */ diff --git a/src/fireedge/src/client/models/Helper.js b/src/fireedge/src/client/models/Helper.js index 157a5b18ad4..ceec4bee3f7 100644 --- a/src/fireedge/src/client/models/Helper.js +++ b/src/fireedge/src/client/models/Helper.js @@ -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) => { diff --git a/src/fireedge/src/client/models/Service.js b/src/fireedge/src/client/models/Service.js new file mode 100644 index 00000000000..12a7f0548d1 --- /dev/null +++ b/src/fireedge/src/client/models/Service.js @@ -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] diff --git a/src/fireedge/src/client/utils/string.js b/src/fireedge/src/client/utils/string.js index 5638cdfc069..da42810dcdb 100644 --- a/src/fireedge/src/client/utils/string.js +++ b/src/fireedge/src/client/utils/string.js @@ -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, '_') diff --git a/src/fireedge/src/server/routes/api/oneflow/schemas.js b/src/fireedge/src/server/routes/api/oneflow/schemas.js index d7929df9a64..e5cd00d27b9 100644 --- a/src/fireedge/src/server/routes/api/oneflow/schemas.js +++ b/src/fireedge/src/server/routes/api/oneflow/schemas.js @@ -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 diff --git a/src/fireedge/src/server/routes/api/oneflow/service/routes.js b/src/fireedge/src/server/routes/api/oneflow/service/routes.js index 2518cbc6908..f4d28616787 100644 --- a/src/fireedge/src/server/routes/api/oneflow/service/routes.js +++ b/src/fireedge/src/server/routes/api/oneflow/service/routes.js @@ -47,7 +47,7 @@ module.exports = { Actions, Commands: { [SERVICE_SHOW]: { - path: `${basepath}/:id`, + path: `${basepath}/:id?`, httpMethod: GET, auth: true, params: { diff --git a/src/fireedge/src/server/routes/api/oneflow/template/functions.js b/src/fireedge/src/server/routes/api/oneflow/template/functions.js index 84b25ae8a31..b7d1fbd8d84 100644 --- a/src/fireedge/src/server/routes/api/oneflow/template/functions.js +++ b/src/fireedge/src/server/routes/api/oneflow/template/functions.js @@ -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, diff --git a/src/fireedge/src/server/routes/api/oneflow/template/routes.js b/src/fireedge/src/server/routes/api/oneflow/template/routes.js index 1f72f3e9fc5..55247adcd0b 100644 --- a/src/fireedge/src/server/routes/api/oneflow/template/routes.js +++ b/src/fireedge/src/server/routes/api/oneflow/template/routes.js @@ -58,7 +58,7 @@ module.exports = { id: { from: resource, }, - template: { + action: { from: postBody, }, }, diff --git a/src/fireedge/src/server/routes/api/oneflow/utils.js b/src/fireedge/src/server/routes/api/oneflow/utils.js index 8db1284fcf8..236b19d0765 100644 --- a/src/fireedge/src/server/routes/api/oneflow/utils.js +++ b/src/fireedge/src/server/routes/api/oneflow/utils.js @@ -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 = {