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

F #5903: Security Groups tab Sunstone (#2289)

This commit is contained in:
Jorge Miguel Lobo Escalona 2022-09-19 18:40:29 +02:00 committed by GitHub
parent 9b8487ec04
commit e31c5deec0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 2153 additions and 102 deletions

View File

@ -37,6 +37,7 @@ import {
Home as SystemIcon,
User as UserIcon,
Group as GroupIcon,
HistoricShield as SecurityGroupIcon,
} from 'iconoir-react'
import loadable from '@loadable/component'
@ -104,6 +105,18 @@ const Files = loadable(() => import('client/containers/Files'), {
const CreateFiles = loadable(() => import('client/containers/Files/Create'), {
ssr: false,
})
const SecurityGroups = loadable(
() => import('client/containers/SecurityGroups'),
{
ssr: false,
}
)
const CreateSecurityGroups = loadable(
() => import('client/containers/SecurityGroups/Create'),
{
ssr: false,
}
)
const CreateImages = loadable(() => import('client/containers/Images/Create'), {
ssr: false,
})
@ -129,6 +142,10 @@ const VirtualNetworks = loadable(
() => import('client/containers/VirtualNetworks'),
{ ssr: false }
)
const VirtualNetworksDetail = loadable(
() => import('client/containers/VirtualNetworks/Detail'),
{ ssr: false }
)
const CreateVirtualNetwork = loadable(
() => import('client/containers/VirtualNetworks/Create'),
{ ssr: false }
@ -138,7 +155,6 @@ const VNetworkTemplates = loadable(
{ ssr: false }
)
// const NetworkTopologies = loadable(() => import('client/containers/NetworkTopologies'), { ssr: false })
// const SecurityGroups = loadable(() => import('client/containers/SecurityGroups'), { ssr: false })
const Clusters = loadable(() => import('client/containers/Clusters'), {
ssr: false,
@ -236,6 +252,7 @@ export const PATH = {
SEC_GROUPS: {
LIST: `/${RESOURCE_NAMES.SEC_GROUP}`,
DETAIL: `/${RESOURCE_NAMES.SEC_GROUP}/:id`,
CREATE: `/${RESOURCE_NAMES.SEC_GROUP}/create`,
},
},
INFRASTRUCTURE: {
@ -434,6 +451,12 @@ const ENDPOINTS = [
title: T.Networks,
icon: NetworksIcon,
routes: [
{
title: T.VirtualNetworks,
description: (params) => `#${params?.id}`,
path: PATH.NETWORK.VNETS.DETAIL,
Component: VirtualNetworksDetail,
},
{
title: T.VirtualNetworks,
path: PATH.NETWORK.VNETS.LIST,
@ -458,6 +481,18 @@ const ENDPOINTS = [
path: PATH.NETWORK.VNETS.CREATE,
Component: CreateVirtualNetwork,
},
{
title: T.SecurityGroups,
path: PATH.NETWORK.SEC_GROUPS.LIST,
sidebar: true,
icon: SecurityGroupIcon,
Component: SecurityGroups,
},
{
title: T.CreateSecurityGroup,
path: PATH.NETWORK.SEC_GROUPS.CREATE,
Component: CreateSecurityGroups,
},
],
},
{

View File

@ -35,7 +35,8 @@ import MultipleTags from 'client/components/MultipleTags'
import { Translate } from 'client/components/HOC'
import { stringToBoolean } from 'client/models/Helper'
import { groupBy } from 'client/utils'
import { T, Nic, NicAlias, PrettySecurityGroupRule } from 'client/constants'
import { T, Nic, NicAlias } from 'client/constants'
import { SecurityGroupRules } from 'client/components/Tabs/Common/RulesSecGroups'
const NicCard = memo(
/**
@ -210,92 +211,4 @@ NicCard.propTypes = {
NicCard.displayName = 'NicCard'
const SecurityGroupRules = memo(({ parentKey, id, actions, rules }) => {
const classes = rowStyles()
const COLUMNS = useMemo(
() => [T.Protocol, T.Type, T.Range, T.Network, T.IcmpType],
[]
)
const name = rules?.[0]?.NAME ?? 'default'
return (
<>
<Stack direction="row" spacing={1} alignItems="center">
<Typography
noWrap
component="span"
variant="subtitle1"
data-cy={`${parentKey}-rule-name`}
>
{`#${id} ${name}`}
</Typography>
{!!actions && <div className={classes.actions}>{actions}</div>}
</Stack>
<Box display="grid" gridTemplateColumns="repeat(5, 1fr)" gap="0.5em">
{COLUMNS.map((col) => (
<Typography
key={`${parentKey}-${col}`}
noWrap
component="span"
variant="subtitle2"
>
<Translate word={col} />
</Typography>
))}
{rules.map((rule) => (
<SecurityGroupRule
key={`${parentKey}-rule-${rule.RULE_TYPE}`}
data-cy={`${parentKey}-rule-${rule.RULE_TYPE}`}
rule={rule}
/>
))}
</Box>
</>
)
})
SecurityGroupRules.propTypes = {
parentKey: PropTypes.string,
id: PropTypes.string,
rules: PropTypes.array,
actions: PropTypes.node,
}
SecurityGroupRules.displayName = 'SecurityGroupRule'
const SecurityGroupRule = memo(({ rule, 'data-cy': parentCy }) => {
/** @type {PrettySecurityGroupRule} */
const { PROTOCOL, RULE_TYPE, ICMP_TYPE, RANGE, NETWORK_ID } = rule
return (
<>
{[
{ text: PROTOCOL, dataCy: 'protocol' },
{ text: RULE_TYPE, dataCy: 'ruletype' },
{ text: RANGE, dataCy: 'range' },
{ text: NETWORK_ID, dataCy: 'networkid' },
{ text: ICMP_TYPE, dataCy: 'icmp-type' },
].map(({ text, dataCy }) => (
<Typography
noWrap
key={`${parentCy}-${dataCy}`}
data-cy={`${parentCy}-${dataCy}`.toLowerCase()}
variant="subtitle2"
>
{text}
</Typography>
))}
</>
)
})
SecurityGroupRule.propTypes = {
rule: PropTypes.object,
'data-cy': PropTypes.string,
}
SecurityGroupRule.displayName = 'SecurityGroupRule'
export default NicCard

View File

@ -0,0 +1,24 @@
/* ------------------------------------------------------------------------- *
* 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 { createForm } from 'client/utils'
import {
SCHEMA,
FIELDS,
} from 'client/components/Forms/SecurityGroups/CloneForm/schema'
const cloneSecGroupForm = createForm(SCHEMA, FIELDS)
export default cloneSecGroupForm

View File

@ -0,0 +1,52 @@
/* ------------------------------------------------------------------------- *
* 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 { string, object } from 'yup'
import { Field, getValidationFromFields } from 'client/utils'
import { T, INPUT_TYPES } from 'client/constants'
const PREFIX = {
name: 'prefix',
label: T.Prefix,
tooltip: T.PrefixSecGroupsMultipleConcept,
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.required()
.default(() => T.CopyOf),
grid: { md: 12 },
}
const SEC_GROUP = {
name: 'name',
label: T.Name,
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.required()
.default(() => ''),
grid: { md: 12 },
}
/**
* @param {object} [stepProps] - Step props
* @param {boolean} [stepProps.isMultiple]
* - If true, the prefix will be added to the name of the new template
* @returns {Field[]} Fields
*/
export const FIELDS = ({ isMultiple } = {}) => [isMultiple ? PREFIX : SEC_GROUP]
export const SCHEMA = object(getValidationFromFields(FIELDS()))

View File

@ -0,0 +1,49 @@
/* ------------------------------------------------------------------------- *
* 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 FormWithSchema from 'client/components/Forms/FormWithSchema'
import {
SCHEMA,
FIELDS,
} from 'client/components/Forms/SecurityGroups/CreateForm/Steps/General/schema'
import { T } from 'client/constants'
export const STEP_ID = 'general'
const Content = () => (
<FormWithSchema id={STEP_ID} fields={FIELDS} cy={`${STEP_ID}`} />
)
/**
* General configuration about Security Groups.
*
* @param {object} securityGroupData - security group data
* @returns {object} Security Groups configuration step
*/
const General = (securityGroupData = {}) => {
const isUpdate = securityGroupData?.NAME
return {
id: STEP_ID,
label: T.Configuration,
resolver: (formdata) => SCHEMA(isUpdate),
optionsValidate: { abortEarly: false },
content: () => Content(securityGroupData),
}
}
export default General

View File

@ -0,0 +1,56 @@
/* ------------------------------------------------------------------------- *
* 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 { string, object, ObjectSchema } from 'yup'
import { Field, getValidationFromFields } from 'client/utils'
import { T, INPUT_TYPES } from 'client/constants'
export const IMAGE_LOCATION_TYPES = {
PATH: 'path',
UPLOAD: 'upload',
}
/** @type {Field} name field */
export const NAME = (isUpdate) => ({
name: 'NAME',
label: T.Name,
type: INPUT_TYPES.TEXT,
validation: string().trim().required(),
grid: { xs: 12, md: 6 },
...(isUpdate && { fieldProps: { disabled: true } }),
})
/** @type {Field} Description field */
export const DESCRIPTION = {
name: 'DESCRIPTION',
label: T.Description,
type: INPUT_TYPES.TEXT,
multiline: true,
validation: string().trim(),
grid: { xs: 12, md: 6 },
}
/**
* @param {boolean} isUpdate - is update.
* @returns {Field[]} Fields
*/
export const FIELDS = (isUpdate) => [NAME(isUpdate), DESCRIPTION]
/**
* @param {boolean} isUpdate - is update.
* @returns {ObjectSchema} Schema
*/
export const SCHEMA = (isUpdate) =>
object(getValidationFromFields(FIELDS(isUpdate)))

View File

@ -0,0 +1,43 @@
/* ------------------------------------------------------------------------- *
* 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 { array, object } from 'yup'
import RulesSection from './rulesSection'
import { SCHEMA } from 'client/components/Forms/SecurityGroups/CreateForm/Steps/Rules/schema'
import { T } from 'client/constants'
export const STEP_ID = 'rules'
const Content = () => <RulesSection stepId={STEP_ID} />
/**
* Rules configuration about Security Groups.
*
* @returns {object} Rules configuration step
*/
const Rules = () => ({
id: STEP_ID,
label: T.Rules,
resolver: object({
RULES: array(SCHEMA),
}),
optionsValidate: { abortEarly: false },
content: Content,
})
export default Rules

View File

@ -0,0 +1,213 @@
/* ------------------------------------------------------------------------- *
* 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, useCallback, memo, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Link as RouterLink } from 'react-router-dom'
import makeStyles from '@mui/styles/makeStyles'
import { Stack, FormControl, Link, Button, IconButton } from '@mui/material'
import Table from '@mui/material/Table'
import TableBody from '@mui/material/TableBody'
import TableCell from '@mui/material/TableCell'
import TableContainer from '@mui/material/TableContainer'
import TableHead from '@mui/material/TableHead'
import TableRow from '@mui/material/TableRow'
import { DeleteCircledOutline, AddCircledOutline } from 'iconoir-react'
import { useFieldArray, useForm, FormProvider } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import { FormWithSchema, Legend } from 'client/components/Forms'
import { Translate } from 'client/components/HOC'
import {
FIELDS,
SCHEMA,
} from 'client/components/Forms/SecurityGroups/CreateForm/Steps/Rules/schema'
import {
T,
ICMP_STRING,
ICMP_V6_STRING,
RESOURCE_NAMES,
} from 'client/constants'
export const SECTION_ID = 'RULES'
const useStyles = makeStyles({
container: {
marginTop: '3rem',
},
})
const RulesSection = memo(
/**
* @param {object} props - Props
* @param {string} [props.stepId] - ID of the step the section belongs to
* @returns {ReactElement} - Inputs section
*/
({ stepId }) => {
const classes = useStyles()
const fields = useMemo(() => FIELDS, [])
const {
fields: rules,
append,
remove,
} = useFieldArray({
name: useMemo(
() => [stepId, SECTION_ID].filter(Boolean).join('.'),
[stepId]
),
})
const getCyPath = useCallback(
(cy) => [stepId, cy].filter(Boolean).join('-'),
[stepId]
)
const methods = useForm({
defaultValues: {
[SECTION_ID]: SCHEMA.default(),
},
resolver: yupResolver(SCHEMA),
})
const onSubmit = (newRule) => {
newRule?.RULES && delete newRule.RULES
append(newRule)
methods.reset()
}
if (fields.length === 0) {
return null
}
return (
<FormControl component="fieldset" sx={{ width: '100%' }}>
<Legend title={T.Rules} />
<FormProvider {...methods}>
<Stack
direction="row"
alignItems="flex-start"
gap="0.5rem"
component="form"
onSubmit={methods.handleSubmit(onSubmit)}
>
<FormWithSchema
cy={getCyPath('rules')}
fields={fields}
rootProps={{ sx: { m: 0 } }}
/>
<Button
variant="contained"
type="submit"
color="secondary"
startIcon={<AddCircledOutline />}
data-cy={getCyPath('add-rules')}
sx={{ mt: '1em' }}
>
<Translate word={T.Add} />
</Button>
</Stack>
</FormProvider>
<TableContainer className={classes.container}>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>
<b>{T.Protocol}</b>
</TableCell>
<TableCell>
<b>{T.Type}</b>
</TableCell>
<TableCell>
<b>{T.Range}</b>
</TableCell>
<TableCell>
<b>{T.Network}</b>
</TableCell>
<TableCell>
<b>{T.IcmpType}</b>
</TableCell>
<TableCell>
<b>{T.IcmpTypeV6}</b>
</TableCell>
<TableCell />
</TableRow>
</TableHead>
<TableBody>
{rules?.map(
(
{
id,
PROTOCOL,
RULE_TYPE,
RANGE = T.All,
IP,
SIZE,
NETWORK_ID,
ICMP_TYPE = T.Any,
// eslint-disable-next-line camelcase
ICMPv6_TYPE = T.Any,
},
index
) => {
let network = T.Any
if (IP && SIZE) {
network = `${T.Start}: ${IP}, ${T.Size}: ${SIZE}`
} else if (!isNaN(NETWORK_ID)) {
network = (
<Link
component={RouterLink}
to={`/${RESOURCE_NAMES.VNET}/${NETWORK_ID}`}
color="secondary"
>
{NETWORK_ID}
</Link>
)
}
return (
<TableRow key={index}>
<TableCell>{PROTOCOL}</TableCell>
<TableCell>{RULE_TYPE}</TableCell>
<TableCell>{RANGE}</TableCell>
<TableCell>{network}</TableCell>
<TableCell>{ICMP_STRING[ICMP_TYPE] || ''}</TableCell>
<TableCell>{ICMP_V6_STRING[ICMPv6_TYPE] || ''}</TableCell>
<TableCell align="right">
<IconButton onClick={() => remove(index)}>
<DeleteCircledOutline />
</IconButton>
</TableCell>
</TableRow>
)
}
)}
</TableBody>
</Table>
</TableContainer>
</FormControl>
)
}
)
RulesSection.propTypes = {
stepId: PropTypes.string,
}
RulesSection.displayName = 'RulesSection'
export default RulesSection

View File

@ -0,0 +1,247 @@
/* ------------------------------------------------------------------------- *
* 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 { string, object, ObjectSchema, mixed } from 'yup'
import {
Field,
arrayToOptions,
getValidationFromFields,
upperCaseFirst,
} from 'client/utils'
import { VNetworksTable } from 'client/components/Tables'
import {
T,
RULE_TYPE_STRING,
INPUT_TYPES,
PROTOCOL_STRING,
ICMP_STRING,
ICMP_V6_STRING,
} from 'client/constants'
/** @type {Field} Rule type field */
export const RULE_TYPE = {
name: 'RULE_TYPE',
label: T.Traffic,
type: INPUT_TYPES.SELECT,
values: arrayToOptions(Object.values(RULE_TYPE_STRING), {
addEmpty: false,
getText: (ruleType) => {
switch (ruleType) {
case RULE_TYPE_STRING.OUTBOUND:
return T.Outbound
case RULE_TYPE_STRING.INBOUND:
return T.Inbound
default:
return upperCaseFirst(ruleType.toLowerCase())
}
},
getValue: (ruleType) => ruleType,
}),
validation: string()
.trim()
.default(() => RULE_TYPE_STRING.OUTBOUND)
.afterSubmit((value) => value.toLowerCase()),
grid: { md: 12 },
}
/** @type {Field} Protocol field */
export const PROTOCOL = {
name: 'PROTOCOL',
label: T.Protocol,
type: INPUT_TYPES.SELECT,
values: arrayToOptions(Object.values(PROTOCOL_STRING), {
addEmpty: false,
getText: (protocol) => {
switch (protocol) {
case PROTOCOL_STRING.TCP:
return T.TCP
case PROTOCOL_STRING.UDP:
return T.UDP
case PROTOCOL_STRING.ICMP:
return T.ICMP
case PROTOCOL_STRING.ICMPV6:
return T.ICMPV6
case PROTOCOL_STRING.IPSEC:
return T.IPSEC
case PROTOCOL_STRING.ALL:
return T.All
default:
return upperCaseFirst(protocol.toLowerCase())
}
},
getValue: (protocol) => protocol,
}),
validation: string()
.trim()
.default(() => PROTOCOL_STRING.TCP),
grid: { md: 12 },
}
/** @type {Field} ICMP_TYPE field */
export const ICMP_TYPE = {
name: 'ICMP_TYPE',
dependOf: PROTOCOL.name,
label: T.ICMP,
type: INPUT_TYPES.SELECT,
htmlType: (protocol) =>
protocol !== PROTOCOL_STRING.ICMP && INPUT_TYPES.HIDDEN,
values: arrayToOptions(Object.entries(ICMP_STRING), {
addEmpty: false,
getText: ([_, value]) => value,
getValue: ([key]) => key,
}),
validation: mixed().when(PROTOCOL.name, {
is: (protocol) => protocol === PROTOCOL_STRING.ICMP,
then: (schema) => schema.required(),
otherwise: (schema) => schema.notRequired().nullable(),
}),
grid: { md: 12 },
}
/** @type {Field} ICMPv6 field */
export const ICMPV6_TYPE = {
name: 'ICMPv6_TYPE',
dependOf: PROTOCOL.name,
label: T.ICMPV6,
type: INPUT_TYPES.SELECT,
htmlType: (protocol) =>
protocol !== PROTOCOL_STRING.ICMPV6 && INPUT_TYPES.HIDDEN,
values: arrayToOptions(Object.entries(ICMP_V6_STRING), {
addEmpty: false,
getText: ([_, value]) => value,
getValue: ([key]) => key,
}),
validation: mixed().when(PROTOCOL.name, {
is: (protocol) => protocol === PROTOCOL_STRING.ICMPV6,
then: (schema) => schema.required(),
otherwise: (schema) => schema.notRequired().nullable(),
}),
grid: { md: 12 },
}
/** @type {Field} RANGE, TYPE field */
export const RANGE_TYPE = {
name: 'RANGE_TYPE',
label: T.PortRange,
type: INPUT_TYPES.SELECT,
values: arrayToOptions([T.All, T.PortRange], {
addEmpty: false,
}),
validation: string()
.trim()
.afterSubmit((value) => undefined),
grid: { xs: 12 },
}
/** @type {Field} Range field */
export const RANGE = {
name: 'RANGE',
dependOf: RANGE_TYPE.name,
label: T.PortRange,
type: INPUT_TYPES.TEXT,
htmlType: (range) => range !== T.PortRange && INPUT_TYPES.HIDDEN,
validation: mixed().when(RANGE_TYPE.name, {
is: (protocol) => protocol === T.PortRange,
then: (schema) => schema.required(),
otherwise: (schema) => schema.notRequired().nullable(),
}),
grid: { md: 12 },
}
/** @type {Field} Target field */
export const TARGET = {
name: 'TARGET',
label: T.TargetNetwork,
type: INPUT_TYPES.SELECT,
values: arrayToOptions(
[T.AnyNetwork, T.ManualNetwork, T.OpennebulaVirtualNetwork],
{
addEmpty: false,
}
),
validation: string()
.trim()
.afterSubmit((value) => undefined),
grid: { xs: 12 },
}
/** @type {Field} IP field */
export const IP = {
name: 'IP',
dependOf: TARGET.name,
label: T.FirstIPIPv6Address,
type: INPUT_TYPES.TEXT,
htmlType: (range) => range !== T.ManualNetwork && INPUT_TYPES.HIDDEN,
validation: mixed().when(TARGET.name, {
is: (protocol) => protocol === T.ManualNetwork,
then: (schema) => schema.required(),
otherwise: (schema) => schema.notRequired().nullable(),
}),
grid: { md: 12 },
}
/** @type {Field} SIZE field */
export const SIZE = {
name: 'SIZE',
dependOf: TARGET.name,
label: T.Size,
type: INPUT_TYPES.TEXT,
htmlType: (range) => range !== T.ManualNetwork && INPUT_TYPES.HIDDEN,
validation: mixed().when(TARGET.name, {
is: (protocol) => protocol === T.ManualNetwork,
then: (schema) => schema.required(),
otherwise: (schema) => schema.notRequired().nullable(),
}),
grid: { md: 12 },
}
/** @type {Field} Networks field */
const NETWORK_ID = {
name: 'NETWORK_ID',
label: T.SelectNewNetwork,
type: INPUT_TYPES.TABLE,
dependOf: TARGET.name,
htmlType: (range) =>
range !== T.OpennebulaVirtualNetwork && INPUT_TYPES.HIDDEN,
Table: () => VNetworksTable,
validation: mixed().when(RANGE.name, {
is: (protocol) => protocol === T.OpennebulaVirtualNetwork,
then: (schema) => schema.required(),
otherwise: (schema) => schema.notRequired().nullable(),
}),
grid: { md: 12 },
}
/**
* @returns {Field[]} Fields
*/
export const FIELDS = [
RULE_TYPE,
PROTOCOL,
ICMP_TYPE,
ICMPV6_TYPE,
RANGE_TYPE,
RANGE,
TARGET,
IP,
SIZE,
NETWORK_ID,
]
/**
* @param {object} [stepProps] - Step props
* @returns {ObjectSchema} Schema
*/
export const SCHEMA = object(getValidationFromFields(FIELDS))

View File

@ -0,0 +1,53 @@
/* ------------------------------------------------------------------------- *
* 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 Rules, {
STEP_ID as RULES_ID,
} from 'client/components/Forms/SecurityGroups/CreateForm/Steps/Rules'
import General, {
STEP_ID as GENERAL_ID,
} from 'client/components/Forms/SecurityGroups/CreateForm/Steps/General'
import { createSteps } from 'client/utils'
const Steps = createSteps([General, Rules], {
transformInitialValue: ({ TEMPLATE, ...secGroup } = {}, schema) =>
schema.cast(
{
[GENERAL_ID]: { ...TEMPLATE, ...secGroup },
[RULES_ID]: {
RULES: Array.isArray(TEMPLATE.RULE) ? TEMPLATE.RULE : [TEMPLATE.RULE],
},
},
{ stripUnknown: true, context: secGroup }
),
transformBeforeSubmit: (formData) => {
const { [GENERAL_ID]: general = {}, [RULES_ID]: rules = {} } =
formData ?? {}
const rtn = {
template: {
...general,
},
}
if (rules?.RULES) rtn.template.RULE = rules.RULES
return rtn
},
})
export default Steps

View File

@ -0,0 +1,16 @@
/* ------------------------------------------------------------------------- *
* 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. *
* ------------------------------------------------------------------------- */
export { default } from 'client/components/Forms/SecurityGroups/CreateForm/Steps'

View File

@ -0,0 +1,34 @@
/* ------------------------------------------------------------------------- *
* 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 { AsyncLoadForm, ConfigurationProps } from 'client/components/HOC'
import { CreateFormCallback } from 'client/utils/schema'
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateFormCallback} Asynchronous loaded form
*/
const CloneForm = (configProps) =>
AsyncLoadForm({ formPath: 'SecurityGroups/CloneForm' }, configProps)
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateFormCallback} Asynchronous loaded form
*/
const CreateForm = (configProps) =>
AsyncLoadForm({ formPath: 'SecurityGroups/CreateForm' }, configProps)
export { CloneForm, CreateForm }

View File

@ -0,0 +1,251 @@
/* ------------------------------------------------------------------------- *
* 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 } from 'react'
import { useHistory } from 'react-router-dom'
import { Typography } from '@mui/material'
import { AddCircledOutline, Group, Trash } from 'iconoir-react'
import { useViews } from 'client/features/Auth'
import { PATH } from 'client/apps/sunstone/routesOne'
import {
useCloneSegGroupMutation,
useChangeSecGroupOwnershipMutation,
useRemoveSecGroupMutation,
useCommitSegGroupMutation,
} from 'client/features/OneApi/securityGroup'
import { ChangeUserForm, ChangeGroupForm } from 'client/components/Forms/Vm'
import { CloneForm } from 'client/components/Forms/SecurityGroups'
import {
createActions,
GlobalAction,
} from 'client/components/Tables/Enhanced/Utils'
import { Tr, Translate } from 'client/components/HOC'
import { T, SEC_GROUP_ACTIONS, RESOURCE_NAMES } from 'client/constants'
const ListSecGroupNames = ({ rows = [] }) =>
rows?.map?.(({ id, original }) => {
const { ID, NAME } = original
return (
<Typography
key={`segGroup-${id}`}
variant="inherit"
component="span"
display="block"
>
{`#${ID} ${NAME}`}
</Typography>
)
})
const SubHeader = (rows) => <ListSecGroupNames rows={rows} />
const MessageToConfirmAction = (rows, message) => (
<>
<ListSecGroupNames rows={rows} />
{message && (
<p>
<Translate word={message} />
</p>
)}
<Translate word={T.DoYouWantProceed} />
</>
)
/**
* Generates the actions to operate resources on Security Groups table.
*
* @returns {GlobalAction} - Actions
*/
const Actions = () => {
const history = useHistory()
const { view, getResourceView } = useViews()
const [clone] = useCloneSegGroupMutation()
const [changeOwnership] = useChangeSecGroupOwnershipMutation()
const [deleteSecGroup] = useRemoveSecGroupMutation()
const [commitSecGroup] = useCommitSegGroupMutation()
const resourcesView = getResourceView(RESOURCE_NAMES.SEC_GROUP)?.actions
const segGroupActions = useMemo(
() =>
createActions({
filters: resourcesView,
actions: [
{
accessor: SEC_GROUP_ACTIONS.CREATE_DIALOG,
dataCy: `securityGroup_${SEC_GROUP_ACTIONS.CREATE_DIALOG}`,
tooltip: T.Create,
icon: AddCircledOutline,
action: () => {
history.push(PATH.NETWORK.SEC_GROUPS.CREATE)
},
},
{
accessor: SEC_GROUP_ACTIONS.UPDATE_DIALOG,
label: T.Update,
tooltip: T.Update,
selected: { max: 1 },
color: 'secondary',
action: (rows) => {
const secGroups = rows?.[0]?.original ?? {}
const path = PATH.NETWORK.SEC_GROUPS.CREATE
history.push(path, secGroups)
},
},
{
accessor: SEC_GROUP_ACTIONS.CLONE,
label: T.Clone,
tooltip: T.Clone,
selected: true,
color: 'secondary',
options: [
{
dialogProps: {
title: (rows) => {
const isMultiple = rows?.length > 1
const { ID, NAME } = rows?.[0]?.original ?? {}
return [
Tr(isMultiple ? T.CloneSecGroups : T.CloneSecGroup),
!isMultiple && `#${ID} ${NAME}`,
]
.filter(Boolean)
.join(' - ')
},
dataCy: 'modal-clone',
},
form: (rows) => {
const names = rows?.map(({ original }) => original?.NAME)
const stepProps = { isMultiple: names.length > 1 }
const initialValues = { name: `Copy of ${names?.[0]}` }
return CloneForm({ stepProps, initialValues })
},
onSubmit:
(rows) =>
async ({ prefix, name } = {}) => {
const secGroups = rows?.map?.(
({ original: { ID, NAME } = {} }) =>
// overwrite all names with prefix+NAME
({
id: ID,
name: prefix ? `${prefix} ${NAME}` : name,
})
)
await Promise.all(secGroups.map(clone))
},
},
],
},
{
accessor: SEC_GROUP_ACTIONS.COMMIT,
label: T.Commit,
tooltip: T.Commit,
selected: true,
color: 'secondary',
options: [
{
isConfirmDialog: true,
dialogProps: {
title: T.Confirm,
dataCy: `modal-${SEC_GROUP_ACTIONS.COMMIT}`,
children: (rows) =>
MessageToConfirmAction(rows, T.CommitMessageSecGroups),
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => commitSecGroup({ id })))
},
},
],
},
{
tooltip: T.Ownership,
icon: Group,
selected: true,
color: 'secondary',
dataCy: 'securityGroup-ownership',
options: [
{
accessor: SEC_GROUP_ACTIONS.CHANGE_OWNER,
name: T.ChangeOwner,
dialogProps: {
title: T.ChangeOwner,
subheader: SubHeader,
dataCy: `modal-${SEC_GROUP_ACTIONS.CHANGE_OWNER}`,
},
form: ChangeUserForm,
onSubmit: (rows) => async (newOwnership) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(
ids.map((id) => changeOwnership({ id, ...newOwnership }))
)
},
},
{
accessor: SEC_GROUP_ACTIONS.CHANGE_GROUP,
name: T.ChangeGroup,
dialogProps: {
title: T.ChangeGroup,
subheader: SubHeader,
dataCy: `modal-${SEC_GROUP_ACTIONS.CHANGE_GROUP}`,
},
form: ChangeGroupForm,
onSubmit: (rows) => async (newOwnership) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(
ids.map((id) => changeOwnership({ id, ...newOwnership }))
)
},
},
],
},
{
accessor: SEC_GROUP_ACTIONS.DELETE,
tooltip: T.Delete,
icon: Trash,
color: 'error',
selected: { min: 1 },
dataCy: `secGroups_${SEC_GROUP_ACTIONS.DELETE}`,
options: [
{
isConfirmDialog: true,
dialogProps: {
title: T.Delete,
dataCy: `modal-${SEC_GROUP_ACTIONS.DELETE}`,
children: MessageToConfirmAction,
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => deleteSecGroup({ id })))
},
},
],
},
],
}),
[view]
)
return segGroupActions
}
export default Actions

