diff --git a/src/fireedge/src/client/apps/sunstone/routesOne.js b/src/fireedge/src/client/apps/sunstone/routesOne.js
index 28c6c5b1d9..a8332157f8 100644
--- a/src/fireedge/src/client/apps/sunstone/routesOne.js
+++ b/src/fireedge/src/client/apps/sunstone/routesOne.js
@@ -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,
+ },
],
},
{
diff --git a/src/fireedge/src/client/components/Cards/NicCard.js b/src/fireedge/src/client/components/Cards/NicCard.js
index 763a79d117..a960753550 100644
--- a/src/fireedge/src/client/components/Cards/NicCard.js
+++ b/src/fireedge/src/client/components/Cards/NicCard.js
@@ -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 (
- <>
-
-
- {`#${id} ${name}`}
-
- {!!actions && {actions}
}
-
-
- {COLUMNS.map((col) => (
-
-
-
- ))}
- {rules.map((rule) => (
-
- ))}
-
- >
- )
-})
-
-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 }) => (
-
- {text}
-
- ))}
- >
- )
-})
-
-SecurityGroupRule.propTypes = {
- rule: PropTypes.object,
- 'data-cy': PropTypes.string,
-}
-
-SecurityGroupRule.displayName = 'SecurityGroupRule'
-
export default NicCard
diff --git a/src/fireedge/src/client/components/Forms/SecurityGroups/CloneForm/index.js b/src/fireedge/src/client/components/Forms/SecurityGroups/CloneForm/index.js
new file mode 100644
index 0000000000..f4d1a115d4
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/SecurityGroups/CloneForm/index.js
@@ -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
diff --git a/src/fireedge/src/client/components/Forms/SecurityGroups/CloneForm/schema.js b/src/fireedge/src/client/components/Forms/SecurityGroups/CloneForm/schema.js
new file mode 100644
index 0000000000..793220c0c6
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/SecurityGroups/CloneForm/schema.js
@@ -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()))
diff --git a/src/fireedge/src/client/components/Forms/SecurityGroups/CreateForm/Steps/General/index.js b/src/fireedge/src/client/components/Forms/SecurityGroups/CreateForm/Steps/General/index.js
new file mode 100644
index 0000000000..c313a823ea
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/SecurityGroups/CreateForm/Steps/General/index.js
@@ -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 = () => (
+
+)
+
+/**
+ * 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
diff --git a/src/fireedge/src/client/components/Forms/SecurityGroups/CreateForm/Steps/General/schema.js b/src/fireedge/src/client/components/Forms/SecurityGroups/CreateForm/Steps/General/schema.js
new file mode 100644
index 0000000000..cbe15b0718
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/SecurityGroups/CreateForm/Steps/General/schema.js
@@ -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)))
diff --git a/src/fireedge/src/client/components/Forms/SecurityGroups/CreateForm/Steps/Rules/index.js b/src/fireedge/src/client/components/Forms/SecurityGroups/CreateForm/Steps/Rules/index.js
new file mode 100644
index 0000000000..e8672a2d8e
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/SecurityGroups/CreateForm/Steps/Rules/index.js
@@ -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 = () =>
+
+/**
+ * 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
diff --git a/src/fireedge/src/client/components/Forms/SecurityGroups/CreateForm/Steps/Rules/rulesSection.js b/src/fireedge/src/client/components/Forms/SecurityGroups/CreateForm/Steps/Rules/rulesSection.js
new file mode 100644
index 0000000000..196cebfef0
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/SecurityGroups/CreateForm/Steps/Rules/rulesSection.js
@@ -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 (
+
+
+
+
+
+ }
+ data-cy={getCyPath('add-rules')}
+ sx={{ mt: '1em' }}
+ >
+
+
+
+
+
+
+
+
+
+ {T.Protocol}
+
+
+ {T.Type}
+
+
+ {T.Range}
+
+
+ {T.Network}
+
+
+ {T.IcmpType}
+
+
+ {T.IcmpTypeV6}
+
+
+
+
+
+ {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 = (
+
+ {NETWORK_ID}
+
+ )
+ }
+
+ return (
+
+ {PROTOCOL}
+ {RULE_TYPE}
+ {RANGE}
+ {network}
+ {ICMP_STRING[ICMP_TYPE] || ''}
+ {ICMP_V6_STRING[ICMPv6_TYPE] || ''}
+
+ remove(index)}>
+
+
+
+
+ )
+ }
+ )}
+
+
+
+
+ )
+ }
+)
+
+RulesSection.propTypes = {
+ stepId: PropTypes.string,
+}
+
+RulesSection.displayName = 'RulesSection'
+
+export default RulesSection
diff --git a/src/fireedge/src/client/components/Forms/SecurityGroups/CreateForm/Steps/Rules/schema.js b/src/fireedge/src/client/components/Forms/SecurityGroups/CreateForm/Steps/Rules/schema.js
new file mode 100644
index 0000000000..59113e8188
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/SecurityGroups/CreateForm/Steps/Rules/schema.js
@@ -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))
diff --git a/src/fireedge/src/client/components/Forms/SecurityGroups/CreateForm/Steps/index.js b/src/fireedge/src/client/components/Forms/SecurityGroups/CreateForm/Steps/index.js
new file mode 100644
index 0000000000..b0a1b49fa8
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/SecurityGroups/CreateForm/Steps/index.js
@@ -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
diff --git a/src/fireedge/src/client/components/Forms/SecurityGroups/CreateForm/index.js b/src/fireedge/src/client/components/Forms/SecurityGroups/CreateForm/index.js
new file mode 100644
index 0000000000..606f9a8c2b
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/SecurityGroups/CreateForm/index.js
@@ -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'
diff --git a/src/fireedge/src/client/components/Forms/SecurityGroups/index.js b/src/fireedge/src/client/components/Forms/SecurityGroups/index.js
new file mode 100644
index 0000000000..f71b3dea3c
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/SecurityGroups/index.js
@@ -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 }
diff --git a/src/fireedge/src/client/components/Tables/SecurityGroups/actions.js b/src/fireedge/src/client/components/Tables/SecurityGroups/actions.js
new file mode 100644
index 0000000000..26b356971b
--- /dev/null
+++ b/src/fireedge/src/client/components/Tables/SecurityGroups/actions.js
@@ -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 (
+
+ {`#${ID} ${NAME}`}
+
+ )
+ })
+
+const SubHeader = (rows) =>
+
+const MessageToConfirmAction = (rows, message) => (
+ <>
+
+ {message && (
+
+
+
+ )}
+
+ >
+)
+
+/**
+ * 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
diff --git a/src/fireedge/src/client/components/Tabs/Common/RulesSecGroups.js b/src/fireedge/src/client/components/Tabs/Common/RulesSecGroups.js
new file mode 100644
index 0000000000..9f5d67c0d8
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/Common/RulesSecGroups.js
@@ -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 (
+
+
+ {title && (
+
+ {typeof title === 'string' ? (
+ {Tr(title)}
+ ) : (
+ title
+ )}
+
+ )}
+ -
+
+
+
+
+ )
+})
+
+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 && (
+
+
+ {`#${id} ${name}`}
+
+
+ {!!actions && {actions}
}
+
+ )}
+
+ {COLUMNS.map((col) => (
+
+
+
+ ))}
+ {rules.map((rule) => (
+
+ ))}
+
+ >
+ )
+ }
+)
+
+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 }) => (
+
+ {link && !isNaN(text) ? (
+
+ {text}
+
+ ) : (
+ text
+ )}
+
+ )
+
+ 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
diff --git a/src/fireedge/src/client/components/Tabs/Common/index.js b/src/fireedge/src/client/components/Tabs/Common/index.js
index 724ef596bd..ba683c94f2 100644
--- a/src/fireedge/src/client/components/Tabs/Common/index.js
+++ b/src/fireedge/src/client/components/Tabs/Common/index.js
@@ -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 }
diff --git a/src/fireedge/src/client/components/Tabs/Image/Vms/index.js b/src/fireedge/src/client/components/Tabs/Image/Vms/index.js
index 9d3c5ae441..fded895e66 100644
--- a/src/fireedge/src/client/components/Tabs/Image/Vms/index.js
+++ b/src/fireedge/src/client/components/Tabs/Image/Vms/index.js
@@ -44,7 +44,7 @@ const VmsTab = ({ id }) => {
displaySelectedRows
host={image}
onRowClick={(row) => handleRowClick(row.ID)}
- noDataMessage={}
+ noDataMessage={}
/>
)
}
diff --git a/src/fireedge/src/client/components/Tabs/SecurityGroup/Info/index.js b/src/fireedge/src/client/components/Tabs/SecurityGroup/Info/index.js
new file mode 100644
index 0000000000..a2aee9775f
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/SecurityGroup/Info/index.js
@@ -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 (
+
+ {informationPanel?.enabled && (
+
+ )}
+ {permissionsPanel?.enabled && (
+
+ )}
+ {ownershipPanel?.enabled && (
+
+ )}
+ {rulesPanel?.enabled && (
+
+ )}
+ {attributesPanel?.enabled && (
+
+ )}
+
+ )
+}
+
+SecurityGroupInfoTab.propTypes = {
+ tabProps: PropTypes.object,
+ id: PropTypes.string,
+}
+
+SecurityGroupInfoTab.displayName = 'ImageInfoTab'
+
+export default SecurityGroupInfoTab
diff --git a/src/fireedge/src/client/components/Tabs/SecurityGroup/Info/information.js b/src/fireedge/src/client/components/Tabs/SecurityGroup/Info/information.js
new file mode 100644
index 0000000000..0771aa0268
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/SecurityGroup/Info/information.js
@@ -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 (
+ <>
+
+ >
+ )
+}
+
+InformationPanel.propTypes = {
+ securityGroup: PropTypes.object,
+ actions: PropTypes.arrayOf(PropTypes.string),
+}
+
+InformationPanel.displayName = 'InformationPanel'
+
+export default InformationPanel
diff --git a/src/fireedge/src/client/components/Tabs/SecurityGroup/Vms/index.js b/src/fireedge/src/client/components/Tabs/SecurityGroup/Vms/index.js
new file mode 100644
index 0000000000..1dd5e481aa
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/SecurityGroup/Vms/index.js
@@ -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 (
+ handleRowClick(row.ID)}
+ noDataMessage={}
+ />
+ )
+}
+
+VmsTab.propTypes = {
+ tabProps: PropTypes.object,
+ id: PropTypes.string,
+}
+
+VmsTab.displayName = 'VmsTab'
+
+export default VmsTab
diff --git a/src/fireedge/src/client/components/Tabs/SecurityGroup/index.js b/src/fireedge/src/client/components/Tabs/SecurityGroup/index.js
new file mode 100644
index 0000000000..bd46d1989b
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/SecurityGroup/index.js
@@ -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 (
+
+ {error.data}
+
+ )
+ }
+
+ return isLoading ? (
+
+ ) : (
+
+ )
+})
+
+SecurityGroupTabs.propTypes = { id: PropTypes.string.isRequired }
+SecurityGroupTabs.displayName = 'SecurityGroupTabs'
+
+export default SecurityGroupTabs
diff --git a/src/fireedge/src/client/constants/securityGroup.js b/src/fireedge/src/client/constants/securityGroup.js
index 772f83dda7..351e2c34ba 100644
--- a/src/fireedge/src/client/constants/securityGroup.js
+++ b/src/fireedge/src/client/constants/securityGroup.js
@@ -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',
+}
diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js
index ad33208652..42936ed3f7 100644
--- a/src/fireedge/src/client/constants/translates.js
+++ b/src/fireedge/src/client/constants/translates.js
@@ -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',
diff --git a/src/fireedge/src/client/containers/Files/index.js b/src/fireedge/src/client/containers/Files/index.js
index 43bea7fac2..c916c5613f 100644
--- a/src/fireedge/src/client/containers/Files/index.js
+++ b/src/fireedge/src/client/containers/Files/index.js
@@ -88,6 +88,9 @@ const InfoTabs = memo(({ file, gotoPage, unselect }) => {
return (
+
+ {`#${id} | ${name}`}
+
}
@@ -111,9 +114,6 @@ const InfoTabs = memo(({ file, gotoPage, unselect }) => {
onClick={() => unselect()}
/>
)}
-
- {`#${id} | ${name}`}
-
diff --git a/src/fireedge/src/client/containers/Images/index.js b/src/fireedge/src/client/containers/Images/index.js
index f66bdb6819..4213ad6cdc 100644
--- a/src/fireedge/src/client/containers/Images/index.js
+++ b/src/fireedge/src/client/containers/Images/index.js
@@ -87,7 +87,10 @@ const InfoTabs = memo(({ image, gotoPage, unselect }) => {
return (
-
+
+
+ {`#${id} | ${name}`}
+
}
@@ -111,9 +114,6 @@ const InfoTabs = memo(({ image, gotoPage, unselect }) => {
onClick={() => unselect()}
/>
)}
-
- {`#${id} | ${name}`}
-
diff --git a/src/fireedge/src/client/containers/SecurityGroups/Create.js b/src/fireedge/src/client/containers/SecurityGroups/Create.js
new file mode 100644
index 0000000000..c6007f8b06
--- /dev/null
+++ b/src/fireedge/src/client/containers/SecurityGroups/Create.js
@@ -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 ? (
+
+ ) : (
+ }
+ initialValues={data}
+ stepProps={data}
+ >
+ {(config) => }
+
+ )
+}
+
+export default CreateSecGroup
diff --git a/src/fireedge/src/client/containers/SecurityGroups/index.js b/src/fireedge/src/client/containers/SecurityGroups/index.js
new file mode 100644
index 0000000000..f009514b60
--- /dev/null
+++ b/src/fireedge/src/client/containers/SecurityGroups/index.js
@@ -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 (
+
+ {({ getGridProps, GutterComponent }) => (
+
+
+
+ {hasSelectedRows && (
+ <>
+
+ {moreThanOneSelected ? (
+
+ ) : (
+ selectedRows[0]?.toggleRowSelected(false)}
+ />
+ )}
+ >
+ )}
+
+ )}
+
+ )
+}
+
+/**
+ * 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 (
+
+
+
+ {`#${id} | ${name}`}
+
+ }
+ tooltip={Tr(T.Refresh)}
+ isSubmitting={isFetching}
+ onClick={() => getSecurityGroup({ id })}
+ />
+ {typeof gotoPage === 'function' && (
+ }
+ tooltip={Tr(T.LocateOnTable)}
+ onClick={() => gotoPage()}
+ />
+ )}
+ {typeof unselect === 'function' && (
+ }
+ tooltip={Tr(T.Close)}
+ onClick={() => unselect()}
+ />
+ )}
+
+
+
+ )
+})
+
+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 = [] }) => (
+
+ (
+ toggleRowSelected(false)}
+ />
+ ))}
+ />
+
+))
+
+GroupedTags.propTypes = { tags: PropTypes.array }
+GroupedTags.displayName = 'GroupedTags'
+
+export default SecurityGroups
diff --git a/src/fireedge/src/client/containers/VirtualNetworks/Detail.js b/src/fireedge/src/client/containers/VirtualNetworks/Detail.js
new file mode 100644
index 0000000000..12cef9f229
--- /dev/null
+++ b/src/fireedge/src/client/containers/VirtualNetworks/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 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
+ }
+
+ return
+}
+
+export default VNetworkDetail
diff --git a/src/fireedge/src/client/features/OneApi/securityGroup.js b/src/fireedge/src/client/features/OneApi/securityGroup.js
index 55e62e5b32..0e3aec50f6 100644
--- a/src/fireedge/src/client/features/OneApi/securityGroup.js
+++ b/src/fireedge/src/client/features/OneApi/securityGroup.js
@@ -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