mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-30 22:50:10 +03:00
parent
fc724f2121
commit
62396fe4dd
@ -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
|
||||
|
@ -141,6 +141,8 @@ info-tabs:
|
||||
actions:
|
||||
attach_nic: true
|
||||
detach_nic: true
|
||||
attach_secgroup: true
|
||||
detach_secgroup: true
|
||||
|
||||
snapshot:
|
||||
enabled: true
|
||||
|
@ -139,6 +139,8 @@ info-tabs:
|
||||
actions:
|
||||
attach_nic: true
|
||||
detach_nic: true
|
||||
attach_secgroup: true
|
||||
detach_secgroup: true
|
||||
|
||||
snapshot:
|
||||
enabled: true
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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 = {
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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 = {
|
||||
|
@ -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',
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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
|
@ -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))
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -102,7 +102,7 @@ const HeaderPopover = memo(
|
||||
placement="bottom-end"
|
||||
keepMounted={false}
|
||||
style={{
|
||||
zIndex: zIndex.appBar + 1,
|
||||
zIndex: zIndex.modal + 1,
|
||||
...mobileStyles,
|
||||
}}
|
||||
{...popperProps}
|
||||
|
@ -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),
|
||||
|
@ -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',
|
||||
},
|
||||
]
|
@ -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
|
@ -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
|
@ -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,
|
||||
{
|
||||
|
@ -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,
|
||||
|
@ -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: {
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
@ -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'
|
||||
|
@ -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/
|
||||
|
@ -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',
|
||||
|
@ -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]: [],
|
||||
|
@ -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))
|
||||
|
@ -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 &&
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
})
|
||||
|
||||
|
43
src/fireedge/src/client/providers/preloadConfigProvider.js
Normal file
43
src/fireedge/src/client/providers/preloadConfigProvider.js
Normal 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
|
@ -24,6 +24,8 @@ const { store } = createStore({
|
||||
extraMiddleware: [onlyForOneadminMiddleware],
|
||||
})
|
||||
|
||||
delete window.__PRELOADED_STATE__
|
||||
|
||||
const rootHTML = document.getElementById('root')?.innerHTML
|
||||
const renderMethod = rootHTML !== '' ? hydrate : render
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -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]])
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user