View File

@ -0,0 +1,224 @@
/* ------------------------------------------------------------------------- *
* 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, ReactElement } from 'react'
import PropTypes from 'prop-types'
import { Link as RouterLink } from 'react-router-dom'
import makeStyles from '@mui/styles/makeStyles'
import { v4 as uuidv4 } from 'uuid'
import { Tr, Translate } from 'client/components/HOC'
import { T, PrettySecurityGroupRule, RESOURCE_NAMES } from 'client/constants'
import {
styled,
List,
ListItem,
Typography,
Paper,
Stack,
Box,
Link,
} from '@mui/material'
import { rowStyles } from 'client/components/Tables/styles'
const Title = styled(ListItem)(({ theme }) => ({
fontWeight: theme.typography.fontWeightBold,
borderBottom: `1px solid ${theme.palette.divider}`,
}))
const Item = styled(ListItem)(({ theme }) => ({
gap: '1em',
'& > *': {
flex: '1 1 50%',
overflow: 'hidden',
minHeight: '100%',
},
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
}))
const useStyles = makeStyles({
container: {
gridColumn: '1 / -1',
},
item: {
'& > *:first-child': {
flex: '1 1 20%',
},
},
})
const RulesSecGroupsTable = memo(({ title, rules = [] }) => {
const classes = useStyles()
return (
<Paper className={classes.container} variant="outlined">
<List variant="outlined">
{title && (
<Title>
{typeof title === 'string' ? (
<Typography noWrap>{Tr(title)}</Typography>
) : (
title
)}
</Title>
)}
<Item>
<SecurityGroupRules parentKey={''} rules={rules} />
</Item>
</List>
</Paper>
)
})
RulesSecGroupsTable.propTypes = {
title: PropTypes.any,
rules: PropTypes.arrayOf(
PropTypes.shape({
PROTOCOL: PropTypes.string,
RULE_TYPE: PropTypes.string,
RANGE: PropTypes.string,
NETWORK: PropTypes.string,
ICMP_TYPE: PropTypes.string,
})
),
}
RulesSecGroupsTable.displayName = 'RulesSecGroupsTable'
export const SecurityGroupRules = memo(
({ parentKey = '', id, actions, rules }) => {
const classes = rowStyles()
const COLUMNS = useMemo(
() => [T.Protocol, T.Type, T.Range, T.Network, T.IcmpType],
[]
)
const name = rules?.[0]?.NAME ?? 'default'
return (
<>
{id !== undefined && (
<Stack direction="row" spacing={1} alignItems="center">
<Typography
noWrap
component="span"
variant="subtitle1"
data-cy={`${parentKey}-rule-name`}
>
{`#${id} ${name}`}
</Typography>
{!!actions && <div className={classes.actions}>{actions}</div>}
</Stack>
)}
<Box display="grid" gridTemplateColumns="repeat(5, 1fr)" gap="0.5em">
{COLUMNS.map((col) => (
<Typography
key={`${parentKey}-${col}`}
noWrap
component="span"
variant="subtitle2"
>
<Translate word={col} />
</Typography>
))}
{rules.map((rule) => (
<SecurityGroupRule
key={uuidv4()}
data-cy={`${parentKey}-rule-${rule.RULE_TYPE}`}
rule={rule}
/>
))}
</Box>
</>
)
}
)
SecurityGroupRules.propTypes = {
parentKey: PropTypes.string,
id: PropTypes.string,
rules: PropTypes.array,
actions: PropTypes.node,
}
SecurityGroupRules.displayName = 'SecurityGroupRule'
export const SecurityGroupRule = memo(({ rule = {}, 'data-cy': parentCy }) => {
/** @type {PrettySecurityGroupRule} */
const {
PROTOCOL = '',
RULE_TYPE = '',
ICMP_TYPE = '',
RANGE = T.All,
NETWORK_ID = T.Any,
} = rule
/**
* @param {object} rule - rule.
* @param {string} rule.text - rule text
* @param {string} rule.dataCy - rule data-cy
* @param {boolean} rule.link - rule link
* @returns {ReactElement} rule line
*/
const renderLine = ({ text, dataCy, link }) => (
<Typography
noWrap
key={`${parentCy}-${dataCy}`}
data-cy={`${parentCy}-${dataCy}`.toLowerCase()}
variant="subtitle2"
>
{link && !isNaN(text) ? (
<Link
component={RouterLink}
to={`/${RESOURCE_NAMES.VNET}/${text}`}
color="secondary"
>
{text}
</Link>
) : (
text
)}
</Typography>
)
return (
<>
{[
{ text: String(PROTOCOL).toUpperCase(), dataCy: 'protocol' },
{ text: String(RULE_TYPE).toUpperCase(), dataCy: 'ruletype' },
{ text: String(RANGE).toUpperCase(), dataCy: 'range' },
{
text: String(NETWORK_ID).toUpperCase(),
dataCy: 'networkid',
link: true,
},
{ text: String(ICMP_TYPE).toUpperCase(), dataCy: 'icmp-type' },
].map(renderLine)}
</>
)
})
SecurityGroupRule.propTypes = {
rule: PropTypes.object,
'data-cy': PropTypes.string,
}
SecurityGroupRule.displayName = 'SecurityGroupRule'
export default RulesSecGroupsTable

