1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-30 22:50:10 +03:00

F #5422: Add attach security group to nic (#1928)

This commit is contained in:
Sergio Betanzos 2022-04-11 19:13:56 +02:00 committed by GitHub
parent fc724f2121
commit 62396fe4dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1043 additions and 337 deletions

View File

@ -43,7 +43,11 @@ oneprovision_optional_create_command: ''
# Currency formatting
# Possible values are the ISO 4217 currency codes
# https://www.six-group.com/en/products-services/financial-information/data-standards.html
currency: 'EUR'
currency: EUR
# Default language setting
# Check that the language exists in client/assets/languages
default_lang: en
# Translations: use it if you want to use certain languages on the client.
# Check that the language exists in client/assets/languages

View File

@ -141,6 +141,8 @@ info-tabs:
actions:
attach_nic: true
detach_nic: true
attach_secgroup: true
detach_secgroup: true
snapshot:
enabled: true

View File

@ -139,6 +139,8 @@ info-tabs:
actions:
attach_nic: true
detach_nic: true
attach_secgroup: true
detach_secgroup: true
snapshot:
enabled: true

View File

@ -31,7 +31,7 @@ import { AuthLayout } from 'client/components/HOC'
import { isDevelopment } from 'client/utils'
import { _APPS } from 'client/constants'
export const APP_NAME = _APPS.provision.name
export const APP_NAME = _APPS.provision
const MESSAGE_PROVISION_SUCCESS_CREATED = 'Provision successfully created'

View File