View File

@ -17,7 +17,8 @@ import AttributePanel from 'client/components/Tabs/Common/AttributePanel'
import List from 'client/components/Tabs/Common/List'
import Ownership from 'client/components/Tabs/Common/Ownership'
import Permissions from 'client/components/Tabs/Common/Permissions'
import RulesSecGroupsTable from 'client/components/Tabs/Common/RulesSecGroups'
export * from 'client/components/Tabs/Common/Attribute'
export { AttributePanel, List, Ownership, Permissions }
export { AttributePanel, List, Ownership, Permissions, RulesSecGroupsTable }

View File

@ -44,7 +44,7 @@ const VmsTab = ({ id }) => {
displaySelectedRows
host={image}
onRowClick={(row) => handleRowClick(row.ID)}
noDataMessage={<EmptyTab label={T.NotVmsCurrenty} />}
noDataMessage={<EmptyTab label={T.NotVmsCurrentyImage} />}
/>
)
}

View File

@ -0,0 +1,165 @@
/* ------------------------------------------------------------------------- *
* 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, useCallback } from 'react'
import PropTypes from 'prop-types'
import { Stack } from '@mui/material'
import {
useGetSecGroupQuery,
useChangeSecGroupOwnershipMutation,
useChangeSecGroupPermissionsMutation,
useUpdateSecGroupMutation,
} from 'client/features/OneApi/securityGroup'
import {
Permissions,
Ownership,
AttributePanel,
RulesSecGroupsTable,
} from 'client/components/Tabs/Common'
import Information from 'client/components/Tabs/SecurityGroup/Info/information'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
import {
getActionsAvailable,
jsonToXml,
filterAttributes,
} from 'client/models/Helper'
import { cloneObject, set } from 'client/utils'
const HIDDEN_ATTRIBUTES = /^(RULE)$/
/**
* Renders mainly information tab.
*
* @param {object} props - Props
* @param {object} props.tabProps - Tab information
* @param {string} props.id - Image id
* @returns {ReactElement} Information tab
*/
const SecurityGroupInfoTab = ({ tabProps = {}, id }) => {
const {
information_panel: informationPanel,
permissions_panel: permissionsPanel,
ownership_panel: ownershipPanel,
attributes_panel: attributesPanel,
rules_panel: rulesPanel,
} = tabProps
const [changeOwnership] = useChangeSecGroupOwnershipMutation()
const [changePermissions] = useChangeSecGroupPermissionsMutation()
const [update] = useUpdateSecGroupMutation()
const { data: securityGroup } = useGetSecGroupQuery({ id })
const { UNAME, UID, GNAME, GID, PERMISSIONS, TEMPLATE } = securityGroup
const { attributes } = filterAttributes(TEMPLATE, {
hidden: HIDDEN_ATTRIBUTES,
})
const handleChangeOwnership = async (newOwnership) => {
await changeOwnership({ id, ...newOwnership })
}
const handleChangePermission = async (newPermission) => {
await changePermissions({ id, ...newPermission })
}
const handleAttributeInXml = async (path, newValue) => {
const newTemplate = cloneObject(TEMPLATE)
set(newTemplate, path, newValue)
const xml = jsonToXml(newTemplate)
await update({ id, template: xml, replace: 0 })
}
const getActions = useCallback(
(actions) => getActionsAvailable(actions),
[getActionsAvailable]
)
const ATTRIBUTE_FUNCTION = {
handleAdd: handleAttributeInXml,
handleEdit: handleAttributeInXml,
handleDelete: handleAttributeInXml,
}
return (
<Stack
display="grid"
gap="1em"
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
padding={{ sm: '0.8em' }}
>
{informationPanel?.enabled && (
<Information
securityGroup={securityGroup}
actions={getActions(informationPanel?.actions)}
/>
)}
{permissionsPanel?.enabled && (
<Permissions
actions={getActions(permissionsPanel?.actions)}
handleEdit={handleChangePermission}
ownerUse={PERMISSIONS.OWNER_U}
ownerManage={PERMISSIONS.OWNER_M}
ownerAdmin={PERMISSIONS.OWNER_A}
groupUse={PERMISSIONS.GROUP_U}
groupManage={PERMISSIONS.GROUP_M}
groupAdmin={PERMISSIONS.GROUP_A}
otherUse={PERMISSIONS.OTHER_U}
otherManage={PERMISSIONS.OTHER_M}
otherAdmin={PERMISSIONS.OTHER_A}
/>
)}
{ownershipPanel?.enabled && (
<Ownership
actions={getActions(ownershipPanel?.actions)}
handleEdit={handleChangeOwnership}
userId={UID}
userName={UNAME}
groupId={GID}
groupName={GNAME}
/>
)}
{rulesPanel?.enabled && (
<RulesSecGroupsTable
title={Tr(T.SecurityGroup)}
rules={
Array.isArray(TEMPLATE?.RULE) ? TEMPLATE?.RULE : [TEMPLATE?.RULE]
}
/>
)}
{attributesPanel?.enabled && (
<AttributePanel
{...ATTRIBUTE_FUNCTION}
attributes={attributes}
actions={getActions(attributesPanel?.actions)}
title={Tr(T.Attributes)}
/>
)}
</Stack>
)
}
SecurityGroupInfoTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
SecurityGroupInfoTab.displayName = 'ImageInfoTab'
export default SecurityGroupInfoTab

View File

@ -0,0 +1,69 @@
/* ------------------------------------------------------------------------- *
* 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 { useRenameSecGroupMutation } from 'client/features/OneApi/securityGroup'
import { List } from 'client/components/Tabs/Common'
import { T, Image, SEC_GROUP_ACTIONS } from 'client/constants'
/**
* Renders mainly information tab.
*
* @param {object} props - Props
* @param {Image} props.securityGroup - Security Group resource
* @param {string[]} props.actions - Available actions to information tab
* @returns {ReactElement} Information tab
*/
const InformationPanel = ({ securityGroup = {}, actions }) => {
const [rename] = useRenameSecGroupMutation()
const { ID, NAME } = securityGroup
const handleRename = async (_, newName) => {
await rename({ id: ID, name: newName })
}
const info = [
{ name: T.ID, value: ID, dataCy: 'id' },
{
name: T.Name,
value: NAME,
dataCy: 'name',
canEdit: actions?.includes?.(SEC_GROUP_ACTIONS.RENAME),
handleEdit: handleRename,
},
]
return (
<>
<List
title={T.Information}
list={info}
containerProps={{ sx: { gridRow: 'span 3' } }}
/>
</>
)
}
InformationPanel.propTypes = {
securityGroup: PropTypes.object,
actions: PropTypes.arrayOf(PropTypes.string),
}
InformationPanel.displayName = 'InformationPanel'
export default InformationPanel