@ -21,6 +21,7 @@ import { StaticRouter, BrowserRouter } from 'react-router-dom'
import { Provider as ReduxProvider } from 'react-redux'
import { Store } from 'redux'
import PreloadConfigProvider from 'client/providers/preloadConfigProvider'
import MuiProvider from 'client/providers/muiProvider'
import NotistackProvider from 'client/providers/notistackProvider'
import { TranslateProvider } from 'client/components/HOC'
@ -39,25 +40,27 @@ buildTranslationLocale()
* @returns {JSXElementConstructor} Provision App
*/
const Provision = ({ store = {}, location = '' }) => (
<ReduxProvider store={store}>
<TranslateProvider>
<MuiProvider theme={theme}>
<NotistackProvider>
{location ? (
// server build
<StaticRouter location={location}>
<App />
</StaticRouter>
) : (
// browser build
<BrowserRouter basename={`${APP_URL}/${ProvisionAppName}`}>
<App />
</BrowserRouter>
)}
</NotistackProvider>
</MuiProvider>
</TranslateProvider>
</ReduxProvider>
<PreloadConfigProvider>
<ReduxProvider store={store}>
<TranslateProvider>
<MuiProvider theme={theme}>
<NotistackProvider>
{location ? (
// server build
<StaticRouter location={location}>
<App />
</StaticRouter>
) : (
// browser build
<BrowserRouter basename={`${APP_URL}/${ProvisionAppName}`}>
<App />
</BrowserRouter>
)}
</NotistackProvider>
</MuiProvider>
</TranslateProvider>
</ReduxProvider>
</PreloadConfigProvider>
)
Provision.propTypes = {

View File

@ -18,12 +18,6 @@ import { ThemeOptions } from '@mui/material'
/** @type {ThemeOptions} Provision theme */
export default {
palette: {
primary: {
light: '#2a2d3d',
main: '#222431',
dark: '#191924',
contrastText: '#ffffff',
},
secondary: {
100: '#ffeae4',
200: '#ffd6c8',
@ -37,7 +31,7 @@ export default {
light: '#ffd6c8',
main: '#fe835a',
dark: '#fe5a23',
contrastText: '#ffffff',
contrastText: '#fff',
},
},
}

View File

@ -33,7 +33,7 @@ import { AuthLayout } from 'client/components/HOC'
import { isDevelopment } from 'client/utils'
import { _APPS } from 'client/constants'
export const APP_NAME = _APPS.sunstone.name
export const APP_NAME = _APPS.sunstone
/**
* Sunstone App component.

View File

@ -21,6 +21,7 @@ import { StaticRouter, BrowserRouter } from 'react-router-dom'
import { Provider as ReduxProvider } from 'react-redux'
import { Store } from 'redux'
import PreloadConfigProvider from 'client/providers/preloadConfigProvider'
import MuiProvider from 'client/providers/muiProvider'
import NotistackProvider from 'client/providers/notistackProvider'
import { TranslateProvider } from 'client/components/HOC'
@ -39,25 +40,27 @@ buildTranslationLocale()
* @returns {JSXElementConstructor} Sunstone App
*/
const Sunstone = ({ store = {}, location = '' }) => (
<ReduxProvider store={store}>
<TranslateProvider>
<MuiProvider theme={theme}>
<NotistackProvider>
{location ? (
// server build
<StaticRouter location={location}>
<App />
</StaticRouter>
) : (
// browser build
<BrowserRouter basename={`${APP_URL}/${SunstoneAppName}`}>
<App />
</BrowserRouter>
)}
</NotistackProvider>
</MuiProvider>
</TranslateProvider>
</ReduxProvider>
<PreloadConfigProvider>
<ReduxProvider store={store}>
<TranslateProvider>
<MuiProvider theme={theme}>
<NotistackProvider>
{location ? (
// server build
<StaticRouter location={location}>
<App />
</StaticRouter>
) : (
// browser build
<BrowserRouter basename={`${APP_URL}/${SunstoneAppName}`}>
<App />
</BrowserRouter>
)}
</NotistackProvider>
</MuiProvider>
</TranslateProvider>
</ReduxProvider>
</PreloadConfigProvider>
)
Sunstone.propTypes = {

View File

@ -18,12 +18,6 @@ import { ThemeOptions } from '@mui/material'
/** @type {ThemeOptions} Sunstone theme */
export default {
palette: {
primary: {
light: '#2a2d3d',
main: '#222431',
dark: '#191924',
contrastText: '#ffffff',
},
secondary: {
100: '#dff2f8',
200: '#bfe6f0',

View File

@ -44,7 +44,7 @@ const GUACAMOLE_BUTTONS = {
}
const openNewBrowserTab = (path) =>
window?.open(`/fireedge/${_APPS.sunstone.name}${path}`, '_blank')
window?.open(`/fireedge/${_APPS.sunstone}${path}`, '_blank')
const GuacamoleButton = memo(({ vm, connectionType, onClick }) => {
const { icon, tooltip } = GUACAMOLE_BUTTONS[connectionType]

View File

@ -13,32 +13,69 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo } from 'react'
import { memo, useMemo } from 'react'
import PropTypes from 'prop-types'
import {
styled,
useMediaQuery,
Typography,
Box,
Paper,
Stack,
Divider,
Accordion as MuiAccordion,
AccordionSummary as MuiAccordionSummary,
AccordionDetails as MuiAccordionDetails,
} from '@mui/material'
import { NavArrowRight } from 'iconoir-react'
import { rowStyles } from 'client/components/Tables/styles'
import { StatusChip } from 'client/components/Status'
import MultipleTags from 'client/components/MultipleTags'
import SecurityGroupCard from 'client/components/Cards/SecurityGroupCard'
import { Translate } from 'client/components/HOC'
import { stringToBoolean } from 'client/models/Helper'
import { T, Nic, NicAlias } from 'client/constants'
import { groupBy } from 'client/utils'
import { T, Nic, NicAlias, PrettySecurityGroupRule } from 'client/constants'
const Accordion = styled((props) => (
<MuiAccordion disableGutters elevation={0} square {...props} />
))(({ theme }) => ({
flexBasis: '100%',
border: `1px solid ${theme.palette.divider}`,
'&:before': { display: 'none' },
}))
const AccordionSummary = styled((props) => (
<MuiAccordionSummary expandIcon={<NavArrowRight />} {...props} />
))(({ theme }) => ({
backgroundColor:
theme.palette.mode === 'dark'
? 'rgba(255, 255, 255, .05)'
: 'rgba(0, 0, 0, .03)',
'&:not(:last-child)': {
borderBottom: 0,
},
flexDirection: 'row-reverse',
'& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': {
transform: 'rotate(90deg)',
},
'& .MuiAccordionSummary-content': {
marginLeft: theme.spacing(1),
},
}))
const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({
padding: theme.spacing(2),
borderTop: '1px solid rgba(0, 0, 0, .125)',
}))
const NicCard = memo(
({
nic = {},
actions = [],
aliasActions = [],
actions,
aliasActions,
securityGroupActions,
showParents = false,
clipboardOnTags = true,
}) => {
@ -85,10 +122,7 @@ const NicCard = memo(
variant="outlined"
className={classes.root}
data-cy={`${dataCy}-${NIC_ID}`}
sx={{
flexWrap: 'wrap',
boxShadow: 'none !important',
}}
sx={{ flexWrap: 'wrap', boxShadow: 'none !important' }}
>
<Box
className={classes.main}
@ -99,6 +133,7 @@ const NicCard = memo(
{`${NIC_ID} | ${NETWORK}`}
</Typography>
<span className={classes.labels}>
{isAlias && <StatusChip stateColor="info" text={'ALIAS'} />}
{noClipboardTags.map((tag) => (
<StatusChip
key={`${dataCy}-${NIC_ID}-${tag.dataCy}`}
@ -121,42 +156,39 @@ const NicCard = memo(
<NicCard
key={alias.NIC_ID}
nic={alias}
actions={aliasActions}
actions={aliasActions?.({ alias })}
showParents={showParents}
/>
))}
</Box>
)}
{Array.isArray(SECURITY_GROUPS) && !!SECURITY_GROUPS?.length && (
<Paper
variant="outlined"
sx={{
display: 'flex',
flexBasis: '100%',
flexDirection: 'column',
gap: '0.5em',
p: '0.8em',
}}
>
<Typography variant="body1">
<Translate word={T.SecurityGroups} />
</Typography>
{useMemo(() => {
if (!Array.isArray(SECURITY_GROUPS) || !SECURITY_GROUPS?.length) {
return null
}
<Stack direction="column" divider={<Divider />} spacing={1}>
{SECURITY_GROUPS?.map((securityGroup, idx) => {
const key = `nic${NIC_ID}-${idx}-${securityGroup.NAME}`
const rulesById = Object.entries(groupBy(SECURITY_GROUPS, 'ID'))
return (
<Accordion TransitionProps={{ unmountOnExit: true }}>
<AccordionSummary>
<Typography variant="body1">
<Translate word={T.SecurityGroups} />
</Typography>
</AccordionSummary>
{rulesById.map(([ID, rules]) => {
const key = `nic-${NIC_ID}-secgroup-${ID}`
const acts = securityGroupActions?.({ securityGroupId: ID })
return (
<SecurityGroupCard
key={key}
data-cy={key}
securityGroup={securityGroup}
/>
<AccordionDetails key={key}>
<SecurityGroupRules id={ID} rules={rules} actions={acts} />
</AccordionDetails>
)
})}
</Stack>
</Paper>
)}
</Accordion>
)
}, [SECURITY_GROUPS])}
</Paper>
)
}
@ -165,11 +197,85 @@ const NicCard = memo(
NicCard.propTypes = {
nic: PropTypes.object,
actions: PropTypes.node,
aliasActions: PropTypes.node,
aliasActions: PropTypes.func,
securityGroupActions: PropTypes.func,
showParents: PropTypes.bool,
clipboardOnTags: PropTypes.bool,
}
NicCard.displayName = 'NicCard'
const SecurityGroupRules = memo(({ 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">
{`#${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={col} noWrap component="span" variant="subtitle2">
<Translate word={col} />
</Typography>
))}
{rules.map((rule, ruleIdx) => (
<SecurityGroupRule key={`${id}-rule-${ruleIdx}`} rule={rule} />
))}
</Box>
</>
)
})
SecurityGroupRules.propTypes = {
id: PropTypes.string,
rules: PropTypes.array,
actions: PropTypes.node,
}
SecurityGroupRules.displayName = 'SecurityGroupRule'
const SecurityGroupRule = memo(({ rule, 'data-cy': cy }) => {
/** @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={cy}
data-cy={`${cy}-${dataCy}`}
variant="subtitle2"
>
{text}
</Typography>
))}
</>
)
})
SecurityGroupRule.propTypes = {
rule: PropTypes.object,
'data-cy': PropTypes.string,
}
SecurityGroupRule.displayName = 'SecurityGroupRule'
export default NicCard

View File

@ -13,46 +13,83 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo } from 'react'
import { memo, ReactElement, useMemo } from 'react'
import PropTypes from 'prop-types'
import { useMediaQuery, Typography } from '@mui/material'
import { User, Group, PcCheck, PcNoEntry, PcWarning } from 'iconoir-react'
import { Typography } from '@mui/material'
import MultipleTags from 'client/components/MultipleTags'
import { rowStyles } from 'client/components/Tables/styles'
import { SecurityGroup } from 'client/constants'
const SecurityGroupCard = memo(({ securityGroup, ...props }) => {
const classes = rowStyles()
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('md'))
const getTotalOfResources = (resources) =>
[resources?.ID ?? []].flat().length || 0
/** @type {SecurityGroup} */
const { ID, NAME, PROTOCOL, RULE_TYPE, ICMP_TYPE, RANGE, NETWORK_ID } =
securityGroup
const SecurityGroupCard = memo(
/**
* @param {object} props - Props
* @param {SecurityGroup} props.securityGroup - Security Group resource
* @param {object} props.rootProps - Props to root component
* @param {ReactElement} [props.actions] - Actions
* @returns {ReactElement} - Card
*/
({ securityGroup, rootProps, actions }) => {
const classes = rowStyles()
const tags = [
{ text: PROTOCOL, dataCy: 'protocol' },
{ text: RULE_TYPE, dataCy: 'ruletype' },
{ text: RANGE, dataCy: 'range' },
{ text: NETWORK_ID, dataCy: 'networkid' },
{ text: ICMP_TYPE, dataCy: 'icmp-type' },
].filter(({ text } = {}) => Boolean(text))
const { ID, NAME, UNAME, GNAME, UPDATED_VMS, OUTDATED_VMS, ERROR_VMS } =
securityGroup
return (
<div data-cy={props['data-cy']} className={classes.title}>
<Typography noWrap component="span" data-cy="name" variant="body2">
{`${ID} | ${NAME}`}
</Typography>
<span className={classes.labels}>
<MultipleTags limitTags={isMobile ? 2 : 5} tags={tags} />
</span>
</div>
)
})
const [totalUpdatedVms, totalOutdatedVms, totalErrorVms] = useMemo(
() => [
getTotalOfResources(UPDATED_VMS),
getTotalOfResources(OUTDATED_VMS),
getTotalOfResources(ERROR_VMS),
],
[UPDATED_VMS?.ID, OUTDATED_VMS?.ID, ERROR_VMS?.ID]
)
return (
<div {...rootProps} data-cy={`secgroup-${ID}`}>
<div className={classes.main}>
<div className={classes.title}>
<Typography component="span">{NAME}</Typography>
</div>
<div className={classes.caption}>
<span>{`#${ID}`}</span>
<span title={`Owner: ${UNAME}`}>
<User />
<span data-cy="uname">{` ${UNAME}`}</span>
</span>
<span title={`Group: ${GNAME}`}>
<Group />
<span data-cy="gname">{` ${GNAME}`}</span>
</span>
<span title={`Total updated VMs: ${totalUpdatedVms}`}>
<PcCheck />
<span>{` ${totalUpdatedVms}`}</span>
</span>
<span title={`Total outdated VMs: ${totalOutdatedVms}`}>
<PcNoEntry />
<span>{` ${totalOutdatedVms}`}</span>
</span>
<span title={`Total error VMs: ${totalErrorVms}`}>
<PcWarning />
<span>{` ${totalErrorVms}`}</span>
</span>
</div>
{actions && <div className={classes.actions}>{actions}</div>}
</div>
</div>
)
}
)
SecurityGroupCard.propTypes = {
securityGroup: PropTypes.object,
'data-cy': PropTypes.string,
rootProps: PropTypes.shape({
className: PropTypes.string,
}),
actions: PropTypes.any,
}
SecurityGroupCard.displayName = 'SecurityGroupCard'

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/Vm/AttachSecGroupForm/schema'
const AttachSecGroupForm = createForm(SCHEMA, FIELDS)
export default AttachSecGroupForm

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 { string, object } from 'yup'
import { SecurityGroupsTable } from 'client/components/Tables'
import { getValidationFromFields } from 'client/utils'
import { T, INPUT_TYPES } from 'client/constants'
const SEC_GROUP = {
name: 'secgroup',
label: T.SelectTheNewSecurityGroup,
type: INPUT_TYPES.TABLE,
Table: () => SecurityGroupsTable,
validation: string()
.trim()
.required()
.default(() => undefined),
grid: { md: 12 },
}
export const FIELDS = [SEC_GROUP]
export const SCHEMA = object(getValidationFromFields(FIELDS))

View File

@ -38,6 +38,13 @@ const VolatileSteps = (configProps) =>
const AttachNicForm = (configProps) =>
AsyncLoadForm({ formPath: 'Vm/AttachNicForm' }, configProps)
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form
*/
const AttachSecGroupForm = (configProps) =>
AsyncLoadForm({ formPath: 'Vm/AttachSecGroupForm' }, configProps)
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateFormCallback} Asynchronous loaded form
@ -144,6 +151,7 @@ const CreateRelativeCharterForm = (configProps) =>
export {
AttachNicForm,
AttachSecGroupForm,
ChangeGroupForm,
ChangeUserForm,
CreateCharterForm,

View File

@ -13,123 +13,153 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useContext, useState, useEffect, createContext } from 'react'
import {
ReactElement,
memo,
useContext,
useState,
useEffect,
Provider,
createContext,
useMemo,
} from 'react'
import PropTypes from 'prop-types'
import root from 'window-or-global'
import { sprintf } from 'sprintf-js'
import { useAuth } from 'client/features/Auth'
import { DEFAULT_LANGUAGE, LANGUAGES_URL } from 'client/constants'
import { isDevelopment } from 'client/utils'
import { LANGUAGES, LANGUAGES_URL } from 'client/constants'
const TranslateContext = createContext()
let languageScript = root.document?.createElement('script')
/**
* @typedef {
* string |
* string[] |
* { word: string, values: string|string[] }
* } WordTranslation - The word to translate
*/
/**
* @typedef {string|string[]} ValuesTranslation
* - The The values to override in the translation
*/
/**
* Checks if the value is valid to translation.
*
* @param {WordTranslation} val - The value to translate
* @returns {boolean} - True if the value can be translated
*/
const labelCanBeTranslated = (val) =>
typeof val === 'string' ||
(Array.isArray(val) && val.length === 2) ||
(typeof val === 'object' && val?.word)
const GenerateScript = (
language = DEFAULT_LANGUAGE,
setHash = () => undefined
) => {
try {
const script = root.document.createElement('script')
script.src = `${LANGUAGES_URL}/${language}.js`
script.async = true
script.onload = () => {
setHash(root.locale)
/**
* Transforms the final string to be translated.
*
* @param {WordTranslation} word - The word to translate
* @param {ValuesTranslation} values - The The values to override in the translation
* @returns {boolean} - True if the value can be translated
*/
const translateString = (word = '', values) => {
const { hash = {} } = useContext(TranslateContext)
const { [word]: wordVal } = hash
const translation = useMemo(() => {
if (!wordVal) return word
if (!Array.isArray(wordVal)) return wordVal
try {
return sprintf(...wordVal)
} catch {
return word
}
root.document.body.appendChild(script)
languageScript = script
} catch (error) {
isDevelopment() &&
console.error('Error while generating script language', error)
}
}
const RemoveScript = () => {
root.document.body.removeChild(languageScript)
}, [word, values])
return translation
}
/**
* Provider for the translate context.
*
* @param {object} props - The props of the provider
* @param {any} props.children - Children
* @returns {Provider} - The translation provider
*/
const TranslateProvider = ({ children = [] }) => {
const [hash, setHash] = useState({})
const { settings: { LANG: lang } = {} } = useAuth()
useEffect(() => {
GenerateScript(lang, setHash)
if (!lang || !LANGUAGES[lang]) return
return () => {
RemoveScript()
try {
const script = root.document.createElement('script', {})
script.src = `${LANGUAGES_URL}/${lang}.js`
script.async = true
script.onload = () => setHash(root.locale)
root.document.body.appendChild(script)
languageScript = script
} catch (error) {
isDevelopment() &&
console.error('Error while generating script language', error)
}
return () => root.document.body.removeChild(languageScript)
}, [lang])
const changeLang = (language = DEFAULT_LANGUAGE) => {
RemoveScript()
GenerateScript(language, setHash)
}
return (
<TranslateContext.Provider value={{ lang, hash, changeLang }}>
<TranslateContext.Provider value={{ lang, hash }}>
{children}
</TranslateContext.Provider>
)
}
const translateString = (str = '', values) => {
const context = useContext(TranslateContext)
let key = str
/**
* Function to translate a label.
*
* @param {WordTranslation} word - The label to translate
* @returns {string} - The translated label
*/
const Tr = (word = '') => {
const [w = '', v] = Array.isArray(word) ? word : [word]
const ensuredValues = !Array.isArray(v) ? [v] : v
if (context?.hash?.[key]) {
key = context.hash[key]
}
if (key && Array.isArray(values)) {
try {
key = sprintf(key, ...values)
} catch (e) {
return str
}
}
return key
return translateString(w, ensuredValues.filter(Boolean))
}
const Tr = (str = '') => {
let key = str
let values
if (Array.isArray(str)) {
key = str[0] || ''
values = str[1]
}
const valuesTr = !Array.isArray(values) ? [values] : values
return translateString(key, valuesTr.filter(Boolean))
}
const Translate = ({ word = '', values = [] }) => {
/**
* Translate component.
*
* @param {object} props - The props of the component
* @param {WordTranslation} props.word - The word to translate
* @param {string|string[]} [props.values] - The values to override in the translation
* @returns {ReactElement} - The translated component
*/
const Translate = memo(({ word = '', values = [] }) => {
const [w, v = values] = Array.isArray(word) ? word : [word, values]
const valuesTr = !Array.isArray(v) ? [v] : v
const ensuredValues = !Array.isArray(v) ? [v] : v
const translation = translateString(w, ensuredValues.filter(Boolean))
return translateString(w, valuesTr)
}
return <>{translation}</>
})
TranslateProvider.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
}
TranslateProvider.propTypes = { children: PropTypes.any }
Translate.propTypes = {
word: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
values: PropTypes.any,
}
Tr.displayName = 'Tr'
Translate.displayName = 'Translate'
export {
TranslateContext,
TranslateProvider,

View File

@ -102,7 +102,7 @@ const HeaderPopover = memo(
placement="bottom-end"
keepMounted={false}
style={{
zIndex: zIndex.appBar + 1,
zIndex: zIndex.modal + 1,
...mobileStyles,
}}
{...popperProps}

View File

@ -19,7 +19,7 @@ import hostApi from 'client/features/OneApi/host'
import { HostCard } from 'client/components/Cards'
const Row = memo(
({ original, ...props }) => {
({ original, value, ...props }) => {
const detail = hostApi.endpoints.getHosts.useQueryState(undefined, {
selectFromResult: ({ data }) =>
[data ?? []].flat().find((host) => +host?.ID === +original.ID),

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. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
const getTotalOfResources = (resources) =>
[resources?.ID ?? []].flat().length || 0
export default [
{ Header: 'ID', accessor: 'ID', sortType: 'number' },
{ Header: 'Name', accessor: 'NAME' },
{ Header: 'Owner', accessor: 'UNAME' },
{ Header: 'Group', accessor: 'GNAME' },
{
Header: 'Updated VMs',
id: 'UPDATED_VMS',
accessor: (row) => getTotalOfResources(row?.UPDATED_VMS),
sortType: 'number',
},
{
Header: 'Outdated VMs',
id: 'OUTDATED_VMS',
accessor: (row) => getTotalOfResources(row?.OUTDATED_VMS),
sortType: 'number',
},
{
Header: 'Updating VMs',
id: 'UPDATING_VMS',
accessor: (row) => getTotalOfResources(row?.UPDATING_VMS),
sortType: 'number',
},
{
Header: 'Error VMs',
id: 'ERROR_VMS',
accessor: (row) => getTotalOfResources(row?.ERROR_VMS),
sortType: 'number',
},
]

View File

@ -0,0 +1,67 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useMemo, ReactElement } from 'react'
import { useViews } from 'client/features/Auth'
import { useGetSecGroupsQuery } from 'client/features/OneApi/securityGroup'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import SecurityGroupColumns from 'client/components/Tables/SecurityGroups/columns'
import SecurityGroupsRow from 'client/components/Tables/SecurityGroups/row'
import { RESOURCE_NAMES } from 'client/constants'
const DEFAULT_DATA_CY = 'secgroup'
/**
* @param {object} props - Props
* @returns {ReactElement} Security Groups table
*/
const SecurityGroupsTable = (props) => {
const { rootProps = {}, searchProps = {}, ...rest } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useViews()
const { data = [], isFetching, refetch } = useGetSecGroupsQuery()
const columns = useMemo(
() =>
createColumns({
filters: getResourceView(RESOURCE_NAMES.SEC_GROUP)?.filters,
columns: SecurityGroupColumns,
}),
[view]
)
return (
<EnhancedTable
columns={columns}
data={useMemo(() => data, [data])}
rootProps={rootProps}
searchProps={searchProps}
refetch={refetch}
isLoading={isFetching}
getRowId={(row) => String(row.ID)}
RowComponent={SecurityGroupsRow}
{...rest}
/>
)
}
SecurityGroupsTable.propTypes = { ...EnhancedTable.propTypes }
SecurityGroupsTable.displayName = 'SecurityGroupsTable'
export default SecurityGroupsTable

View File

@ -0,0 +1,44 @@
/* ------------------------------------------------------------------------- *
* 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 } from 'react'
import PropTypes from 'prop-types'
import secGroupApi from 'client/features/OneApi/securityGroup'
import { SecurityGroupCard } from 'client/components/Cards'
const Row = memo(
({ original, value, ...props }) => {
const state = secGroupApi.endpoints.getSecGroups.useQueryState(undefined, {
selectFromResult: ({ data = [] }) =>
data.find((secgroup) => +secgroup.ID === +original.ID),
})
return (
<SecurityGroupCard securityGroup={state ?? original} rootProps={props} />
)
},
(prev, next) => prev.className === next.className
)
Row.propTypes = {
original: PropTypes.object,
value: PropTypes.object,
isSelected: PropTypes.bool,
handleClick: PropTypes.func,
}
Row.displayName = 'SecurityGroupRow'
export default Row

View File

@ -19,7 +19,7 @@ import vmTemplateApi from 'client/features/OneApi/vmTemplate'
import { VmTemplateCard } from 'client/components/Cards'
const Row = memo(
({ original, ...props }) => {
({ original, value, ...props }) => {
const state = vmTemplateApi.endpoints.getTemplates.useQueryState(
undefined,
{

View File

@ -22,6 +22,7 @@ import HostsTable from 'client/components/Tables/Hosts'
import ImagesTable from 'client/components/Tables/Images'
import MarketplaceAppsTable from 'client/components/Tables/MarketplaceApps'
import MarketplacesTable from 'client/components/Tables/Marketplaces'
import SecurityGroupsTable from 'client/components/Tables/SecurityGroups'
import SkeletonTable from 'client/components/Tables/Skeleton'
import UsersTable from 'client/components/Tables/Users'
import VirtualizedTable from 'client/components/Tables/Virtualized'
@ -44,6 +45,7 @@ export {
ImagesTable,
MarketplaceAppsTable,
MarketplacesTable,
SecurityGroupsTable,
UsersTable,
VmsTable,
VmTemplatesTable,

View File

@ -49,7 +49,7 @@ export const rowStyles = makeStyles(
main: {
flex: 'auto',
overflow: 'hidden',
alignSelf: 'start',
alignSelf: 'center',
},
title: {
color: palette.text.primary,
@ -57,10 +57,10 @@ export const rowStyles = makeStyles(
gap: 6,
alignItems: 'center',
flexWrap: 'wrap',
marginBottom: 8,
},
labels: {
display: 'inline-flex',
alignItems: 'center',
gap: 6,
},
caption: {

View File

@ -15,14 +15,16 @@
* ------------------------------------------------------------------------- */
import { memo } from 'react'
import PropTypes from 'prop-types'
import { Edit, Trash } from 'iconoir-react'
import { Edit, Trash, ShieldAdd, ShieldCross } from 'iconoir-react'
import {
useAttachNicMutation,
useDetachNicMutation,
useAttachSecurityGroupMutation,
useDetachSecurityGroupMutation,
} from 'client/features/OneApi/vm'
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
import { AttachNicForm } from 'client/components/Forms/Vm'
import { AttachNicForm, AttachSecGroupForm } from 'client/components/Forms/Vm'
import { jsonToXml } from 'client/models/Helper'
import { Tr, Translate } from 'client/components/HOC'
@ -115,11 +117,84 @@ const DetachAction = memo(({ vmId, nic, onSubmit, sx }) => {
)
})
const AttachSecGroupAction = memo(({ vmId, nic, onSubmit, sx }) => {
const [attachSecGroup] = useAttachSecurityGroupMutation()
const { NIC_ID } = nic
const handleAttachNic = async ({ secgroup } = {}) => {
const handleAttachSecGroup = onSubmit ?? attachSecGroup
secgroup !== undefined &&
(await handleAttachSecGroup({ id: vmId, nic: NIC_ID, secgroup }))
}
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': `attach-secgroup-${NIC_ID}`,
icon: <ShieldAdd />,
tooltip: Tr(T.AttachSecurityGroup),
sx,
}}
options={[
{
dialogProps: {
title: T.AttachSecurityGroup,
dataCy: 'modal-attach-secgroup',
},
form: AttachSecGroupForm,
onSubmit: handleAttachNic,
},
]}
/>
)
})
const DetachSecGroupAction = memo(
({ vmId, nic, securityGroupId, onSubmit, sx }) => {
const [detachSecGroup] = useDetachSecurityGroupMutation()
const { NIC_ID } = nic
const handleDetachNic = async () => {
const handleDetachSecGroup = onSubmit ?? detachSecGroup
const data = { id: vmId, nic: NIC_ID, secgroup: securityGroupId }
await handleDetachSecGroup(data)
}
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': `detach-secgroup-${securityGroupId}-from-${NIC_ID}`,
icon: <ShieldCross />,
tooltip: Tr(T.DetachSecurityGroup),
sx,
}}
options={[
{
isConfirmDialog: true,
dialogProps: {
title: (
<Translate
word={T.DetachSecurityGroupFromNic}
values={[`#${securityGroupId}`, `#${NIC_ID}`]}
/>
),
children: <p>{Tr(T.DoYouWantProceed)}</p>,
},
onSubmit: handleDetachNic,
},
]}
/>
)
}
)
const ActionPropTypes = {
vmId: PropTypes.string,
hypervisor: PropTypes.string,
currentNics: PropTypes.array,
nic: PropTypes.object,
securityGroupId: PropTypes.string,
onSubmit: PropTypes.func,
sx: PropTypes.object,
}
@ -128,5 +203,14 @@ AttachAction.propTypes = ActionPropTypes
AttachAction.displayName = 'AttachActionButton'
DetachAction.propTypes = ActionPropTypes
DetachAction.displayName = 'DetachActionButton'
AttachSecGroupAction.propTypes = ActionPropTypes
AttachSecGroupAction.displayName = 'AttachSecGroupButton'
DetachSecGroupAction.propTypes = ActionPropTypes
DetachSecGroupAction.displayName = 'DetachSecGroupButton'
export { AttachAction, DetachAction }
export {
AttachAction,
DetachAction,
AttachSecGroupAction,
DetachSecGroupAction,
}

View File

@ -22,6 +22,8 @@ import NicCard from 'client/components/Cards/NicCard'
import {
AttachAction,
DetachAction,
AttachSecGroupAction,
DetachSecGroupAction,
} from 'client/components/Tabs/Vm/Network/Actions'
import {
@ -32,7 +34,7 @@ import {
import { getActionsAvailable } from 'client/models/Helper'
import { VM_ACTIONS } from 'client/constants'
const { ATTACH_NIC, DETACH_NIC } = VM_ACTIONS
const { ATTACH_NIC, DETACH_NIC, ATTACH_SEC_GROUP } = VM_ACTIONS
/**
* Renders the list of networks from a VM.
@ -76,8 +78,27 @@ const VmNetworkTab = ({ tabProps: { actions } = {}, id }) => {
key={key}
nic={nic}
actions={
<>
{actionsAvailable.includes(DETACH_NIC) && (
<DetachAction nic={nic} vmId={id} />
)}
{actionsAvailable.includes(ATTACH_SEC_GROUP) && (
<AttachSecGroupAction nic={nic} vmId={id} />
)}
</>
}
aliasActions={({ alias }) =>
actionsAvailable.includes(DETACH_NIC) && (
<DetachAction nic={nic} vmId={id} />
<DetachAction nic={alias} vmId={id} />
)
}
securityGroupActions={({ securityGroupId }) =>
actionsAvailable.includes(DETACH_NIC) && (
<DetachSecGroupAction
nic={nic}
vmId={id}
securityGroupId={securityGroupId}
/>
)
}
/>

View File

@ -13,28 +13,37 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import {
defaultApps,
defaultAppName,
availableLanguages,
} from 'server/utils/constants/defaults'
import * as Setting from 'client/constants/setting'
import { isBackend } from 'client/utils/environments'
export const JWT_NAME = 'FireedgeToken'
export const BY = {
text: 'by OpenNebula',
url: 'https://opennebula.io/',
}
export const BY = { text: 'by OpenNebula', url: 'https://opennebula.io/' }
export const _APPS = { ...defaultApps }
export const APPS = Object.keys(defaultApps)
export const APPS_IN_BETA = [_APPS.sunstone.name]
export const APPS_WITH_SWITCHER = [_APPS.sunstone.name]
export const APP_URL = defaultAppName ? `/${defaultAppName}` : ''
/**
* Server side constants (not all of them are used in client)
* Check `window.__PRELOADED_CONFIG__` in src/server/routes/entrypoints/App.js
*
* @type {object} - Server configuration
*/
export const SERVER_CONFIG = (() => {
if (isBackend()) return {}
const config = { ...(window.__PRELOADED_CONFIG__ ?? {}) }
delete window.__PRELOADED_CONFIG__
return config
})()
// should be equal to the apps in src/server/utils/constants/defaults.js
export const _APPS = { sunstone: 'sunstone', provision: 'provision' }
export const APPS = Object.keys(_APPS)
export const APPS_IN_BETA = [_APPS.sunstone]
export const APPS_WITH_SWITCHER = [_APPS.sunstone]
export const APP_URL = '/fireedge'
export const WEBSOCKET_URL = `${APP_URL}/websockets`
export const STATIC_FILES_URL = `${APP_URL}/client/assets`
export const IMAGES_URL = `${STATIC_FILES_URL}/images`
export const LOGO_IMAGES_URL = `${IMAGES_URL}/logos`
export const PROVIDER_IMAGES_URL = `${IMAGES_URL}/providers`
@ -47,9 +56,50 @@ export const FONTS_URL = `${STATIC_FILES_URL}/fonts`
export const SCHEMES = Setting.SCHEMES
export const DEFAULT_SCHEME = Setting.SCHEMES.SYSTEM
export const LANGUAGES = availableLanguages
export const DEFAULT_LANGUAGE = 'en'
export const CURRENCY = SERVER_CONFIG?.currency ?? 'EUR'
export const DEFAULT_LANGUAGE = SERVER_CONFIG?.default_lang ?? 'en'
export const LANGUAGES_URL = `${STATIC_FILES_URL}/languages`
export const LANGUAGES = SERVER_CONFIG.langs ?? {
bg_BG: 'Bulgarian (Bulgaria)',
bg: 'Bulgarian',
ca: 'Catalan',
cs_CZ: 'Czech',
da: 'Danish',
de_CH: 'German (Switzerland)',
de: 'German',
el_GR: 'Greek (Greece)',
en: 'English',
es_ES: 'Spanish',
et_EE: 'Estonian',
fa_IR: 'Persian (Iran)',
fa: 'Persian',
fr_CA: 'French (Canada)',
fr_FR: 'French',
hu_HU: 'Hungary',
it_IT: 'Italian',
ja: 'Japanese',
ka: 'Georgian',
lt_LT: 'Lithuanian',
nl_NL: 'Dutch',
pl: 'Polish',
pt_PT: 'Portuguese',
ro_RO: 'Romanian',
ru_RU: 'Russian',
ru: 'Russian',
si: 'Sinhala',
sk_SK: 'Slavak',
sr_RS: 'Serbian',
sv: 'Swedish',
th_TH: 'Thai (Thailand)',
th: 'Thai',
tr_TR: 'Turkish (Turkey)',
tr: 'Turkish',
uk_UA: 'Ukrainian (Ukraine)',
uk: 'Ukrainian',
vi: 'Vietnamese',
zh_CN: 'Chinese (China)',
zh_TW: 'Chinese (Taiwan)',
}
export const ONEADMIN_ID = '0'
export const SERVERADMIN_ID = '1'

View File

@ -16,7 +16,7 @@
import { T } from 'client/constants'
/**
* @typedef {object} SecurityGroupRule
* @typedef SecurityGroupRule
* @property {number|string} SECURITY_GROUP_ID - ID
* @property {string} SECURITY_GROUP_NAME - Name
* @property {string} PROTOCOL - Protocol
@ -30,6 +30,21 @@ import { T } from 'client/constants'
* @property {string} [MAC] - Network MAC
*/
/**
* @typedef PrettySecurityGroupRule
* @property {string} ID - ID
* @property {string} NAME - Name
* @property {PROTOCOL_STRING} PROTOCOL - Protocol
* @property {RULE_TYPE_STRING} RULE_TYPE - Rule type
* @property {ICMP_STRING} ICMP_TYPE - ICMP type
* @property {ICMP_V6_STRING} [ICMPv6_TYPE] - ICMP v6 type
* @property {string|'All'} [RANGE] - Range
* @property {string} [NETWORK_ID] - Network id
* @property {string} [SIZE] - Network size
* @property {string} [IP] - Network IP
* @property {string} [MAC] - Network MAC
*/
/**
* ICMP Codes for each ICMP type as in:
* http://www.iana.org/assignments/icmp-parameters/

View File

@ -120,6 +120,7 @@ module.exports = {
SelectTheNewDatastore: 'Select the new datastore',
SelectTheNewGroup: 'Select the new group',
SelectTheNewOwner: 'Select the new owner',
SelectTheNewSecurityGroup: 'Select the new security group',
SelectVmTemplate: 'Select a VM Template',
SelectYourActiveGroup: 'Select your active group',
Share: 'Share',
@ -440,6 +441,9 @@ module.exports = {
OverrideNetworkValuesIPv6: 'Override Network Values IPv6',
OverrideNetworkInboundTrafficQos: 'Override Network Inbound Traffic QoS',
OverrideNetworkOutboundTrafficQos: 'Override Network Outbound Traffic QoS',
AttachSecurityGroup: 'Attach Security Group',
DetachSecurityGroup: 'Detach Security Group',
DetachSecurityGroupFromNic: 'Detach Security Group %1$s from NIC %2$s',
/* VM schema - snapshot */
VmSnapshotNameConcept: 'The new snapshot name. It can be empty',
/* VM schema - actions */
@ -752,6 +756,9 @@ module.exports = {
IPSEC: 'IPsec',
Outbound: 'Outbound',
Inbound: 'Inbound',
Any: 'Any',
Protocol: 'Protocol',
IcmpType: 'ICMP Type',
/* Host schema */
IM_MAD: 'IM MAD',

View File

@ -772,6 +772,8 @@ export const VM_ACTIONS = {
// NETWORK
ATTACH_NIC: 'attach_nic',
DETACH_NIC: 'detach_nic',
ATTACH_SEC_GROUP: 'attach_secgroup',
DETACH_SEC_GROUP: 'detach_secgroup',
// SNAPSHOT
SNAPSHOT_CREATE: 'snapshot_create',
@ -919,6 +921,8 @@ export const VM_ACTIONS_BY_STATE = {
// NETWORK
[VM_ACTIONS.ATTACH_NIC]: [STATES.POWEROFF, STATES.RUNNING],
[VM_ACTIONS.DETACH_NIC]: [STATES.POWEROFF, STATES.RUNNING],
[VM_ACTIONS.ATTACH_SEC_GROUP]: [STATES.POWEROFF, STATES.RUNNING],
[VM_ACTIONS.DETACH_SEC_GROUP]: [STATES.POWEROFF, STATES.RUNNING],
// SNAPSHOT
[VM_ACTIONS.SNAPSHOT_CREATE]: [],

View File

@ -14,16 +14,17 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { object, boolean, string } from 'yup'
import { arrayToOptions, getValidationFromFields } from 'client/utils'
import {
T,
INPUT_TYPES,
SCHEMES,
LANGUAGES,
DEFAULT_SCHEME,
DEFAULT_LANGUAGE,
} from 'client/constants'
import { getValidationFromFields } from 'client/utils'
const SCHEME = {
const SCHEME_FIELD = {
name: 'SCHEME',
label: T.Schema,
type: INPUT_TYPES.SELECT,
@ -39,12 +40,16 @@ const SCHEME = {
grid: { md: 12 },
}
const LANGUAGES = {
const LANG_FIELD = {
name: 'LANG',
label: T.Language,
type: INPUT_TYPES.SELECT,
values: () =>
window?.langs?.map(({ key, value }) => ({ text: value, value: key })) ?? [],
arrayToOptions(Object.entries(LANGUAGES), {
addEmpty: false,
getText: ([, text]) => text,
getValue: ([value]) => value,
}),
validation: string()
.trim()
.required()
@ -52,7 +57,7 @@ const LANGUAGES = {
grid: { md: 12 },
}
const DISABLE_ANIMATIONS = {
const DISABLE_ANIMATIONS_FIELD = {
name: 'DISABLE_ANIMATIONS',
label: T.DisableDashboardAnimations,
type: INPUT_TYPES.CHECKBOX,
@ -62,6 +67,6 @@ const DISABLE_ANIMATIONS = {
grid: { md: 12 },
}
export const FORM_FIELDS = [SCHEME, LANGUAGES, DISABLE_ANIMATIONS]
export const FORM_FIELDS = [SCHEME_FIELD, LANG_FIELD, DISABLE_ANIMATIONS_FIELD]
export const FORM_SCHEMA = object(getValidationFromFields(FORM_FIELDS))

View File

@ -21,10 +21,16 @@ import { name as generalSlice } from 'client/features/General/slice'
import { name as authSlice, actions } from 'client/features/Auth/slice'
import groupApi from 'client/features/OneApi/group'
import systemApi from 'client/features/OneApi/system'
import { _APPS, RESOURCE_NAMES, ONEADMIN_ID } from 'client/constants'
import { ResourceView } from 'client/apps/sunstone/routes'
import {
_APPS,
RESOURCE_NAMES,
ONEADMIN_ID,
DEFAULT_SCHEME,
DEFAULT_LANGUAGE,
} from 'client/constants'
const APPS_WITH_VIEWS = [_APPS.sunstone.name].map((app) => app.toLowerCase())
const APPS_WITH_VIEWS = [_APPS.sunstone].map((app) => app.toLowerCase())
const appNeedViews = () => {
const { appTitle } = useSelector((state) => state[generalSlice], shallowEqual)
@ -38,7 +44,7 @@ const appNeedViews = () => {
export const useAuth = () => {
const auth = useSelector((state) => state[authSlice], shallowEqual)
const { jwt, user, view, settings, isLoginInProgress } = auth
const { jwt, user, view, isLoginInProgress } = auth
const waitViewToLogin = appNeedViews() ? !!view : true
@ -61,8 +67,13 @@ export const useAuth = () => {
user,
isOneAdmin: user?.ID === ONEADMIN_ID,
groups: authGroups,
// Merge user settings with the existing one
settings: { ...settings, ...(user?.TEMPLATE?.FIREEDGE ?? {}) },
// Merge user settings with the defaults
settings: {
SCHEME: DEFAULT_SCHEME,
LANG: DEFAULT_LANGUAGE,
DISABLE_ANIMATIONS: 'NO',
...(user?.TEMPLATE?.FIREEDGE ?? {}),
},
isLogged:
!!jwt &&
!!user &&

View File

@ -16,12 +16,7 @@
import { createAction, createSlice } from '@reduxjs/toolkit'
import { removeStoreData } from 'client/utils'
import {
JWT_NAME,
FILTER_POOL,
DEFAULT_SCHEME,
DEFAULT_LANGUAGE,
} from 'client/constants'
import { JWT_NAME, FILTER_POOL } from 'client/constants'
export const logout = createAction('logout')
@ -29,11 +24,6 @@ const initial = () => ({
jwt: null,
user: null,
filterPool: FILTER_POOL.ALL_RESOURCES,
settings: {
SCHEME: DEFAULT_SCHEME,
LANG: DEFAULT_LANGUAGE,
DISABLE_ANIMATIONS: 'NO',
},
isLoginInProgress: false,
})
@ -48,9 +38,6 @@ const slice = createSlice({
changeJwt: (state, { payload }) => {
state.jwt = payload
},
changeSettings: (state, { payload }) => {
state.settings = { ...state.settings, payload }
},
changeFilterPool: (state, { payload: filterPool }) => {
state.filterPool = filterPool
state.isLoginInProgress = false

View File

@ -531,8 +531,8 @@ const vmApi = oneApi.injectEndpoints({
* Detaches a network interface from a virtual machine.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - Virtual machine id
* @param {string|number} params.nic - NIC id
* @param {string} params.id - Virtual machine id
* @param {string} params.nic - NIC id
* @returns {number} Virtual machine id
* @throws Fails when response isn't code 200
*/
@ -544,6 +544,46 @@ const vmApi = oneApi.injectEndpoints({
},
invalidatesTags: (_, __, { id }) => [{ type: VM, id }],
}),
attachSecurityGroup: builder.mutation({
/**
* Attaches a security group to a network interface of a VM,
* if the VM is running it updates the associated rules.
*
* @param {object} params - Request parameters
* @param {string} params.id - Virtual machine id
* @param {string} params.nic - The NIC ID
* @param {string} params.secgroup - The Security Group ID, which should be added to the NIC
* @returns {number} Virtual machine id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.VM_SEC_GROUP_ATTACH
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: VM, id }],
}),
detachSecurityGroup: builder.mutation({
/**
* Detaches a security group from a network interface of a VM,
* if the VM is running it removes the associated rules.
*
* @param {object} params - Request parameters
* @param {string} params.id - Virtual machine id
* @param {string} params.nic - The NIC ID
* @param {string} params.secgroup - The Security Group ID
* @returns {number} Virtual machine id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.VM_SEC_GROUP_DETACH
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: VM, id }],
}),
changeVmPermissions: builder.mutation({
/**
* Changes the permission bits of a virtual machine.
@ -962,6 +1002,8 @@ export const {
useResizeDiskMutation,
useAttachNicMutation,
useDetachNicMutation,
useAttachSecurityGroupMutation,
useDetachSecurityGroupMutation,
useChangeVmPermissionsMutation,
useChangeVmOwnershipMutation,
useRenameVmMutation,

View File

@ -21,8 +21,13 @@ import {
J2xOptions,
} from 'fast-xml-parser'
import { T, UserInputObject, USER_INPUT_TYPES } from 'client/constants'
import { camelCase } from 'client/utils'
import {
T,
UserInputObject,
USER_INPUT_TYPES,
SERVER_CONFIG,
} from 'client/constants'
/**
* @param {object} json - JSON
@ -84,8 +89,8 @@ export const stringToBoolean = (str) =>
*/
export const formatNumberByCurrency = (number, options) => {
try {
const currency = window?.currency ?? 'EUR'
const locale = window?.lang?.replace('_', '-') ?? undefined
const currency = SERVER_CONFIG?.currency ?? 'EUR'
const locale = SERVER_CONFIG?.lang?.replace('_', '-') ?? undefined
return Intl.NumberFormat(locale, {
style: 'currency',

View File

@ -20,25 +20,14 @@ import {
ICMP_STRING,
ICMP_V6_STRING,
SecurityGroupRule,
PrettySecurityGroupRule,
} from 'client/constants'
/**
* Converts a security group attributes into a readable format.
*
* @param {SecurityGroupRule} securityGroup - Security group
* @returns {{
* SECURITY_GROUP_ID: number|string,
* SECURITY_GROUP_NAME: string,
* PROTOCOL: PROTOCOL_STRING,
* RULE_TYPE: RULE_TYPE_STRING,
* ICMP_TYPE: ICMP_STRING,
* ICMPv6_TYPE: ICMP_V6_STRING,
* RANGE: string,
* NETWORK_ID: number|string,
* SIZE: number|string,
* IP: string,
* MAC: string
* }} Readable attributes
* @returns {PrettySecurityGroupRule} Readable attributes
*/
export const prettySecurityGroup = ({
SECURITY_GROUP_ID: ID,
@ -48,6 +37,7 @@ export const prettySecurityGroup = ({
ICMP_TYPE: icmpType,
ICMPv6_TYPE: icmpv6Type,
RANGE: range,
NETWORK_ID: networkId,
...rest
}) => ({
ID,
@ -57,6 +47,7 @@ export const prettySecurityGroup = ({
ICMP_TYPE: ICMP_STRING[+icmpType] ?? '',
ICMPv6_TYPE: ICMP_V6_STRING[+icmpv6Type] ?? '',
RANGE: range || T.All,
NETWORK_ID: networkId ?? T.Any,
...rest,
})

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 { ReactElement, useEffect } from 'react'
import PropTypes from 'prop-types'
/**
* Provider component to preload configuration from server.
*
* @param {object} props - Props
* @param {any} props.children - Children
* @returns {ReactElement} React element
*/
const PreloadConfigProvider = ({ children }) => {
useEffect(() => {
const preload = document.querySelector('#preload-server-side')
if (preload) {
// remove preload script from DOM after it's loaded
preload.parentElement.removeChild(preload)
}
}, [])
return <>{children}</>
}
PreloadConfigProvider.propTypes = {
children: PropTypes.node,
}
export default PreloadConfigProvider

View File

@ -24,6 +24,8 @@ const { store } = createStore({
extraMiddleware: [onlyForOneadminMiddleware],
})
delete window.__PRELOADED_STATE__
const rootHTML = document.getElementById('root')?.innerHTML
const renderMethod = rootHTML !== '' ? hydrate : render

View File

@ -20,6 +20,8 @@ import App from 'client/apps/sunstone'
const { store } = createStore({ initState: window.__PRELOADED_STATE__ })
delete window.__PRELOADED_STATE__
const rootHTML = document.getElementById('root')?.innerHTML
const renderMethod = rootHTML !== '' ? hydrate : render

View File

@ -21,6 +21,14 @@ const defaultTheme = createTheme()
const { grey } = colors
const black = '#1D1D1D'
const white = '#ffffff'
const bgBlueGrey = '#f2f4f8'
const defaultPrimary = {
light: '#2a2d3d',
main: '#222431',
dark: '#191924',
contrastText: '#ffffff',
}
const systemFont = [
'-apple-system',
@ -70,9 +78,11 @@ const buttonSvgStyle = {
* @returns {ThemeOptions} Material theme options
*/
export default (appTheme, mode = SCHEMES.DARK) => {
const { primary, secondary } = appTheme.palette
const isDarkMode = `${mode}`.toLowerCase() === SCHEMES.DARK
const { primary = defaultPrimary, secondary } = appTheme?.palette || {}
const defaultContrastText = isDarkMode ? white : 'rgba(0, 0, 0, 0.87)'
return {
palette: {
mode,
@ -84,7 +94,7 @@ export default (appTheme, mode = SCHEMES.DARK) => {
},
background: {
paper: isDarkMode ? primary.light : white,
default: isDarkMode ? primary.main : '#f2f4f8',
default: isDarkMode ? primary.main : bgBlueGrey,
},
error: {
100: '#e98e7f',
@ -112,13 +122,13 @@ export default (appTheme, mode = SCHEMES.DARK) => {
light: '#ffe4a3',
main: '#f1a204',
dark: '#f1a204',
contrastText: 'rgba(0, 0, 0, 0.87)',
contrastText: defaultContrastText,
},
info: {
light: '#64b5f6',
main: '#2196f3',
dark: '#01579b',
contrastText: white,
contrastText: defaultContrastText,
},
success: {
100: '#bce1bd',
@ -132,13 +142,13 @@ export default (appTheme, mode = SCHEMES.DARK) => {
light: '#3adb76',
main: '#4caf50',
dark: '#388e3c',
contrastText: white,
contrastText: defaultContrastText,
},
debug: {
light: '#e0e0e0',
main: '#757575',
dark: '#424242',
contrastText: isDarkMode ? white : black,
light: grey[300],
main: grey[600],
dark: grey[800],
contrastText: defaultContrastText,
},
},
breakpoints: {
@ -414,7 +424,7 @@ export default (appTheme, mode = SCHEMES.DARK) => {
MuiToggleButtonGroup: {
styleOverrides: {
root: {
backgroundColor: isDarkMode ? primary.main : '#f2f4f8',
backgroundColor: 'background.default',
},
},
defaultProps: {

View File

@ -339,14 +339,14 @@ export const set = (obj, path, value) => {
* @returns {object} Objects group by the property
*/
export const groupBy = (list, key) =>
list.reduce((objectsByKeyValue, obj) => {
list?.reduce((objectsByKeyValue, obj) => {
const keyValue = get(obj, key)
const newValue = (objectsByKeyValue[keyValue] || []).concat(obj)
set(objectsByKeyValue, keyValue, newValue)
return objectsByKeyValue
}, {})
}, {}) ?? {}
/**
* Clone an object.
@ -460,3 +460,21 @@ export const isDivisibleBy = (number, divisor) => !(number % divisor)
*/
export const getFactorsOfNumber = (value) =>
[...Array(+value + 1).keys()].filter((idx) => value % idx === 0)
/**
* Returns an array with the separator interspersed between elements of the given array.
*
* @param {any} arr - Array
* @param {any} sep - Separator
* @returns {number[]} Returns list of numbers
* @example [1,2,3].intersperse(0) => [1,0,2,0,3]
*/
export const intersperse = (arr, sep) => {
const ensuredArr = (Array.isArray(arr) ? arr : [arr]).filter(Boolean)
if (ensuredArr.length === 0) return []
return ensuredArr
.slice(1)
.reduce((xs, x, i) => xs.concat([sep, x]), [ensuredArr[0]])
}

View File

@ -21,26 +21,31 @@ const root = require('window-or-global')
const { createStore, compose, applyMiddleware } = require('redux')
const thunk = require('redux-thunk').default
const { ServerStyleSheets } = require('@mui/styles')
const rootReducer = require('client/store/reducers')
// server side constants (not all of them are used in client)
const { getFireedgeConfig } = require('server/utils/yml')
const {
availableLanguages,
defaultCurrency,
defaultApps,
} = require('server/utils/constants/defaults')
const { APP_URL, STATIC_FILES_URL } = require('client/constants')
const { defaultApps } = require('server/utils/constants/defaults')
// client
const rootReducer = require('client/store/reducers')
const { upperCaseFirst } = require('client/utils')
const { APP_URL, STATIC_FILES_URL } = require('client/constants')
// settings
const appConfig = getFireedgeConfig()
const currency = appConfig.currency || defaultCurrency
const langs = appConfig.langs || availableLanguages
const ALLOWED_KEYS_FROM_CONFIG = ['currency', 'default_lang', 'langs']
const languages = Object.keys(langs)
const scriptLanguages = languages.map((language) => ({
key: language,
value: `${langs[language]}`,
}))
const ensuredConfig = Object.entries(getFireedgeConfig()).reduce(
(config, [key, value]) => {
if (ALLOWED_KEYS_FROM_CONFIG.includes(key)) {
config[key] = value
}
return config
},
{}
)
const ensuredScriptValue = (value) =>
JSON.stringify(value).replace(/</g, '\\u003c')
const router = Router()
@ -61,9 +66,9 @@ router.get('*', (req, res) => {
composeEnhancer(applyMiddleware(thunk))
)
const storeRender = `<script id="preloadState">window.__PRELOADED_STATE__ = ${JSON.stringify(
const storeRender = `<script id="preloadState">window.__PRELOADED_STATE__ = ${ensuredScriptValue(
store.getState()
).replace(/</g, '\\u003c')}</script>`
)}</script>`
const App = require(`../../../client/apps/${appName}/index.js`).default
@ -90,8 +95,9 @@ router.get('*', (req, res) => {
<body>
<div id="root">${rootComponent}</div>
${storeRender}
<script>${`langs = ${JSON.stringify(scriptLanguages)}`}</script>
<script>${`currency = ${JSON.stringify(currency)}`}</script>
<script id="preload-server-side">
${`window.__PRELOADED_CONFIG__ = ${ensuredScriptValue(ensuredConfig)}`}
</script>
<script src='${APP_URL}/client/bundle.${appName}.js'></script>
</body>
</html>

View File

@ -47,6 +47,8 @@ const VM_DISK_DETACH = 'vm.detach'
const VM_DISK_RESIZE = 'vm.diskresize'
const VM_NIC_ATTACH = 'vm.attachnic'
const VM_NIC_DETACH = 'vm.detachnic'
const VM_SEC_GROUP_ATTACH = 'vm.attachsg'
const VM_SEC_GROUP_DETACH = 'vm.detachsg'
const VM_SCHED_ADD = 'vm.schedadd'
const VM_SCHED_UPDATE = 'vm.schedupdate'
const VM_SCHED_DELETE = 'vm.scheddelete'
@ -86,6 +88,8 @@ const Actions = {
VM_DISK_RESIZE,
VM_NIC_ATTACH,
VM_NIC_DETACH,
VM_SEC_GROUP_ATTACH,
VM_SEC_GROUP_DETACH,
VM_SCHED_ADD,
VM_SCHED_UPDATE,
VM_SCHED_DELETE,
@ -356,6 +360,42 @@ module.exports = {
},
},
},
[VM_SEC_GROUP_ATTACH]: {
// inspected
httpMethod: PUT,
params: {
id: {
from: resource,
default: 0,
},
nic: {
from: postBody,
default: 0,
},
secgroup: {
from: postBody,
default: 0,
},
},
},
[VM_SEC_GROUP_DETACH]: {
// inspected
httpMethod: PUT,
params: {
id: {
from: resource,
default: 0,
},
nic: {
from: postBody,
default: 0,
},
secgroup: {
from: postBody,
default: 0,
},
},
},
[VM_CHMOD]: {
// inspected
httpMethod: PUT,

View File

@ -163,48 +163,6 @@ const defaults = {
defaultHost: '0.0.0.0',
defaultPort: 2616,
defaultEvents: ['SIGINT', 'SIGTERM'],
defaultCurrency: 'EUR',
availableLanguages: {
bg_BG: 'Bulgarian (Bulgaria)',
bg: 'Bulgarian',
ca: 'Catalan',
cs_CZ: 'Czech',
da: 'Danish',
de_CH: 'German (Switzerland)',
de: 'German',
el_GR: 'Greek (Greece)',
en: 'English',
es_ES: 'Spanish',
et_EE: 'Estonian',
fa_IR: 'Persian (Iran)',
fa: 'Persian',
fr_CA: 'French (Canada)',
fr_FR: 'French',
hu_HU: 'Hungary',
it_IT: 'Italian',
ja: 'Japanese',
ka: 'Georgian',
lt_LT: 'Lithuanian',
nl_NL: 'Dutch',
pl: 'Polish',
pt_PT: 'Portuguese',
ro_RO: 'Romanian',
ru_RU: 'Russian',
ru: 'Russian',
si: 'Sinhala',
sk_SK: 'Slavak',
sr_RS: 'Serbian',
sv: 'Swedish',
th_TH: 'Thai (Thailand)',
th: 'Thai',
tr_TR: 'Turkish (Turkey)',
tr: 'Turkish',
uk_UA: 'Ukrainian (Ukraine)',
uk: 'Ukrainian',
vi: 'Vietnamese',
zh_CN: 'Chinese (China)',
zh_TW: 'Chinese (Taiwan)',
},
}
module.exports = defaults