View File

@ -0,0 +1,59 @@
/* ------------------------------------------------------------------------- *
* 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 { T } from 'client/constants'
import EmptyTab from 'client/components/Tabs/EmptyTab'
import { useHistory, generatePath } from 'react-router-dom'
import { PATH } from 'client/apps/sunstone/routesOne'
import { useGetSecGroupQuery } from 'client/features/OneApi/securityGroup'
import { VmsTable } from 'client/components/Tables'
/**
* Renders mainly Vms tab.
*
* @param {object} props - Props
* @param {string} props.id - Image id
* @returns {ReactElement} vms tab
*/
const VmsTab = ({ id }) => {
const { data: secGroup = {} } = useGetSecGroupQuery({ id })
const path = PATH.INSTANCE.VMS.DETAIL
const history = useHistory()
const handleRowClick = (rowId) => {
history.push(generatePath(path, { id: String(rowId) }))
}
return (
<VmsTable
disableGlobalSort
displaySelectedRows
host={secGroup}
onRowClick={(row) => handleRowClick(row.ID)}
noDataMessage={<EmptyTab label={T.NotVmsCurrentySecGroups} />}
/>
)
}
VmsTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
VmsTab.displayName = 'VmsTab'
export default VmsTab

View File

@ -0,0 +1,64 @@
/* ------------------------------------------------------------------------- *
* 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 { useGetSecGroupQuery } from 'client/features/OneApi/securityGroup'
import { getAvailableInfoTabs } from 'client/models/Helper'
import { RESOURCE_NAMES } from 'client/constants'
import Tabs from 'client/components/Tabs'
import Info from 'client/components/Tabs/SecurityGroup/Info'
import Vms from 'client/components/Tabs/SecurityGroup/Vms'
const getTabComponent = (tabName) =>
({
info: Info,
vms: Vms,
}[tabName])
const SecurityGroupTabs = memo(({ id }) => {
const { view, getResourceView } = useViews()
const { isLoading, isError, error } = useGetSecGroupQuery({ id })
const tabsAvailable = useMemo(() => {
const resource = RESOURCE_NAMES.SEC_GROUP
const infoTabs = getResourceView(resource)?.['info-tabs'] ?? {}
return getAvailableInfoTabs(infoTabs, getTabComponent, id)
}, [view])
if (isError) {
return (
<Alert severity="error" variant="outlined">
{error.data}
</Alert>
)
}
return isLoading ? (
<LinearProgress color="secondary" sx={{ width: '100%' }} />
) : (
<Tabs addBorder tabs={tabsAvailable ?? []} />
)
})
SecurityGroupTabs.propTypes = { id: PropTypes.string.isRequired }
SecurityGroupTabs.displayName = 'SecurityGroupTabs'
export default SecurityGroupTabs

View File

@ -14,6 +14,7 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { T } from 'client/constants'
import * as ACTIONS from 'client/constants/actions'
/**
* @typedef SecurityGroupRule
@ -116,3 +117,18 @@ export const RULE_TYPE_STRING = {
OUTBOUND: T.Outbound,
INBOUND: T.Inbound,
}
/** @enum {string} Image actions */
export const SEC_GROUP_ACTIONS = {
CREATE_DIALOG: 'create_dialog',
UPDATE_DIALOG: 'update_dialog',
DELETE: 'delete',
COMMIT: 'commit',
CLONE: 'clone',
// INFORMATION
RENAME: ACTIONS.RENAME,
CHANGE_OWNER: ACTIONS.CHANGE_OWNER,
CHANGE_GROUP: ACTIONS.CHANGE_GROUP,
CHANGE_TYPE: 'chtype',
}

View File

@ -56,9 +56,13 @@ module.exports = {
ChangeOwner: 'Change owner',
Clear: 'Clear',
ClickToCopy: 'Click to copy',
Confirm: 'Confirm',
Commit: 'Commit',
Clone: 'Clone',
CloneSeveralTemplates: 'Clone several Templates',
CloneTemplate: 'Clone Template',
CloneSecGroup: 'Clone Security Group',
CloneSecGroups: 'Clone Security Groups',
Close: 'Close',
Collapse: 'Collapse',
Configuration: 'Configuration',
@ -72,6 +76,7 @@ module.exports = {
CreateVirtualNetwork: 'Create Virtual Network',
CreateVmTemplate: 'Create VM Template',
CreateImage: 'Create Image',
CreateSecurityGroup: 'Create Security Group',
CreateFile: 'Create File',
CreateDockerfile: 'Create Dockerfile',
CurrentGroup: 'Current group: %s',
@ -870,6 +875,8 @@ module.exports = {
CopyOf: 'Copy of ',
PrefixMultipleConcept:
'Several templates are selected, please choose prefix to name the new copies',
PrefixSecGroupsMultipleConcept:
'Several security groups are selected, please choose a prefix to name the new copies Prefix',
NewTemplateNameConcept: 'New Image name',
CloneWithImages: 'Clone with images',
CloneWithImagesConcept: `
@ -1017,6 +1024,7 @@ module.exports = {
Any: 'Any',
Protocol: 'Protocol',
IcmpType: 'ICMP Type',
IcmpTypeV6: 'ICMPv6 Type',
/* Host schema */
IM_MAD: 'IM MAD',
@ -1130,9 +1138,25 @@ module.exports = {
DefaultValue: 'Default value',
Mandatory: 'Mandatory',
PressKeysToAddAValue: 'Press any of the following keys to add a value: %s',
/** Security Groups */
Start: 'Start',
Rules: 'Rules',
PortRange: 'Port Range',
FirstIPIPv6Address: 'First IP/IPv6 address',
TargetNetwork: 'Target Network',
AnyNetwork: 'Any Network',
ManualNetwork: 'Manual Network',
OpennebulaVirtualNetwork: 'OpenNebula Virtual Network',
SelectNewNetwork: 'Please select a network from the list',
NotVmsCurrentySecGroups:
'There are currently no VMs associated with this Security Group',
CommitMessageSecGroups: `
Please note: each time the rules are edited, the commit operation is done automatically.
This action will force the propagation of security group changes to VMs.
The operation takes time to iterate over all VMs in the security group,
the progress can be checked in the "VMs" panel.`,
/** Image */
NotVmsCurrenty: 'There are currently no VMs associated with this image',
NotVmsCurrentyImage: 'There are currently no VMs associated with this image',
NotSnapshotCurrenty:
'There are currently no snapshots associated with this image',

View File

@ -88,6 +88,9 @@ const InfoTabs = memo(({ file, gotoPage, unselect }) => {
return (
<Stack overflow="auto">
<Stack direction="row" alignItems="center" gap={1} mb={1}>
<Typography color="text.primary" noWrap flexGrow={1}>
{`#${id} | ${name}`}
</Typography>
<SubmitButton
data-cy="detail-refresh"
icon={<RefreshDouble />}
@ -111,9 +114,6 @@ const InfoTabs = memo(({ file, gotoPage, unselect }) => {
onClick={() => unselect()}
/>
)}
<Typography color="text.primary" noWrap>
{`#${id} | ${name}`}
</Typography>
</Stack>
<FileTabs id={file.ID} />
</Stack>

View File

@ -87,7 +87,10 @@ const InfoTabs = memo(({ image, gotoPage, unselect }) => {
return (
<Stack overflow="auto">
<Stack direction="row" alignItems="center" gap={1} mb={1}>
<Stack direction="row" alignItems="center" gap={1} mx={1} mb={1}>
<Typography color="text.primary" noWrap flexGrow={1}>
{`#${id} | ${name}`}
</Typography>
<SubmitButton
data-cy="detail-refresh"
icon={<RefreshDouble />}
@ -111,9 +114,6 @@ const InfoTabs = memo(({ image, gotoPage, unselect }) => {
onClick={() => unselect()}
/>
)}
<Typography color="text.primary" noWrap>
{`#${id} | ${name}`}
</Typography>
</Stack>
<ImageTabs id={image.ID} />
</Stack>

View File

@ -0,0 +1,79 @@
/* ------------------------------------------------------------------------- *
* 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 { useHistory, useLocation } from 'react-router'
import { jsonToXml } from 'client/models/Helper'
import { useGeneralApi } from 'client/features/General'
import {
useAllocateSecGroupMutation,
useUpdateSecGroupMutation,
useGetSecGroupQuery,
} from 'client/features/OneApi/securityGroup'
import {
DefaultFormStepper,
SkeletonStepsForm,
} from 'client/components/FormStepper'
import { CreateForm } from 'client/components/Forms/SecurityGroups'
import { PATH } from 'client/apps/sunstone/routesOne'
/**
* Displays the creation or modification form to a VM Template.
*
* @returns {ReactElement} VM Template form
*/
function CreateSecGroup() {
const history = useHistory()
const { state: { ID: secID, NAME } = {} } = useLocation()
const [allocate] = useAllocateSecGroupMutation()
const [update] = useUpdateSecGroupMutation()
const { enqueueSuccess } = useGeneralApi()
const { data } = useGetSecGroupQuery({ id: secID })
const onSubmit = async ({ template }) => {
try {
if (!secID) {
const newTemplateId = await allocate({
template: jsonToXml(template),
}).unwrap()
history.push(PATH.NETWORK.SEC_GROUPS.LIST)
enqueueSuccess(`Security Group created - #${newTemplateId}`)
} else {
await update({ id: secID, template: jsonToXml(template) }).unwrap()
history.push(PATH.NETWORK.SEC_GROUPS.LIST)
enqueueSuccess(`Security Group updated - #${secID} ${NAME}`)
}
} catch {}
}
return secID && !data ? (
<SkeletonStepsForm />
) : (
<CreateForm
onSubmit={onSubmit}
fallback={<SkeletonStepsForm />}
initialValues={data}
stepProps={data}
>
{(config) => <DefaultFormStepper {...config} />}
</CreateForm>
)
}
export default CreateSecGroup

View File

@ -0,0 +1,157 @@
/* ------------------------------------------------------------------------- *
* 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 GotoIcon from 'iconoir-react/dist/Pin'
import RefreshDouble from 'iconoir-react/dist/RefreshDouble'
import Cancel from 'iconoir-react/dist/Cancel'
import { Typography, Box, Stack, Chip } from '@mui/material'
import { Row } from 'react-table'
import { useLazyGetSecGroupQuery } from 'client/features/OneApi/securityGroup'
import { SecurityGroupsTable } from 'client/components/Tables'
import SecurityGroupsActions from 'client/components/Tables/SecurityGroups/actions'
import SecurityGroupsTabs from 'client/components/Tabs/SecurityGroup'
import SplitPane from 'client/components/SplitPane'
import MultipleTags from 'client/components/MultipleTags'
import { SubmitButton } from 'client/components/FormControl'
import { Tr } from 'client/components/HOC'
import { T, Image } from 'client/constants'
/**
* Displays a list of Security Groups with a split pane between the list and selected row(s).
*
* @returns {ReactElement} Security Groups list and selected row(s)
*/
function SecurityGroups() {
const [selectedRows, onSelectedRowsChange] = useState(() => [])
const actions = SecurityGroupsActions()
const hasSelectedRows = selectedRows?.length > 0
const moreThanOneSelected = selectedRows?.length > 1
return (
<SplitPane gridTemplateRows="1fr auto 1fr">
{({ getGridProps, GutterComponent }) => (
<Box height={1} {...(hasSelectedRows && getGridProps())}>
<SecurityGroupsTable
onSelectedRowsChange={onSelectedRowsChange}
globalActions={actions}
/>
{hasSelectedRows && (
<>
<GutterComponent direction="row" track={1} />
{moreThanOneSelected ? (
<GroupedTags tags={selectedRows} />
) : (
<InfoTabs
securityGroup={selectedRows[0]?.original}
gotoPage={selectedRows[0]?.gotoPage}
unselect={() => selectedRows[0]?.toggleRowSelected(false)}
/>
)}
</>
)}
</Box>
)}
</SplitPane>
)
}
/**
* Displays details of Security Group.
*
* @param {Image} securityGroup - Security Group to display
* @param {Function} [gotoPage] - Function to navigate to a page of an Security Group
* @param {Function} [unselect] - Function to unselect a Security Group
* @returns {ReactElement} Security Group details
*/
const InfoTabs = memo(({ securityGroup, gotoPage, unselect }) => {
const [getSecurityGroup, { data: lazyData, isFetching }] =
useLazyGetSecGroupQuery()
const id = lazyData?.ID ?? securityGroup.ID
const name = lazyData?.NAME ?? securityGroup.NAME
return (
<Stack overflow="auto">
<Stack direction="row" alignItems="center" gap={1} mx={1} mb={1}>
<Typography color="text.primary" noWrap flexGrow={1}>
{`#${id} | ${name}`}
</Typography>
<SubmitButton
data-cy="detail-refresh"
icon={<RefreshDouble />}
tooltip={Tr(T.Refresh)}
isSubmitting={isFetching}
onClick={() => getSecurityGroup({ id })}
/>
{typeof gotoPage === 'function' && (
<SubmitButton
data-cy="locate-on-table"
icon={<GotoIcon />}
tooltip={Tr(T.LocateOnTable)}
onClick={() => gotoPage()}
/>
)}
{typeof unselect === 'function' && (
<SubmitButton
data-cy="unselect"
icon={<Cancel />}
tooltip={Tr(T.Close)}
onClick={() => unselect()}
/>
)}
</Stack>
<SecurityGroupsTabs id={securityGroup.ID} />
</Stack>
)
})
InfoTabs.propTypes = {
securityGroup: PropTypes.object.isRequired,
gotoPage: PropTypes.func,
unselect: PropTypes.func,
}
InfoTabs.displayName = 'InfoTabs'
/**
* Displays a list of tags that represent the selected rows.
*
* @param {Row[]} tags - Row(s) to display as tags
* @returns {ReactElement} List of tags
*/
const GroupedTags = memo(({ tags = [] }) => (
<Stack direction="row" flexWrap="wrap" gap={1} alignContent="flex-start">
<MultipleTags
limitTags={10}
tags={tags?.map(({ original, id, toggleRowSelected, gotoPage }) => (
<Chip
key={id}
label={original?.NAME ?? id}
onClick={gotoPage}
onDelete={() => toggleRowSelected(false)}
/>
))}
/>
</Stack>
))
GroupedTags.propTypes = { tags: PropTypes.array }
GroupedTags.displayName = 'GroupedTags'
export default SecurityGroups

View File

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

View File

@ -19,7 +19,7 @@ import {
ONE_RESOURCES,
ONE_RESOURCES_POOL,
} from 'client/features/OneApi'
import { FilterFlag } from 'client/constants'
import { FilterFlag, Permission } from 'client/constants'
const { SECURITYGROUP } = ONE_RESOURCES
const { SECURITYGROUP_POOL } = ONE_RESOURCES_POOL
@ -75,6 +75,169 @@ const securityGroupApi = oneApi.injectEndpoints({
transformResponse: (data) => data?.SECURITY_GROUP ?? {},
providesTags: (_, __, { id }) => [{ type: SECURITYGROUP, id }],
}),
renameSecGroup: builder.mutation({
/**
* Renames a Security Group.
*
* @param {object} params - Request parameters
* @param {string} params.id - Security group id
* @param {string} params.name - The new name
* @returns {number} Security group id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.SECGROUP_RENAME
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [
{ type: SECURITYGROUP, id },
SECURITYGROUP_POOL,
],
}),
changeSecGroupPermissions: builder.mutation({
/**
* Changes the permission bits of a Image.
* If set any permission to -1, it's not changed.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - Image id
* @param {Permission|'-1'} params.ownerUse - User use
* @param {Permission|'-1'} params.ownerManage - User manage
* @param {Permission|'-1'} params.ownerAdmin - User administrator
* @param {Permission|'-1'} params.groupUse - Group use
* @param {Permission|'-1'} params.groupManage - Group manage
* @param {Permission|'-1'} params.groupAdmin - Group administrator
* @param {Permission|'-1'} params.otherUse - Other use
* @param {Permission|'-1'} params.otherManage - Other manage
* @param {Permission|'-1'} params.otherAdmin - Other administrator
* @returns {number} Image id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.SECGROUP_CHMOD
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: SECURITYGROUP, id }],
}),
changeSecGroupOwnership: builder.mutation({
/**
* Changes the ownership of Security Group.
* If set `user` or `group` to -1, it's not changed.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - Security Group id
* @param {string|number|'-1'} [params.userId] - User id
* @param {Permission|'-1'} [params.groupId] - Group id
* @returns {number} Security Group id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.SECGROUP_CHOWN
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [
{ type: SECURITYGROUP, id },
SECURITYGROUP_POOL,
],
}),
allocateSecGroup: builder.mutation({
/**
* Allocates a new Security Group in OpenNebula.
*
* @param {object} params - Request params
* @param {string} params.template - A string containing the template of the Security Group on syntax XML
* @returns {number} Security Group id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.SECGROUP_ALLOCATE
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: [SECURITYGROUP_POOL],
}),
cloneSegGroup: builder.mutation({
/**
* Clones an existing Security Group.
*
* @param {object} params - Request params
* @param {string} params.id - The id of the Security Group to be cloned
* @param {string} params.name - Name for the new Security Group
* @returns {number} The new Security Group id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.SECGROUP_CLONE
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: [SECURITYGROUP_POOL],
}),
removeSecGroup: builder.mutation({
/**
* Deletes the given Security Group from the pool.
*
* @param {object} params - Request params
* @param {string} params.id - The object id
* @returns {number} Security Group id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.SECGROUP_DELETE
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: [SECURITYGROUP_POOL],
}),
updateSecGroup: builder.mutation({
/**
* Replaces the Security Group template contents.
*
* @param {number|string} params - Request params
* @param {string} params.id - Security Group id
* @param {string} params.template - The new template contents on syntax XML
* @param {0|1} params.replace -
* - Update type:
* ``0``: Replace the whole template.
* ``1``: Merge new template with the existing one.
* @returns {number} Security Group id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.SECGROUP_UPDATE
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: SECURITYGROUP, id }],
}),
commitSegGroup: builder.mutation({
/**
* Commit an existing Security Group.
*
* @param {object} params - Request params
* @param {string} params.id - The id of the Security Group to be cloned
* @param {string} params.vms - Vms for the new Security Group
* @returns {number} The new Security Group id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.SECGROUP_COMMIT
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: [SECURITYGROUP_POOL],
}),
}),
})
@ -84,6 +247,14 @@ export const {
useLazyGetSecGroupQuery,
useGetSecGroupsQuery,
useLazyGetSecGroupsQuery,
useRenameSecGroupMutation,
useAllocateSecGroupMutation,
useCloneSegGroupMutation,
useRemoveSecGroupMutation,
useUpdateSecGroupMutation,
useCommitSegGroupMutation,
useChangeSecGroupPermissionsMutation,
useChangeSecGroupOwnershipMutation,
} = securityGroupApi
export default securityGroupApi