1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-01-10 01:17:40 +03:00

Merge branch 'f-3951'

This commit is contained in:
Tino Vazquez 2021-01-12 12:23:39 +01:00
commit b4f0324000
No known key found for this signature in database
GPG Key ID: 14201E424D02047E
164 changed files with 1833 additions and 1058 deletions

View File

@ -8,6 +8,7 @@
"server": "./src/server"
}
}],
"react-hot-loader/babel",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-proposal-nullish-coalescing-operator",

View File

@ -8219,6 +8219,30 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.9.2.tgz",
"integrity": "sha512-vCPEbHVCRvsoqrQARgQ7a3VrXzqbFOO53gHFRdQzLzHMT9kxum3wfcSi8A1b49KPRsomvsqexH4tBUJMneEu+Q=="
},
"react-hot-loader": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.13.0.tgz",
"integrity": "sha512-JrLlvUPqh6wIkrK2hZDfOyq/Uh/WeVEr8nc7hkn2/3Ul0sx1Kr5y4kOGNacNRoj7RhwLNcQ3Udf1KJXrqc0ZtA==",
"dev": true,
"requires": {
"fast-levenshtein": "^2.0.6",
"global": "^4.3.0",
"hoist-non-react-statics": "^3.3.0",
"loader-utils": "^1.1.0",
"prop-types": "^15.6.1",
"react-lifecycles-compat": "^3.0.4",
"shallowequal": "^1.1.0",
"source-map": "^0.7.3"
},
"dependencies": {
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true
}
}
},
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -8232,6 +8256,17 @@
"prop-types": "^15.6.2"
}
},
"react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
"dev": true
},
"react-minimal-pie-chart": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/react-minimal-pie-chart/-/react-minimal-pie-chart-8.1.0.tgz",
"integrity": "sha512-eoorRrGySbkkzpG1vX0/p18xxhSA/6OqIlMnlAlMez+cjfLnsX/u8+PFaOsWltQwhiwtA4DaO8VriWKryIluyg=="
},
"react-proxy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/react-proxy/-/react-proxy-1.1.8.tgz",
@ -8840,6 +8875,12 @@
"safe-buffer": "^5.0.1"
}
},
"shallowequal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
"dev": true
},
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@ -41,7 +41,8 @@
"eslint-plugin-react": "^7.21.4",
"eslint-plugin-standard": "^4.0.1",
"fireedge-genpotfile": "^1.0.0",
"fireedge-pojson": "^1.0.2"
"fireedge-pojson": "^1.0.2",
"react-hot-loader": "^4.13.0"
},
"dependencies": {
"@babel/cli": "^7.10.4",
@ -101,6 +102,7 @@
"react-flow-renderer": "^5.11.1",
"react-hook-form": "^6.8.6",
"react-json-pretty": "^2.2.0",
"react-minimal-pie-chart": "^8.1.0",
"react-redux": "^7.2.1",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */

View File

@ -10,6 +10,11 @@ const Actions = {
module.exports = {
Actions,
// --------------------------------------------
// ONE
// --------------------------------------------
setVms: vms => ({
type: SUCCESS_ONE_REQUEST,
payload: { vms }
@ -18,14 +23,6 @@ module.exports = {
type: SUCCESS_ONE_REQUEST,
payload: { templates }
}),
setApplications: applications => ({
type: SUCCESS_ONE_REQUEST,
payload: { applications }
}),
setApplicationsTemplates: applicationsTemplates => ({
type: SUCCESS_ONE_REQUEST,
payload: { applicationsTemplates }
}),
setDatastores: datastores => ({
type: SUCCESS_ONE_REQUEST,
payload: { datastores }
@ -94,22 +91,41 @@ module.exports = {
type: SUCCESS_ONE_REQUEST,
payload: { acl }
}),
setProvidersTemplates: providersTemplates => ({
// --------------------------------------------
// ONE FLOW
// --------------------------------------------
setApplications: applications => ({
type: SUCCESS_ONE_REQUEST,
payload: { providersTemplates }
payload: { applications }
}),
setApplicationsTemplates: applicationsTemplates => ({
type: SUCCESS_ONE_REQUEST,
payload: { applicationsTemplates }
}),
// --------------------------------------------
// ONE PROVISION
// --------------------------------------------
setProvisionsTemplates: provisionsTemplates => ({
type: SUCCESS_ONE_REQUEST,
payload: { provisionsTemplates }
}),
setProviders: providers => ({
type: SUCCESS_ONE_REQUEST,
payload: { providers }
}),
setProvisionsTemplates: provisionsTemplates => ({
type: SUCCESS_ONE_REQUEST,
payload: { provisionsTemplates }
}),
setProvisions: provisions => ({
type: SUCCESS_ONE_REQUEST,
payload: { provisions }
}),
// --------------------------------------------
// ONE REQUEST
// --------------------------------------------
startOneRequest: () => ({
type: START_ONE_REQUEST
}),

View File

@ -1,97 +0,0 @@
/* Copyright 2002-2020, 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 * as React from 'react'
import PropTypes from 'prop-types'
import { StaticRouter, BrowserRouter } from 'react-router-dom'
import { Provider as ReduxProvider } from 'react-redux'
import root from 'window-or-global'
import SocketProvider from 'client/providers/socketProvider'
import MuiProvider from 'client/providers/muiProvider'
import NotistackProvider from 'client/providers/notistackProvider'
import { TranslateProvider } from 'client/components/HOC'
import { APPS, APP_URL } from 'client/constants'
import Router from 'client/router'
if (process?.env?.NODE_ENV === 'development') {
const webpackHotMiddlewareClient = require('webpack-hot-middleware/client')
webpackHotMiddlewareClient.subscribeAll(function (message) {
if (message?.action === 'built' && root?.location?.reload) {
root.location.reload()
}
})
}
const App = ({ location, context, store, app }) => {
let appName = app
if (
process?.env?.NODE_ENV === 'development' &&
typeof window !== 'undefined'
) {
const parseUrl = window.location.pathname
.split(/\//gi)
.filter(sub => sub?.length > 0)
parseUrl.forEach(resource => {
if (resource && APPS.includes(resource)) {
appName = resource
}
})
}
return (
<MuiProvider app={appName} location={location}>
<ReduxProvider store={store}>
<NotistackProvider>
<SocketProvider>
<TranslateProvider>
{location && context ? (
// server build
<StaticRouter location={location} context={context}>
<Router app={appName} />
</StaticRouter>
) : (
// browser build
<BrowserRouter basename={`${APP_URL}/${appName}`}>
<Router app={appName} />
</BrowserRouter>
)}
</TranslateProvider>
</SocketProvider>
</NotistackProvider>
</ReduxProvider>
</MuiProvider>
)
}
App.propTypes = {
location: PropTypes.string,
context: PropTypes.shape({}),
store: PropTypes.shape({}),
app: PropTypes.string
}
App.defaultProps = {
location: '',
context: {},
store: {},
app: ''
}
export default App

View File

@ -0,0 +1,29 @@
/* Copyright 2002-2021, 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 * as React from 'react'
import Router from 'client/router'
import routes from 'client/router/flow'
import { _APPS } from 'client/constants'
const APP_NAME = _APPS.flow.name
const FlowApp = () => <Router title={APP_NAME} routes={routes} />
FlowApp.displayName = '_FlowApp'
export default FlowApp

View File

@ -0,0 +1,71 @@
/* Copyright 2002-2021, 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 * as React from 'react'
import PropTypes from 'prop-types'
import { StaticRouter, BrowserRouter } from 'react-router-dom'
import { Provider as ReduxProvider } from 'react-redux'
import SocketProvider from 'client/providers/socketProvider'
import MuiProvider from 'client/providers/muiProvider'
import NotistackProvider from 'client/providers/notistackProvider'
import { TranslateProvider } from 'client/components/HOC'
import App from 'client/apps/flow/_app'
import theme from 'client/apps/flow/theme'
import { _APPS, APP_URL } from 'client/constants'
const APP_NAME = _APPS.flow.name
const Flow = ({ store, location, context }) => (
<ReduxProvider store={store}>
<SocketProvider>
<TranslateProvider>
<MuiProvider theme={theme}>
<NotistackProvider>
{location && context ? (
// server build
<StaticRouter location={location} context={context}>
<App />
</StaticRouter>
) : (
// browser build
<BrowserRouter basename={`${APP_URL}/${APP_NAME}`}>
<App />
</BrowserRouter>
)}
</NotistackProvider>
</MuiProvider>
</TranslateProvider>
</SocketProvider>
</ReduxProvider>
)
Flow.propTypes = {
location: PropTypes.string,
context: PropTypes.shape({}),
store: PropTypes.shape({})
}
Flow.defaultProps = {
location: '',
context: {},
store: {}
}
Flow.displayName = 'FlowApp'
export default Flow

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */
@ -14,31 +14,28 @@
/* -------------------------------------------------------------------------- */
import * as React from 'react'
import { hydrate, render } from 'react-dom'
import { createStore } from 'redux'
import root from 'window-or-global'
import rootReducer from 'client/reducers'
import App from 'client/app'
import { useAuth, useProvision } from 'client/hooks'
import Router from 'client/router'
import routes from 'client/router/provision'
// eslint-disable-next-line no-underscore-dangle
const preloadedState = root.__PRELOADED_STATE__
import { _APPS } from 'client/constants'
// eslint-disable-next-line no-underscore-dangle
delete root.__PRELOADED_STATE__
const APP_NAME = _APPS.provision.name
const store = createStore(
rootReducer(),
preloadedState,
// eslint-disable-next-line no-underscore-dangle
root.__REDUX_DEVTOOLS_EXTENSION__ && root.__REDUX_DEVTOOLS_EXTENSION__()
)
const ProvisionApp = () => {
const { isLogged, isLoginInProcess } = useAuth()
const { getProvisionsTemplates } = useProvision()
const element = document.getElementById('preloadState')
if (element) {
element.remove()
React.useEffect(() => {
if (isLogged && !isLoginInProcess) {
getProvisionsTemplates()
}
}, [isLogged])
return <Router title={APP_NAME} routes={routes} />
}
const mainDiv = document.getElementById('root')
const renderMethod = mainDiv && mainDiv.innerHTML !== '' ? hydrate : render
renderMethod(<App store={store} />, document.getElementById('root'))
ProvisionApp.displayName = '_ProvisionApp'
export default ProvisionApp

View File

@ -0,0 +1,71 @@
/* Copyright 2002-2021, 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 * as React from 'react'
import PropTypes from 'prop-types'
import { StaticRouter, BrowserRouter } from 'react-router-dom'
import { Provider as ReduxProvider } from 'react-redux'
import SocketProvider from 'client/providers/socketProvider'
import MuiProvider from 'client/providers/muiProvider'
import NotistackProvider from 'client/providers/notistackProvider'
import { TranslateProvider } from 'client/components/HOC'
import App from 'client/apps/provision/_app'
import theme from 'client/apps/provision/theme'
import { _APPS, APP_URL } from 'client/constants'
const APP_NAME = _APPS.provision.name
const Provision = ({ store, location, context }) => (
<ReduxProvider store={store}>
<SocketProvider>
<TranslateProvider>
<MuiProvider theme={theme}>
<NotistackProvider>
{location && context ? (
// server build
<StaticRouter location={location} context={context}>
<App />
</StaticRouter>
) : (
// browser build
<BrowserRouter basename={`${APP_URL}/${APP_NAME}`}>
<App />
</BrowserRouter>
)}
</NotistackProvider>
</MuiProvider>
</TranslateProvider>
</SocketProvider>
</ReduxProvider>
)
Provision.propTypes = {
location: PropTypes.string,
context: PropTypes.shape({}),
store: PropTypes.shape({})
}
Provision.defaultProps = {
location: '',
context: {},
store: {}
}
Provision.displayName = 'ProvisionApp'
export default Provision

View File

@ -1,7 +1,7 @@
export default {
palette: {
type: 'dark',
common: { black: '#000', white: '#fff' },
common: { black: '#000000', white: '#ffffff' },
background: {
paper: '#2a2d3d',
default: '#222431'
@ -10,19 +10,19 @@ export default {
light: '#2a2d3d',
main: '#222431',
dark: '#191924',
contrastText: '#fff'
contrastText: '#ffffff'
},
secondary: {
light: '#fb8554',
main: '#fa6c43',
dark: '#fe5a23',
contrastText: '#fff'
contrastText: '#ffffff'
},
error: {
light: '#e57373',
main: '#f44336',
dark: '#d32f2f',
contrastText: '#fff'
contrastText: '#ffffff'
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,51 +0,0 @@
import React, { memo } from 'react'
import PropTypes from 'prop-types'
import { CardContent, Typography } from '@material-ui/core'
import ProvidersIcon from '@material-ui/icons/Public'
import SelectCard from 'client/components/Cards/SelectCard'
const LocationCard = memo(
({ value, isSelected, handleClick }) => {
const { key, properties } = value
return (
<SelectCard
title={key}
icon={<ProvidersIcon />}
isSelected={isSelected}
handleClick={handleClick}
>
<CardContent>
{properties && Object.entries(properties)
?.map(([pKey, pVal]) => (
<Typography key={pKey} variant="body2">
<b>{pKey}</b>{` - ${pVal}`}
</Typography>
))}
</CardContent>
</SelectCard>
)
},
(prev, next) => prev.isSelected === next.isSelected
)
LocationCard.propTypes = {
value: PropTypes.shape({
key: PropTypes.string.isRequired,
properties: PropTypes.object
}),
isSelected: PropTypes.bool,
handleClick: PropTypes.func
}
LocationCard.defaultProps = {
value: {},
isSelected: false,
handleClick: undefined
}
LocationCard.displayName = 'LocationCard'
export default LocationCard

View File

@ -0,0 +1,56 @@
import React, { memo, useMemo } from 'react'
import PropTypes from 'prop-types'
import SelectCard from 'client/components/Cards/SelectCard'
import { isExternalURL } from 'client/utils'
import { PROVIDER_IMAGES_URL, PROVISION_IMAGES_URL } from 'client/constants'
const ProvisionTemplateCard = memo(
({ value, title, isSelected, isProvider, handleClick }) => {
const IMAGES_URL = isProvider ? PROVIDER_IMAGES_URL : PROVISION_IMAGES_URL
const { image } = (isProvider ? value?.plain : value) ?? {}
const imgSource = useMemo(() =>
isExternalURL(image) ? image : `${IMAGES_URL}/${image}`
, [image])
return (
<SelectCard
stylesProps={{ minHeight: 80 }}
isSelected={isSelected}
handleClick={handleClick}
title={title}
mediaProps={image && {
component: 'img',
image: imgSource,
draggable: false
}}
/>
)
}, (prev, next) => prev.isSelected === next.isSelected
)
ProvisionTemplateCard.propTypes = {
value: PropTypes.shape({
name: PropTypes.string.isRequired,
plain: PropTypes.shape({
image: PropTypes.string
})
}),
title: PropTypes.string,
isProvider: PropTypes.bool,
isSelected: PropTypes.bool,
handleClick: PropTypes.func
}
ProvisionTemplateCard.defaultProps = {
value: {},
title: undefined,
isProvider: undefined,
isSelected: undefined,
handleClick: undefined
}
ProvisionTemplateCard.displayName = 'ProvisionTemplateCard'
export default ProvisionTemplateCard

View File

@ -1,25 +1,28 @@
import React, { memo, useMemo } from 'react'
import * as React from 'react'
import PropTypes from 'prop-types'
import ProvidersIcon from '@material-ui/icons/Public'
import SelectCard from 'client/components/Cards/SelectCard'
import { isExternalURL } from 'client/utils'
import { PROVIDER_IMAGES_URL, PROVISION_IMAGES_URL } from 'client/constants'
const ProvisionTemplateCard = memo(
({ value, title, isSelected, isProvider, handleClick }) => {
const ProvisionTemplateCard = React.memo(
({ value, isProvider, isSelected, handleClick }) => {
const { description, name, plain: { image } = {} } = value
const IMAGES_URL = isProvider ? PROVIDER_IMAGES_URL : PROVISION_IMAGES_URL
const { image } = (isProvider ? value?.plain : value) ?? {}
const imgSource = useMemo(() =>
const imgSource = React.useMemo(() =>
isExternalURL(image) ? image : `${IMAGES_URL}/${image}`
, [image])
return (
<SelectCard
stylesProps={{ minHeight: 80 }}
title={name}
subheader={description}
icon={<ProvidersIcon />}
isSelected={isSelected}
handleClick={handleClick}
title={title}
mediaProps={image && {
component: 'img',
image: imgSource,
@ -27,27 +30,27 @@ const ProvisionTemplateCard = memo(
}}
/>
)
}, (prev, next) => prev.isSelected === next.isSelected
},
(prev, next) => prev.isSelected === next.isSelected
)
ProvisionTemplateCard.propTypes = {
value: PropTypes.shape({
name: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
plain: PropTypes.shape({
image: PropTypes.string
})
}),
title: PropTypes.string,
isProvider: PropTypes.bool,
isSelected: PropTypes.bool,
handleClick: PropTypes.func
}
ProvisionTemplateCard.defaultProps = {
value: {},
title: undefined,
value: { name: '', description: '' },
isProvider: undefined,
isSelected: undefined,
isSelected: false,
handleClick: undefined
}

View File

@ -0,0 +1,79 @@
import * as React from 'react'
import PropTypes from 'prop-types'
import clsx from 'clsx'
import { Paper, Typography, makeStyles, lighten } from '@material-ui/core'
import { addOpacityToColor } from 'client/utils'
const useStyles = makeStyles(theme => ({
root: {
padding: '2em',
position: 'relative',
overflow: 'hidden',
backgroundColor: ({ bgColor }) => bgColor
},
icon: {
position: 'absolute',
top: 0,
right: 0,
fontSize: '10em',
fill: addOpacityToColor(theme.palette.common.white, 0.2)
},
wave: {
display: 'block',
position: 'absolute',
opacity: 0.4,
top: '-5%',
left: '50%',
width: 220,
height: 220,
borderRadius: '43%'
},
wave1: {
backgroundColor: ({ bgColor }) => lighten(bgColor, 0.4),
animation: '$drift 7s infinite linear'
},
wave2: {
backgroundColor: ({ bgColor }) => lighten(bgColor, 0.6),
animation: '$drift 5s infinite linear'
},
'@keyframes drift': {
from: { transform: 'rotate(0deg)' },
to: { transform: 'rotate(360deg)' }
}
}))
const WavesCard = React.memo(({ text, value, bgColor, icon: Icon }) => {
const classes = useStyles({ bgColor })
return (
<Paper className={classes.root}>
<Typography variant='h6'>{text}</Typography>
<Typography variant='h4'>{value}</Typography>
<span className={clsx(classes.wave, classes.wave1)}></span>
<span className={clsx(classes.wave, classes.wave2)}></span>
{Icon && <Icon className={classes.icon} />}
</Paper>
)
}, (prev, next) => prev.value === next.value)
WavesCard.propTypes = {
text: PropTypes.string,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]),
bgColor: PropTypes.string,
icon: PropTypes.any
}
WavesCard.defaultProps = {
text: undefined,
value: undefined,
bgColor: '#ffffff00',
icon: undefined
}
WavesCard.displayName = 'WavesCard'
export default WavesCard

View File

@ -1,66 +0,0 @@
import * as React from 'react'
import PropTypes from 'prop-types'
import { makeStyles, CardContent } from '@material-ui/core'
import SelectCard from 'client/components/Cards/SelectCard'
const useStyles = makeStyles(theme => ({
card: {
backgroundColor: theme.palette.primary.light
}
}))
const WidgetCard = React.memo(({ value }) => {
const { title, widget, actions } = value
const classes = useStyles()
return (
<SelectCard
cardProps={{
variant: 'outlined',
className: classes.card
}}
actions={actions}
cardActionsProps={{ style: { justifyContent: 'center' } }}
title={title}
cardHeaderProps={{
titleTypographyProps: {
variant: 'h4',
style: { textAlign: 'center' }
}
}}
>
<CardContent>{widget}</CardContent>
</SelectCard>
)
})
WidgetCard.propTypes = {
value: PropTypes.shape({
icon: PropTypes.node,
title: PropTypes.string,
subtitle: PropTypes.string,
widget: PropTypes.node,
actions: PropTypes.arrayOf(
PropTypes.shape({
handleClick: PropTypes.func.isRequired,
icon: PropTypes.object.isRequired,
cy: PropTypes.string
})
)
})
}
WidgetCard.defaultProps = {
value: {
icon: undefined,
title: undefined,
subtitle: undefined,
widget: undefined,
actions: []
}
}
WidgetCard.displayName = 'WidgetCard'
export default WidgetCard

View File

@ -1,4 +1,3 @@
import WidgetCard from 'client/components/Cards/WidgetCard'
import ClusterCard from 'client/components/Cards/ClusterCard'
import DatastoreCard from 'client/components/Cards/DatastoreCard'
import HostCard from 'client/components/Cards/HostCard'
@ -10,12 +9,11 @@ import ApplicationTemplateCard from 'client/components/Cards/ApplicationTemplate
import ApplicationCard from 'client/components/Cards/ApplicationCard'
import ApplicationNetworkCard from 'client/components/Cards/ApplicationNetworkCard'
import PolicyCard from 'client/components/Cards/PolicyCard'
import ProvisionTemplateCard from 'client/components/Cards/ProvisionTemplateCard'
import ProvisionCard from 'client/components/Cards/ProvisionCard'
import LocationCard from 'client/components/Cards/LocationCard'
import ProvisionTemplateCard from 'client/components/Cards/ProvisionTemplateCard'
import WavesCard from 'client/components/Cards/WavesCard'
export {
WidgetCard,
ClusterCard,
DatastoreCard,
HostCard,
@ -29,5 +27,5 @@ export {
PolicyCard,
ProvisionTemplateCard,
ProvisionCard,
LocationCard
WavesCard
}

View File

@ -0,0 +1,85 @@
import * as React from 'react'
import PropTypes from 'prop-types'
import { makeStyles, Tooltip } from '@material-ui/core'
import { TypographyWithPoint } from 'client/components/Typography'
import { addOpacityToColor } from 'client/utils'
const useStyles = makeStyles(() => ({
legend: {
display: 'grid',
gridGap: '1rem',
gridTemplateColumns: 'repeat(auto-fit, minmax(125px, 1fr))'
},
bar: {
marginTop: '1rem',
display: 'grid',
height: '1rem',
width: '100%',
backgroundColor: '#616161e0',
transition: '1s',
gridTemplateColumns: ({ fragments }) =>
fragments?.map(fragment => `${fragment}fr`)?.join(' ')
}
}))
const SingleBar = ({ legend, data, total }) => {
const fragments = data.map(data => Math.floor(data * 10 / (total || 1)))
const classes = useStyles({ fragments })
return (
<>
{/* LEGEND */}
<div className={classes.legend}>
{legend?.map(({ name, color }) => (
<TypographyWithPoint key={name} pointColor={color}>
{name}
</TypographyWithPoint>
))}
</div>
{/* BAR FRAGMENTS */}
<div className={classes.bar}>
{data?.map((value, idx) => {
const label = legend[idx]?.name
const color = legend[idx]?.color
const style = {
backgroundColor: color,
'&:hover': { backgroundColor: addOpacityToColor(color, 0.6) }
}
return (
<Tooltip arrow key={label} placement="top" title={`${label}: ${value}`}>
<div style={style}></div>
</Tooltip>
)
})}
</div>
</>
)
}
SingleBar.propTypes = {
legend: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
color: PropTypes.string
})),
data: PropTypes.arrayOf(
PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
])
),
total: PropTypes.number
}
SingleBar.defaultProps = {
legend: undefined,
data: undefined,
total: 0
}
SingleBar.displayName = 'SingleBar'
export default SingleBar

View File

@ -0,0 +1,7 @@
import SimpleCircle from 'client/components/Charts/SimpleCircle'
import SingleBar from 'client/components/Charts/SingleBar'
export {
SimpleCircle,
SingleBar
}

View File

@ -7,7 +7,8 @@ import {
Step,
StepLabel,
Box,
Typography
Typography,
StepButton
} from '@material-ui/core'
import { makeStyles, fade } from '@material-ui/core/styles'
@ -46,6 +47,7 @@ const CustomStepper = ({
activeStep,
lastStep,
disabledBack,
handleStep,
handleNext,
handleBack,
errors,
@ -55,25 +57,31 @@ const CustomStepper = ({
return (
<>
<Stepper activeStep={activeStep} className={classes.root}>
{steps?.map(({ id, label }) => (
<Stepper nonLinear activeStep={activeStep} className={classes.root}>
{steps?.map(({ id, label }, stepIdx) => (
<Step key={id}>
<StepLabel
StepIconProps={{
classes: {
root: classes.icon,
completed: classes.completed,
active: classes.active,
error: classes.error
}
}}
{...(Boolean(errors[id]) && { error: true })}
<StepButton
onClick={() => handleStep(stepIdx)}
completed={activeStep > stepIdx}
disabled={activeStep + 1 < stepIdx}
optional={
<Typography variant="caption" color="error">
<Typography variant='caption' color='error'>
{errors[id]?.message}
</Typography>
}
>{Tr(label)}</StepLabel>
>
<StepLabel
StepIconProps={{
classes: {
root: classes.icon,
completed: classes.completed,
active: classes.active,
error: classes.error
}
}}
{...(Boolean(errors[id]) && { error: true })}
>{Tr(label)}</StepLabel>
</StepButton>
</Step>
))}
</Stepper>
@ -83,7 +91,7 @@ const CustomStepper = ({
</Button>
<SubmitButton
color='secondary'
data-cy="stepper-next-button"
data-cy='stepper-next-button'
onClick={handleNext}
isSubmitting={isSubmitting}
label={activeStep === lastStep ? Tr(T.Finish) : Tr(T.Next)}
@ -107,6 +115,7 @@ CustomStepper.propTypes = {
lastStep: PropTypes.number.isRequired,
disabledBack: PropTypes.bool.isRequired,
isSubmitting: PropTypes.bool,
handleStep: PropTypes.func,
handleNext: PropTypes.func,
handleBack: PropTypes.func,
errors: PropTypes.shape({
@ -119,6 +128,7 @@ CustomStepper.defaultProps = {
activeStep: 0,
lastStep: 0,
disabledBack: false,
handleStep: () => undefined,
handleNext: () => undefined,
handleBack: () => undefined,
errors: undefined,

View File

@ -25,46 +25,76 @@ const FormStepper = ({ steps, schema, onSubmit }) => {
reset({ ...formData }, { errors: false })
}, [formData])
const handleNext = () => {
const { id, resolver, optionsValidate } = steps[activeStep]
const currentData = watch(id)
const validateSchema = step => {
const { id, resolver, optionsValidate } = steps[step]
const stepData = watch(id)
const stepSchema = typeof resolver === 'function' ? resolver() : resolver
stepSchema
.validate(currentData, optionsValidate)
.then(() => {
if (activeStep === lastStep) {
onSubmit(schema().cast({ ...formData, [id]: currentData }))
} else {
setFormData(prev => ({ ...prev, [id]: currentData }))
setActiveStep(prevActiveStep => prevActiveStep + 1)
}
return stepSchema
.validate(stepData, optionsValidate)
.then(() => ({ id, data: stepData }))
}
const setErrors = ({ inner: errors, ...rest }) => {
const errorsByPath = groupBy(errors, 'path') ?? {}
const totalErrors = Object.keys(errorsByPath).length
totalErrors > 0
? setError(id, {
type: 'manual',
message: `${totalErrors} error(s) occurred`
})
.catch(({ inner, ...err }) => {
const errorsByPath = groupBy(inner, 'path') ?? {}
const totalErrors = Object.keys(errorsByPath).length
: setError(id, rest)
totalErrors > 0
? setError(id, {
type: 'manual',
message: `${totalErrors} error(s) occurred`
errors?.forEach(({ path, type, message }) =>
setError(`${id}.${path}`, { type, message })
)
}
const handleStep = stepToAdvance => {
const isBackAction = activeStep > stepToAdvance
isBackAction && handleBack(isBackAction)
steps
.slice(FIRST_STEP, stepToAdvance)
.forEach((_, step, stepsToValidate) => {
validateSchema(step)
.then(({ id, data }) => {
activeStep === step &&
setFormData(prev => ({ ...prev, [id]: data }))
step === stepsToValidate.length - 1 &&
Number.isInteger(stepToAdvance) &&
setActiveStep(stepToAdvance)
})
: setError(id, err)
inner?.forEach(({ path, type, message }) =>
setError(`${id}.${path}`, { type, message })
)
.catch(setErrors)
})
}
const handleBack = useCallback(() => {
if (activeStep <= FIRST_STEP) return
const handleNext = () => {
validateSchema(activeStep)
.then(({ id, data }) => {
if (activeStep === lastStep) {
onSubmit(schema().cast({ ...formData, [id]: data }))
} else {
setFormData(prev => ({ ...prev, [id]: data }))
setActiveStep(prevActiveStep => prevActiveStep + 1)
}
})
.catch(setErrors)
}
const handleBack = useCallback(stepToBack => {
if (activeStep < FIRST_STEP) return
const { id } = steps[activeStep]
const currentData = watch(id)
const stepData = watch(id)
setFormData(prev => ({ ...prev, [id]: currentData }))
setActiveStep(prevActiveStep => prevActiveStep - 1)
setFormData(prev => ({ ...prev, [id]: stepData }))
setActiveStep(prevActiveStep =>
Number.isInteger(stepToBack) ? stepToBack : (prevActiveStep - 1)
)
}, [activeStep])
const { id, content: Content } = useMemo(() => steps[activeStep], [
@ -92,6 +122,7 @@ const FormStepper = ({ steps, schema, onSubmit }) => {
activeStep={activeStep}
lastStep={lastStep}
disabledBack={disabledBack}
handleStep={handleStep}
handleNext={handleNext}
handleBack={handleBack}
errors={errors}

View File

@ -25,12 +25,12 @@ import Header from 'client/components/Header'
import Footer from 'client/components/Footer'
import internalStyles from 'client/components/HOC/InternalLayout/styles'
const InternalLayout = ({ authRoute, label, children }) => {
const InternalLayout = ({ label, children }) => {
const classes = internalStyles()
const scroll = React.useRef()
const { isFixMenu } = useGeneral()
return authRoute ? (
return (
<Box className={clsx(classes.root, { [classes.isDrawerFixed]: isFixMenu })}>
<Header title={label} scrollableContainer={scroll?.current} />
<Box component="main" className={classes.main}>
@ -56,8 +56,6 @@ const InternalLayout = ({ authRoute, label, children }) => {
</Box>
<Footer />
</Box>
) : (
children
)
}
@ -68,13 +66,11 @@ InternalLayout.propTypes = {
PropTypes.node,
PropTypes.string
]),
authRoute: PropTypes.bool.isRequired,
label: PropTypes.string
}
InternalLayout.defaultProps = {
children: [],
authRoute: false,
label: null
}

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */
@ -78,7 +78,7 @@ const Header = ({ title, scrollableContainer }) => {
</Toolbar>
</AppBar>
),
[isFixMenu, fixMenu, isUpLg, isMobile, isOneAdmin]
[isFixMenu, fixMenu, isUpLg, isMobile, isOneAdmin, classes]
)
}

View File

@ -57,12 +57,12 @@ const ListCards = memo(({
return (
<CSSTransition
// use key to render transition (default: id or ID)
// use key to render transition (default: id or ID)
key={`card-${key.replace(/\s/g, '')}`}
classNames={classes.item}
timeout={400}
>
<Grid item {...breakpoints}>
<Grid item {...breakpoints} {...value?.breakpoints}>
<CardComponent value={value} {...cardsProps({ index, value })} />
</Grid>
</CSSTransition>

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */

View File

@ -21,11 +21,11 @@ const useStyles = makeStyles(theme => ({
}
},
'@keyframes ripple': {
'0%': {
from: {
transform: 'scale(.8)',
opacity: 1
},
'100%': {
to: {
transform: 'scale(2.4)',
opacity: 0
}

View File

@ -0,0 +1,43 @@
import * as React from 'react'
import PropTypes from 'prop-types'
import { makeStyles, Typography } from '@material-ui/core'
const useStateStyles = makeStyles(theme => ({
root: {
color: theme.palette.text.secondary,
width: 'max-content',
'&::before': {
content: "''",
display: 'inline-flex',
marginRight: '0.5rem',
backgroundColor: ({ color }) => color,
height: '0.7rem',
width: '0.7rem',
borderRadius: '50%'
}
}
}))
const TypographyWithPoint = ({ pointColor, children }) => {
const classes = useStateStyles({ color: pointColor })
return (
<Typography className={classes.root}>
{children}
</Typography>
)
}
TypographyWithPoint.propTypes = {
pointColor: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.string, PropTypes.element])
}
TypographyWithPoint.defaultProps = {
pointColor: undefined,
children: undefined
}
TypographyWithPoint.displayName = 'TypographyWithPoint'
export default TypographyWithPoint

View File

@ -1,5 +1,7 @@
import DevTypography from 'client/components/Typography/DevTypography'
import TypographyWithPoint from 'client/components/Typography/TypographyWithPoint'
export {
DevTypography
DevTypography,
TypographyWithPoint
}

View File

@ -1,40 +0,0 @@
import * as React from 'react'
import { useHistory } from 'react-router'
import { AddCircle } from '@material-ui/icons'
import { useFetch, useProvision } from 'client/hooks'
import Circle from 'client/components/Widgets/SimpleCircle'
import { PATH } from 'client/router/provision'
import { T } from 'client/constants'
const cy = 'dashboard-widget-total-providers'
const title = T.Providers
const TotalProviders = () => {
const history = useHistory()
const { providers, getProviders } = useProvision()
const { fetchRequest } = useFetch(getProviders)
React.useEffect(() => { fetchRequest() }, [])
const actions = React.useMemo(() => [{
handleClick: () => history.push(PATH.PROVIDERS.CREATE),
icon: <AddCircle />,
cy: `${cy}-create`
}], [history])
const widget = React.useMemo(() => (
<Circle
label={`${providers?.length}`}
onClick={() => history.push(PATH.PROVIDERS.LIST)}
/>
), [providers?.length])
return { cy, title, actions, widget }
}
TotalProviders.displayName = 'TotalProviders'
export default TotalProviders

View File

@ -0,0 +1,94 @@
import * as React from 'react'
import { PieChart } from 'react-minimal-pie-chart'
import { Typography, useTheme, lighten, Paper } from '@material-ui/core'
import { Public as ProvidersIcon } from '@material-ui/icons'
import { useProvision } from 'client/hooks'
import { TypographyWithPoint } from 'client/components/Typography'
import { get } from 'client/utils'
import { T } from 'client/constants'
import useStyles from 'client/components/Widgets/TotalProviders/styles'
const TotalProviders = () => {
const { providers, provisions } = useProvision()
const classes = useStyles()
const theme = useTheme()
const usedColor = theme.palette.secondary.main
const bgColor = lighten(theme.palette.background.paper, 0.8)
const totalProviders = React.useMemo(() => providers.length, [providers])
const usedProviders = React.useMemo(() =>
provisions
?.map(provision => get(provision, 'TEMPLATE.BODY.provider'))
?.filter((provision, idx, self) => self.indexOf(provision) === idx)
?.length ?? 0
, [provisions])
const usedPercent = React.useMemo(() =>
totalProviders !== 0 ? (usedProviders * 100) / totalProviders : 0
, [totalProviders, usedProviders])
const title = React.useMemo(() => (
<div className={classes.title}>
<Typography className={classes.titlePrimary}>
{`${totalProviders} ${T.Providers}`}
</Typography>
<Typography className={classes.titleSecondary}>
{T.InTotal}
</Typography>
</div>
), [classes, totalProviders])
const legend = React.useMemo(() => (
<div>
<TypographyWithPoint key={usedProviders} pointColor={usedColor}>
{`${usedProviders}`}
</TypographyWithPoint>
<Typography className={classes.legendSecondary}>
{T.Used}
</Typography>
</div>
), [classes, usedProviders])
const chart = React.useMemo(() => (
<PieChart
className={classes.chart}
background={bgColor}
data={[{ value: 1, key: 1, color: usedColor }]}
reveal={usedPercent}
lineWidth={20}
lengthAngle={270}
rounded
animate
label={({ dataIndex }) => (
<ProvidersIcon
key={dataIndex}
x={25} y={25} width='50'height='50'
style={{ fill: bgColor }}
/>
)}
labelPosition={0}
/>
), [classes, usedPercent])
return (
<Paper
data-cy='dashboard-widget-total-providers-by-state'
className={classes.root}
>
{title}
<div className={classes.content}>
{chart}
{legend}
</div>
</Paper>
)
}
TotalProviders.displayName = 'TotalProviders'
export default TotalProviders

View File

@ -0,0 +1,32 @@
import { makeStyles } from '@material-ui/core'
export default makeStyles(theme => ({
root: {
padding: '2em'
},
title: {
padding: '0 2em 2em',
textAlign: 'left'
},
titlePrimary: {
fontSize: '2rem',
color: theme.palette.text.primary
},
titleSecondary: {
fontSize: '1.4rem',
color: theme.palette.text.secondary
},
content: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(125px, 1fr))',
gridGap: '2em'
},
legendSecondary: {
fontSize: '0.9rem',
marginLeft: '1.2rem',
color: theme.palette.text.secondary
},
chart: {
height: 200
}
}))

View File

@ -0,0 +1,77 @@
import * as React from 'react'
import {
Storage as ClusterIcon,
VideogameAsset as HostIcon,
FolderOpen as DatastoreIcon,
AccountTree as NetworkIcon
} from '@material-ui/icons'
import { useProvision } from 'client/hooks'
import { WavesCard } from 'client/components/Cards'
import { get } from 'client/utils'
import { T } from 'client/constants'
const TotalProvisionInfrastructures = () => {
const { provisions } = useProvision()
const provisionsByProvider = React.useMemo(() =>
provisions
?.map(provision => ({
provider: get(provision, 'TEMPLATE.BODY.provider'),
clusters: get(provision, 'TEMPLATE.BODY.provision.infrastructure.clusters', []).length,
hosts: get(provision, 'TEMPLATE.BODY.provision.infrastructure.hosts', []).length,
networks: get(provision, 'TEMPLATE.BODY.provision.infrastructure.networks', []).length,
datastores: get(provision, 'TEMPLATE.BODY.provision.infrastructure.datastores', []).length
}))
, [provisions])
const totals = React.useMemo(() =>
provisionsByProvider?.reduce((total, { clusters, hosts, datastores, networks }) => ({
clusters: clusters + total.clusters,
hosts: hosts + total.hosts,
datastores: datastores + total.datastores,
networks: networks + total.networks
}), { clusters: 0, hosts: 0, datastores: 0, networks: 0 })
, [provisionsByProvider])
return React.useMemo(() => (
<div
data-cy='dashboard-widget-total-infrastructures'
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
gridGap: '2em'
}}
>
<WavesCard
text={T.Clusters}
value={totals.clusters}
bgColor='#fa7892'
icon={ClusterIcon}
/>
<WavesCard
text={T.Hosts}
value={totals.hosts}
bgColor='#b25aff'
icon={HostIcon}
/>
<WavesCard
text={T.Datastores}
value={totals.datastores}
bgColor='#1fbbc6'
icon={DatastoreIcon}
/>
<WavesCard
text={T.Networks}
value={totals.networks}
bgColor='#f09d42'
icon={NetworkIcon}
/>
</div>
), [totals])
}
TotalProvisionInfrastructures.displayName = 'TotalProvisionInfrastructures'
export default TotalProvisionInfrastructures

View File

@ -1,40 +0,0 @@
import * as React from 'react'
import { useHistory } from 'react-router'
import { AddCircle } from '@material-ui/icons'
import { useFetch, useProvision } from 'client/hooks'
import Circle from 'client/components/Widgets/SimpleCircle'
import { PATH } from 'client/router/provision'
import { T } from 'client/constants'
const cy = 'dashboard-widget-total-provisions'
const title = T.Provisions
const TotalProvisions = () => {
const history = useHistory()
const { provisions, getProvisions } = useProvision()
const { fetchRequest } = useFetch(getProvisions)
React.useEffect(() => { fetchRequest() }, [])
const actions = React.useMemo(() => [{
handleClick: () => history.push(PATH.PROVISIONS.CREATE),
icon: <AddCircle />,
cy: `${cy}-create`
}], [history])
const widget = React.useMemo(() => (
<Circle
label={`${provisions?.length}`}
onClick={() => history.push(PATH.PROVISIONS.LIST)}
/>
), [provisions?.length])
return { cy, title, actions, widget }
}
TotalProvisions.displayName = 'TotalProvisions'
export default TotalProvisions

View File

@ -0,0 +1,56 @@
import * as React from 'react'
import { Paper, Typography } from '@material-ui/core'
import { useProvision } from 'client/hooks'
import { SingleBar } from 'client/components/Charts'
import { groupBy } from 'client/utils'
import { T, PROVISIONS_STATES } from 'client/constants'
import useStyles from 'client/components/Widgets/TotalProvisionsByState/styles'
const TotalProvisionsByState = () => {
const { provisions } = useProvision()
const classes = useStyles()
const chartData = React.useMemo(() => {
const groups = groupBy(provisions, 'TEMPLATE.BODY.state')
return PROVISIONS_STATES.map((_, stateIndex) =>
groups[stateIndex]?.length ?? 0
)
}, [provisions])
const totalProvisions = provisions.length
const title = React.useMemo(() => (
<div className={classes.title}>
<Typography className={classes.titlePrimary}>
{`${totalProvisions} ${T.Provisions}`}
</Typography>
<Typography className={classes.titleSecondary}>
{T.InTotal}
</Typography>
</div>
), [classes, totalProvisions])
return React.useMemo(() => (
<Paper
data-cy='dashboard-widget-provisions-by-states'
className={classes.root}
>
{title}
<div className={classes.content}>
<SingleBar
legend={PROVISIONS_STATES}
data={chartData}
total={totalProvisions}
/>
</div>
</Paper>
), [classes, chartData])
}
TotalProvisionsByState.displayName = 'TotalProvisionsByState'
export default TotalProvisionsByState

View File

@ -0,0 +1,26 @@
import { makeStyles } from '@material-ui/core'
export default makeStyles(theme => ({
root: {
height: '100%',
padding: '2em',
display: 'grid',
gridAutoRows: 'auto 1fr',
alignItems: 'center'
},
title: {
padding: '0 2em 2em',
textAlign: 'left'
},
titlePrimary: {
fontSize: '2rem',
color: theme.palette.text.primary
},
titleSecondary: {
fontSize: '1.4rem',
color: theme.palette.text.secondary
},
content: {
padding: '0 2em'
}
}))

View File

@ -1,9 +1,9 @@
import SimpleCircle from 'client/components/Widgets/SimpleCircle'
import TotalProviders from 'client/components/Widgets/TotalProviders'
import TotalProvisions from 'client/components/Widgets/TotalProvisions'
import TotalProvisionsByState from 'client/components/Widgets/TotalProvisionsByState'
import TotalProvisionInfrastructures from 'client/components/Widgets/TotalProvisionInfrastructures'
export {
SimpleCircle,
TotalProviders,
TotalProvisions
TotalProvisionInfrastructures,
TotalProvisionsByState
}

View File

@ -23,6 +23,7 @@ export const BY = {
url: 'https://opennebula.io/'
}
export const _APPS = defaultApps
export const APPS = Object.keys(defaultApps)
export const APP_URL = defaultAppName ? `/${defaultAppName}` : ''
export const WEBSOCKET_URL = `${APP_URL}/websocket`

View File

@ -3,7 +3,7 @@ import * as STATES from 'client/constants/states'
export const PROVISIONS_STATES = [
{
name: STATES.PENDING,
color: '#4DBBD3',
color: '#966615',
meaning: ''
},
{
@ -18,17 +18,12 @@ export const PROVISIONS_STATES = [
},
{
name: STATES.RUNNING,
color: '#3adb76',
color: '#318b77',
meaning: ''
},
{
name: STATES.ERROR,
color: '#ec5840',
meaning: ''
},
{
name: STATES.DONE,
color: '#ec5840',
color: '#8c352a',
meaning: ''
}
]

View File

@ -24,6 +24,10 @@ module.exports = {
SelectGroup: 'Select a group',
SelectRequest: 'Select request',
/* dashboard */
InTotal: 'In Total',
Used: 'Used',
/* login */
Username: 'Username',
Password: 'Password',

View File

@ -1,26 +1,32 @@
import * as React from 'react'
import { Box, Container } from '@material-ui/core'
import { Container, Box, Grid } from '@material-ui/core'
import { ListCards } from 'client/components/List'
import { WidgetCard } from 'client/components/Cards'
import { useFetchAll, useProvision } from 'client/hooks'
import * as Widgets from 'client/components/Widgets'
function Dashboard () {
const widgets = [
Widgets.TotalProviders(),
Widgets.TotalProvisions()
]
const { getProviders, getProvisions } = useProvision()
const { fetchRequestAll } = useFetchAll()
React.useEffect(() => {
fetchRequestAll([getProviders(), getProvisions()])
}, [])
return (
<Container disableGutters>
<Box py={3}>
<ListCards
list={widgets}
keyProp='cy'
CardComponent={WidgetCard}
breakpoints={{ xs: 12, sm: 6 }}
/>
<Grid container spacing={3}>
<Grid item xs={12}>
<Widgets.TotalProvisionInfrastructures />
</Grid>
<Grid item xs={12} sm={6}>
<Widgets.TotalProviders />
</Grid>
<Grid item xs={12} sm={6}>
<Widgets.TotalProvisionsByState />
</Grid>
</Grid>
</Box>
</Container>
)

View File

@ -7,9 +7,7 @@ import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { EmptyCard } from 'client/components/Cards'
import { T } from 'client/constants'
import {
STEP_ID as PROVIDER_ID
} from 'client/containers/Providers/Form/Create/Steps/Provider'
import { STEP_ID as TEMPLATE_ID } from 'client/containers/Providers/Form/Create/Steps/Template'
import { FORM_FIELDS, STEP_FORM_SCHEMA } from './schema'
export const STEP_ID = 'connection'
@ -23,18 +21,27 @@ const Connection = () => ({
optionsValidate: { abortEarly: false },
content: useCallback(() => {
const [fields, setFields] = useState([])
const { providersTemplates } = useProvision()
const { provisionsTemplates } = useProvision()
const { watch, reset } = useFormContext()
useEffect(() => {
const {
[PROVIDER_ID]: provider,
[TEMPLATE_ID]: templateSelected,
[STEP_ID]: currentConnections
} = watch()
const providerTemplate = providersTemplates
.find(({ name }) => name === provider?.[0])
connection = providerTemplate?.connection ?? {}
const { name, provision, provider } = templateSelected?.[0]
const providerTemplate = provisionsTemplates
?.[provision]
?.providers?.[provider]
?.find(providerSelected => providerSelected.name === name) ?? {}
const {
location_key: locationKey = '',
connection: { [locationKey]: _, ...connectionEditable } = {}
} = providerTemplate
connection = connectionEditable ?? {}
setFields(FORM_FIELDS(connection))
// set defaults connection values when first render

View File

@ -0,0 +1,59 @@
import React, { useCallback, useEffect, useState } from 'react'
import { useFormContext } from 'react-hook-form'
import { useProvision } from 'client/hooks'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { EmptyCard } from 'client/components/Cards'
import { T } from 'client/constants'
import { STEP_ID as TEMPLATE_ID } from 'client/containers/Providers/Form/Create/Steps/Template'
import {
FORM_FIELDS, STEP_FORM_SCHEMA
} from 'client/containers/Providers/Form/Create/Steps/Inputs/schema'
export const STEP_ID = 'inputs'
let inputs = []
const Inputs = () => ({
id: STEP_ID,
label: T.ConfigureInputs,
resolver: () => STEP_FORM_SCHEMA(inputs),
optionsValidate: { abortEarly: false },
content: useCallback(() => {
const [fields, setFields] = useState([])
const { provisionsTemplates } = useProvision()
const { watch, reset } = useFormContext()
useEffect(() => {
const {
[TEMPLATE_ID]: templateSelected,
[STEP_ID]: currentInputs
} = watch()
const { name, provision, provider } = templateSelected?.[0]
const providerTemplate = provisionsTemplates
?.[provision]
?.providers?.[provider]
?.find(providerSelected => providerSelected.name === name) ?? {}
inputs = providerTemplate?.inputs ?? []
setFields(FORM_FIELDS(inputs))
// set defaults inputs values when first render
!currentInputs && reset({
...watch(),
[STEP_ID]: STEP_FORM_SCHEMA(inputs).default()
})
}, [])
return (fields?.length === 0) ? (
<EmptyCard title={'✔️ There is not inputs to fill'} />
) : (
<FormWithSchema cy="form-provider" fields={fields} id={STEP_ID} />
)
}, [])
})
export default Inputs

View File

@ -0,0 +1,31 @@
import * as yup from 'yup'
import { getValidationFromFields, schemaUserInput } from 'client/utils'
export const FORM_FIELDS = inputs =>
inputs?.map(({
name,
description,
type,
default: defaultValue,
min_value: min,
max_value: max,
options
}) => {
const optionsValue = options?.join(',') ?? `${min}..${max}`
return {
name,
label: `${description ?? name} *`,
...schemaUserInput({
mandatory: true,
name,
type,
options: optionsValue,
defaultValue
})
}
})
export const STEP_FORM_SCHEMA = inputs => yup.object(
getValidationFromFields(FORM_FIELDS(inputs))
)

View File

@ -1,59 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react'
import { useFormContext } from 'react-hook-form'
import { useProvision, useListForm } from 'client/hooks'
import { ListCards } from 'client/components/List'
import { EmptyCard, LocationCard } from 'client/components/Cards'
import { T } from 'client/constants'
import { STEP_ID as PROVIDER_ID } from 'client/containers/Providers/Form/Create/Steps/Provider'
import { STEP_FORM_SCHEMA } from 'client/containers/Providers/Form/Create/Steps/Locations/schema'
export const STEP_ID = 'location'
const Locations = () => ({
id: STEP_ID,
label: T.Location,
resolver: () => STEP_FORM_SCHEMA,
content: useCallback(({ data, setFormData }) => {
const [locationsList, setLocationsList] = useState([])
const { providersTemplates } = useProvision()
const { watch } = useFormContext()
const { handleSelect, handleUnselect } = useListForm({
key: STEP_ID,
setList: setFormData
})
useEffect(() => {
const { [PROVIDER_ID]: selectedProvider } = watch()
const provider = providersTemplates
.find(({ name }) => name === selectedProvider?.[0]) ?? {}
setLocationsList(
Object.entries(provider?.locations)
?.map(([key, properties]) => ({ key, properties })) ?? []
)
}, [])
return (
<ListCards
list={locationsList}
keyProp='key'
EmptyComponent={<EmptyCard title={'Your locations list is empty'} />}
CardComponent={LocationCard}
cardsProps={({ value: { key } }) => {
const isSelected = data?.some(selected => selected === key)
const handleClick = () =>
isSelected ? handleUnselect(key) : handleSelect(key)
return { isSelected, handleClick }
}}
/>
)
}, [])
})
export default Locations

View File

@ -1,8 +0,0 @@
import * as yup from 'yup'
export const STEP_FORM_SCHEMA = yup
.array(yup.string().trim())
.min(1, 'Select location')
.max(1, 'Max. one location selected')
.required('Location field is required')
.default([])

View File

@ -1,65 +0,0 @@
import React, { useCallback, useEffect } from 'react'
import { Redirect } from 'react-router-dom'
import { useFetch, useProvision, useListForm } from 'client/hooks'
import { ListCards } from 'client/components/List'
import { EmptyCard, ProvisionTemplateCard } from 'client/components/Cards'
import { PATH } from 'client/router/provision'
import { T } from 'client/constants'
import { STEP_ID as CONNECTION_ID } from 'client/containers/Providers/Form/Create/Steps/Connection'
import { STEP_ID as LOCATION_ID } from 'client/containers/Providers/Form/Create/Steps/Locations'
import { STEP_FORM_SCHEMA } from 'client/containers/Providers/Form/Create/Steps/Provider/schema'
export const STEP_ID = 'provider'
const Provider = () => ({
id: STEP_ID,
label: T.ProviderTemplate,
resolver: () => STEP_FORM_SCHEMA,
content: useCallback(({ data, setFormData }) => {
const { getProvidersTemplates } = useProvision()
const { data: templates, fetchRequest, loading, error } = useFetch(
getProvidersTemplates
)
const { handleSelect, handleUnselect } = useListForm({
key: STEP_ID,
setList: setFormData
})
useEffect(() => { fetchRequest() }, [])
const handleClick = (nameTemplate, isSelected) => {
setFormData({ [LOCATION_ID]: undefined, [CONNECTION_ID]: undefined })
isSelected ? handleUnselect(nameTemplate) : handleSelect(nameTemplate)
}
if (error) {
return <Redirect to={PATH.DASHBOARD} />
}
return (
<ListCards
list={templates}
keyProp='name'
isLoading={!templates || loading}
EmptyComponent={
<EmptyCard title={'Your providers templates list is empty'} />
}
CardComponent={ProvisionTemplateCard}
cardsProps={({ value: { name } }) => {
const isSelected = data?.some(selected => selected === name)
return {
isProvider: true,
isSelected,
handleClick: () => handleClick(name, isSelected)
}
}}
/>
)
}, [])
})
export default Provider

View File

@ -1,8 +0,0 @@
import * as yup from 'yup'
export const STEP_FORM_SCHEMA = yup
.array(yup.string().trim())
.min(1, 'Select Provider template')
.max(1, 'Max. one template selected')
.required('Provider template field is required')
.default([])

View File

@ -0,0 +1,116 @@
import React, { useCallback } from 'react'
import { useProvision, useListForm, useGeneral } from 'client/hooks'
import { ListCards } from 'client/components/List'
import { EmptyCard, ProvisionTemplateCard } from 'client/components/Cards'
import { T } from 'client/constants'
import { STEP_ID as CONNECTION_ID } from 'client/containers/Providers/Form/Create/Steps/Connection'
import { STEP_ID as INPUTS_ID } from 'client/containers/Providers/Form/Create/Steps/Inputs'
import { STEP_FORM_SCHEMA } from 'client/containers/Providers/Form/Create/Steps/Template/schema'
import { Divider, Select } from '@material-ui/core'
export const STEP_ID = 'template'
const Template = () => ({
id: STEP_ID,
label: T.ProviderTemplate,
resolver: () => STEP_FORM_SCHEMA,
content: useCallback(({ data, setFormData }) => {
const templateSelected = data?.[0]
const [provisionSelected, setProvision] = React.useState(templateSelected?.provision)
const [providerSelected, setProvider] = React.useState(templateSelected?.provider)
const { showError } = useGeneral()
const { provisionsTemplates } = useProvision()
const providersTypes = provisionsTemplates?.[provisionSelected]?.providers ?? []
const templatesAvailable = providersTypes?.[providerSelected]
const {
handleSelect,
handleUnselect,
handleClear
} = useListForm({ key: STEP_ID, setList: setFormData })
const handleChangeProvision = evt => {
setProvision(evt.target.value)
setProvider(undefined)
templateSelected && handleClear()
}
const handleChangeProvider = evt => {
setProvider(evt.target.value)
templateSelected && handleClear()
}
const handleClick = ({ name, provider, provision }, isSelected) => {
if (name === undefined || provider === undefined || provision === undefined) {
showError({ message: 'This template has bad format. Ask your cloud administrator' })
} else {
setFormData({ [INPUTS_ID]: undefined, [CONNECTION_ID]: undefined })
isSelected
? handleUnselect(name, item => item.name === name)
: handleSelect({ name, provider, provision })
}
}
const RenderOptions = ({ options = {} }) => Object.keys(options)?.map(option => (
<option key={option} value={option}>{option}</option>
))
return (
<>
<div>
<Select
color='secondary'
data-cy='select-provision-type'
native
style={{ minWidth: '8em' }}
onChange={handleChangeProvision}
value={provisionSelected}
variant='outlined'
>
<option value="">{T.None}</option>
<RenderOptions options={provisionsTemplates} />
</Select>
{provisionSelected && <Select
color='secondary'
data-cy='select-provider-type'
native
style={{ minWidth: '8em' }}
onChange={handleChangeProvider}
value={providerSelected}
variant='outlined'
>
<option value="">{T.None}</option>
<RenderOptions options={providersTypes} />
</Select>}
</div>
<Divider style={{ margin: '1rem 0' }} />
<ListCards
keyProp='name'
list={templatesAvailable}
EmptyComponent={
<EmptyCard title={'Your providers templates list is empty'} />
}
CardComponent={ProvisionTemplateCard}
cardsProps={({ value = {} }) => {
const isSelected = data?.some(selected =>
selected.name === value.name
)
return {
isProvider: true,
isSelected,
handleClick: () => handleClick(value, isSelected)
}
}}
/>
</>
)
}, [])
})
export default Template

View File

@ -0,0 +1,40 @@
import * as yup from 'yup'
import { getValidationFromFields } from 'client/utils'
const NAME = {
name: 'name',
validation: yup
.string()
.trim()
.required('Template field is required')
.default(undefined)
}
const PROVISION = {
name: 'provision',
validation: yup
.string()
.trim()
.required('Provision type field is required')
.default(undefined)
}
const PROVIDER = {
name: 'provider',
validation: yup
.string()
.trim()
.required('Provider type field is required')
.default(undefined)
}
export const PROVIDER_TEMPLATE_SCHEMA = yup.object(
getValidationFromFields([NAME, PROVISION, PROVIDER])
)
export const STEP_FORM_SCHEMA = yup
.array(PROVIDER_TEMPLATE_SCHEMA)
.min(1, 'Select provider template')
.max(1, 'Max. one template selected')
.required('Provider template field is required')
.default([])

View File

@ -1,22 +1,22 @@
import * as yup from 'yup'
import Provider from './Provider'
import Template from './Template'
import Connection from './Connection'
import Locations from './Locations'
import Inputs from './Inputs'
const Steps = ({ isUpdate }) => {
const provider = Provider()
const template = Template()
const connection = Connection()
const locations = Locations()
const inputs = Inputs()
const steps = [connection, locations]
!isUpdate && steps.unshift(provider)
const steps = [connection, inputs]
!isUpdate && steps.unshift(template)
const resolvers = () => yup
.object({
[provider.id]: provider.resolver(),
[template.id]: template.resolver(),
[connection.id]: connection.resolver(),
[locations.id]: locations.resolver()
[inputs.id]: inputs.resolver()
})
const defaultValues = resolvers().default()

View File

@ -8,30 +8,25 @@ import { yupResolver } from '@hookform/resolvers'
import FormStepper from 'client/components/FormStepper'
import Steps from 'client/containers/Providers/Form/Create/Steps'
import { useFetch, useProvision, useGeneral } from 'client/hooks'
import { PATH } from 'client/router/provision'
import { useFetchAll, useProvision, useGeneral } from 'client/hooks'
import { mapUserInputs } from 'client/utils'
function ProviderCreateForm () {
const history = useHistory()
const { id } = useParams()
const isUpdate = id !== undefined
const { showError } = useGeneral()
const {
steps,
defaultValues,
resolvers
} = Steps({ isUpdate })
const {
getProvider,
getProvidersTemplates,
createProvider,
updateProvider,
providersTemplates
provisionsTemplates
} = useProvision()
const { data, fetchRequestAll, loading, error } = useFetchAll()
const { data, fetchRequest, loading, error } = useFetch(getProvider)
const { steps, defaultValues, resolvers } = Steps({ isUpdate })
const { showError } = useGeneral()
const methods = useForm({
mode: 'onSubmit',
@ -39,33 +34,44 @@ function ProviderCreateForm () {
resolver: yupResolver(resolvers())
})
const onSubmit = formData => {
const { provider, location, connection, registration_time: time } = formData
const providerSelected = provider[0]
const locationSelected = location[0]
const getTemplate = ({ provision, provider, name } = {}) => {
const template = provisionsTemplates
?.[provision]
?.providers?.[provider]
?.find(providerSelected => providerSelected.name === name)
const providerTemplate = providersTemplates
.find(({ name }) => name === providerSelected) ?? {}
if (!providerTemplate) {
if (!template) {
showError({
message: `
Cannot found provider template (${providerSelected}),
Cannot found provider template (${provider}),
ask your cloud administrator`
})
history.push(PATH.PROVISIONS.LIST)
}
history.push(PATH.PROVIDERS.LIST)
} else return template
}
const { plain, location_key: locationKey } = providerTemplate
const onSubmit = formData => {
const { template, inputs, connection, registration_time: time } = formData
const templateSelected = template?.[0]
const providerTemplate = getTemplate(templateSelected)
const parseInputs = mapUserInputs(inputs)
const {
plain,
location_key: locationKey,
connection: { [locationKey]: connectionFixed }
} = providerTemplate
const formatData = {
...(!isUpdate && { name: `${providerSelected}_${locationSelected}` }),
...(!isUpdate && templateSelected),
...(plain && { plain }),
provider: providerSelected,
connection: {
...connection,
[locationKey]: locationSelected
[locationKey]: connectionFixed
},
inputs: providerTemplate?.inputs
?.map(input => ({ ...input, value: `${parseInputs[input?.name]}` })),
registration_time: time
}
@ -79,40 +85,35 @@ function ProviderCreateForm () {
}
useEffect(() => {
isUpdate && fetchRequestAll([
getProvider({ id }),
getProvidersTemplates()
])
isUpdate && fetchRequest({ id })
}, [isUpdate])
useEffect(() => {
if (data) {
const [provider = {}, templates = []] = data
const {
connection, provider: providerName, registration_time: time
} = provider?.TEMPLATE?.PROVISION_BODY ?? {}
connection,
inputs,
name,
provider,
provision,
registration_time: time
} = data?.TEMPLATE?.PROVISION_BODY ?? {}
const {
location_key: key
} = templates?.find(({ name }) => name === providerName) ?? {}
const templateSelected = { name, provision, provider }
const providerTemplate = getTemplate(templateSelected)
if (!key) {
showError({
message: `
Cannot found provider template (${providerName}),
ask your cloud administrator`
})
history.push(PATH.PROVIDERS.LIST)
}
const { location_key: locationKey } = providerTemplate
const { [locationKey]: _, ...connectionEditable } = connection
const { [key]: location, ...connections } = connection
const inputsNameValue = inputs?.reduce((res, input) => (
{ ...res, [input.name]: input.value }
), {})
methods.reset({
provider: [providerName],
registration_time: time,
connection: connections,
location: [location]
connection: connectionEditable,
inputs: inputsNameValue,
template: [templateSelected],
registration_time: time
}, { errors: false })
}
}, [data])

View File

@ -49,7 +49,7 @@ const Inputs = () => ({
return (fields?.length === 0) ? (
<EmptyCard title={'✔️ There is not inputs to fill'} />
) : (
<FormWithSchema cy="form-provider" fields={fields} id={STEP_ID} />
<FormWithSchema cy="form-provision" fields={fields} id={STEP_ID} />
)
}, [])
})

View File

@ -18,9 +18,9 @@ const Provision = () => ({
label: T.ProvisionTemplate,
resolver: () => STEP_FORM_SCHEMA,
content: useCallback(({ data, setFormData }) => {
const { getProvisionsTemplates } = useProvision()
const { getTemplates } = useProvision()
const { data: templates, fetchRequest, loading, error } = useFetch(
getProvisionsTemplates
getTemplates
)
const { handleSelect, handleUnselect } = useListForm({

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */

View File

@ -0,0 +1,51 @@
/* Copyright 2002-2021, 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 * as React from 'react'
import FlowApp from 'client/apps/flow'
import ProvisionApp from 'client/apps/provision'
import { _APPS, APPS } from 'client/constants'
const DevelopmentApp = props => {
let appName = ''
if (
process?.env?.NODE_ENV === 'development' &&
typeof window !== 'undefined'
) {
const parseUrl = window.location.pathname
.split(/\//gi)
.filter(sub => sub?.length > 0)
parseUrl.forEach(resource => {
if (resource && APPS.includes(resource)) {
appName = resource
}
})
}
return (
<>
{appName === _APPS.provision.name && <ProvisionApp {...props} />}
{appName === _APPS.flow.name && <FlowApp {...props} />}
</>
)
}
DevelopmentApp.displayName = 'DevelopmentApp'
export default DevelopmentApp

View File

@ -0,0 +1,36 @@
/* Copyright 2002-2021, 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 * as React from 'react'
import { render } from 'react-dom'
import store from 'client/store'
import App from 'client/dev/_app'
render(
<App store={store} />,
document.getElementById('root')
)
if (process.env.NODE_ENV === 'development' && module.hot) {
module.hot.accept('./_app', () => {
const SyncApp = require('./_app').default
render(<SyncApp store={store} />, document.getElementById('root'))
})
module.hot.accept('../reducers', () => {
store.replaceReducer(require('../reducers').default)
})
}

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */
@ -15,30 +15,11 @@
import * as React from 'react'
import { hydrate, render } from 'react-dom'
import { createStore } from 'redux'
import root from 'window-or-global'
import rootReducer from 'client/reducers'
import App from 'client/app'
import store from 'client/store'
import App from 'client/apps/flow'
// eslint-disable-next-line no-underscore-dangle
const preloadedState = root.__PRELOADED_STATE__
// eslint-disable-next-line no-underscore-dangle
delete root.__PRELOADED_STATE__
const store = createStore(
rootReducer(),
preloadedState,
// eslint-disable-next-line no-underscore-dangle
root.__REDUX_DEVTOOLS_EXTENSION__ && root.__REDUX_DEVTOOLS_EXTENSION__()
)
const element = document.getElementById('preloadState')
if (element) {
element.remove()
}
const mainDiv = document.getElementById('root')
const renderMethod = mainDiv && mainDiv.innerHTML !== '' ? hydrate : render
renderMethod(<App store={store} app='flow'/>, document.getElementById('root'))
renderMethod(<App store={store} />, document.getElementById('root'))

View File

@ -25,14 +25,18 @@ const useListForm = ({ multiple, key, list, setList, defaultValue }) => {
)
const handleUnselect = useCallback(
id =>
(id, filter = item => item === id) =>
setList(prevList => ({
...prevList,
[key]: prevList[key]?.filter(item => item !== id)
[key]: prevList[key]?.filter(filter)
})),
[key, list]
)
const handleClear = useCallback(
() => setList(prevList => ({ ...prevList, [key]: [] })), [key]
)
const handleClone = useCallback(
id => {
const itemIndex = getIndexById(list, id)
@ -53,7 +57,7 @@ const useListForm = ({ multiple, key, list, setList, defaultValue }) => {
)
const handleRemove = useCallback(
(id) => {
id => {
const newList = list?.filter(item => item.id !== id)
handleSetList(newList)
@ -87,6 +91,7 @@ const useListForm = ({ multiple, key, list, setList, defaultValue }) => {
editingData,
handleSelect,
handleUnselect,
handleClear,
handleClone,
handleRemove,
handleSetList,

View File

@ -2,20 +2,18 @@ import { useCallback } from 'react'
import { useSelector, useDispatch, shallowEqual } from 'react-redux'
import {
setProvidersTemplates,
setProviders,
setProvisionsTemplates,
setProvisions
setProvisions,
setProvisionsTemplates
} from 'client/actions/pool'
import { enqueueError, enqueueSuccess } from 'client/actions/general'
import * as serviceProvision from 'client/services/provision'
export default function useOpennebula () {
export default function useProvision () {
const dispatch = useDispatch()
const {
providersTemplates,
providers,
provisionsTemplates,
provisions,
@ -29,22 +27,22 @@ export default function useOpennebula () {
)
// --------------------------------------------
// PROVIDERS TEMPLATES REQUESTS
// ALL PROVISION TEMPLATES REQUESTS
// --------------------------------------------
const getProvidersTemplates = useCallback(
const getProvisionsTemplates = useCallback(
() =>
serviceProvision
.getProvidersTemplates({ filter })
.getProvisionsTemplates({ filter })
.then(doc => {
dispatch(setProvidersTemplates(doc))
dispatch(setProvisionsTemplates(doc))
return doc
})
.catch(err => {
dispatch(enqueueError(err ?? 'Error GET providers templates'))
dispatch(enqueueError(err ?? 'Error GET templates'))
throw err
}),
[dispatch, filter]
[dispatch]
)
// --------------------------------------------
@ -98,31 +96,14 @@ export default function useOpennebula () {
serviceProvision
.deleteProvider({ id })
.then(() => {
const newList = providers.filter(({ ID }) => ID !== id)
dispatch(enqueueSuccess(`Provider deleted - ID: ${id}`))
dispatch(setProviders(providers.filter(({ ID }) => ID !== id)))
dispatch(setProviders(newList))
})
.catch(err => dispatch(enqueueError(err ?? 'Error DELETE provider')))
, [dispatch, providers]
)
// --------------------------------------------
// PROVISIONS TEMPLATES REQUESTS
// --------------------------------------------
const getProvisionsTemplates = useCallback(
({ end, start } = { end: -1, start: -1 }) =>
serviceProvision
.getProvisionsTemplates({ filter, end, start })
.then(doc => {
dispatch(setProvisionsTemplates(doc))
return doc
})
.catch(err => {
dispatch(enqueueError(err ?? 'Error GET provisions templates'))
}),
[dispatch, filter]
)
// --------------------------------------------
// PROVISIONS REQUESTS
// --------------------------------------------
@ -234,8 +215,8 @@ export default function useOpennebula () {
)
return {
providersTemplates,
getProvidersTemplates,
getProvisionsTemplates,
provisionsTemplates,
providers,
getProvider,
@ -244,9 +225,6 @@ export default function useOpennebula () {
updateProvider,
deleteProvider,
provisionsTemplates,
getProvisionsTemplates,
provisions,
getProvision,
getProvisions,

View File

@ -2,11 +2,10 @@ import React, { useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import { CssBaseline, ThemeProvider, StylesProvider } from '@material-ui/core'
import { createTheme, generateClassName, THEMES } from 'client/theme'
import { defaultApps } from 'server/utils/constants/defaults'
import { createTheme, generateClassName } from 'client/theme'
const MuiProvider = ({ app, children }) => {
const [theme, setTheme] = useState(() => createTheme())
const MuiProvider = ({ theme: appTheme, children }) => {
const [theme, setTheme] = useState(() => createTheme(appTheme))
useEffect(() => {
const jssStyles = document.querySelector('#jss-server-side')
@ -16,8 +15,8 @@ const MuiProvider = ({ app, children }) => {
}, [])
useEffect(() => {
app && setTheme(() => createTheme(THEMES[app]))
}, [app])
appTheme && setTheme(() => createTheme(appTheme))
}, [appTheme])
return (
<ThemeProvider theme={theme}>
@ -30,7 +29,7 @@ const MuiProvider = ({ app, children }) => {
}
MuiProvider.propTypes = {
app: PropTypes.oneOf([undefined, ...Object.keys(defaultApps)]),
theme: PropTypes.object,
children: PropTypes.oneOfType([
PropTypes.node,
PropTypes.arrayOf(PropTypes.node)
@ -38,7 +37,7 @@ MuiProvider.propTypes = {
}
MuiProvider.defaultProps = {
app: undefined,
theme: {},
children: undefined
}

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */
@ -15,30 +15,11 @@
import * as React from 'react'
import { hydrate, render } from 'react-dom'
import { createStore } from 'redux'
import root from 'window-or-global'
import rootReducer from 'client/reducers'
import App from 'client/app'
import store from 'client/store'
import App from 'client/apps/provision'
// eslint-disable-next-line no-underscore-dangle
const preloadedState = root.__PRELOADED_STATE__
// eslint-disable-next-line no-underscore-dangle
delete root.__PRELOADED_STATE__
const store = createStore(
rootReducer(),
preloadedState,
// eslint-disable-next-line no-underscore-dangle
root.__REDUX_DEVTOOLS_EXTENSION__ && root.__REDUX_DEVTOOLS_EXTENSION__()
)
const element = document.getElementById('preloadState')
if (element) {
element.remove()
}
const mainDiv = document.getElementById('root')
const renderMethod = mainDiv && mainDiv.innerHTML !== '' ? hydrate : render
renderMethod(<App store={store} app='provision' />, document.getElementById('root'))
renderMethod(<App store={store} />, document.getElementById('root'))

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */
@ -38,9 +38,8 @@ const initial = {
groups: [],
vdc: [],
acl: [],
providersTemplates: [],
providers: [],
provisionsTemplates: [],
providers: [],
provisions: []
}

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */

View File

@ -16,7 +16,7 @@ export const ENDPOINTS = [
devMode: true,
sidebar: true,
icon: BallotIcon,
component: TestApi
Component: TestApi
},
{
label: 'Webconsole',
@ -25,6 +25,8 @@ export const ENDPOINTS = [
devMode: true,
sidebar: true,
icon: BallotIcon,
component: Webconsole
Component: Webconsole
}
]
export default { PATH, ENDPOINTS }

View File

@ -1,3 +0,0 @@
export * as dev from 'client/router/dev'
export * as flow from 'client/router/flow'
export * as provision from 'client/router/provision'

View File

@ -32,7 +32,7 @@ export const ENDPOINTS = [
label: 'Login',
path: PATH.LOGIN,
authenticated: false,
component: Login
Component: Login
},
{
label: 'Dashboard',
@ -40,14 +40,14 @@ export const ENDPOINTS = [
authenticated: true,
sidebar: true,
icon: DashboardIcon,
component: Dashboard
Component: Dashboard
},
{
label: 'Settings',
path: PATH.SETTINGS,
authenticated: true,
header: true,
component: Settings
Component: Settings
},
{
label: 'Templates',
@ -55,19 +55,19 @@ export const ENDPOINTS = [
authenticated: true,
sidebar: true,
icon: TemplatesIcons,
component: ApplicationsTemplates
Component: ApplicationsTemplates
},
{
label: 'Create Application template',
path: PATH.APPLICATIONS_TEMPLATES.CREATE,
authenticated: true,
component: ApplicationsTemplatesCreateForm
Component: ApplicationsTemplatesCreateForm
},
{
label: 'Edit Application template',
path: PATH.APPLICATIONS_TEMPLATES.EDIT,
authenticated: true,
component: ApplicationsTemplatesCreateForm
Component: ApplicationsTemplatesCreateForm
},
{
label: 'Instances',
@ -75,6 +75,8 @@ export const ENDPOINTS = [
authenticated: true,
sidebar: true,
icon: InstancesIcons,
component: ApplicationsInstances
Component: ApplicationsInstances
}
]
export default { PATH, ENDPOINTS }

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */
@ -13,64 +13,78 @@
/* limitations under the License. */
/* -------------------------------------------------------------------------- */
import React, { useCallback, useMemo } from 'react'
import * as React from 'react'
import PropTypes from 'prop-types'
import { Redirect, Route, Switch } from 'react-router-dom'
import { TransitionGroup } from 'react-transition-group'
import * as endpoints from 'client/router/endpoints'
import devRoutes from 'client/router/dev'
import { InternalLayout, MainLayout } from 'client/components/HOC'
import { APPS } from 'client/constants'
const Router = ({ app }) => {
const { ENDPOINTS, PATH } = useMemo(() => ({
...endpoints[app],
const Router = React.memo(({ title, routes }) => {
const { ENDPOINTS, PATH } = React.useMemo(() => ({
...routes,
...(process?.env?.NODE_ENV === 'development' &&
{ ENDPOINTS: endpoints[app].ENDPOINTS.concat(endpoints.dev.ENDPOINTS) }
{
PATH: { ...routes.PATH, ...devRoutes.PATH },
ENDPOINTS: routes.ENDPOINTS.concat(devRoutes.ENDPOINTS)
}
)
}), [app])
const renderRoute = useCallback(({
label = '',
path = '',
authenticated = true,
component: Component,
...route
}) => (
<Route
key={`key-${label.replace(' ', '-')}`}
exact
path={path}
component={() => (
<InternalLayout label={app} authRoute={authenticated}>
<Component />
</InternalLayout>
)}
{...route}
/>
), [app])
}), [])
return (
<MainLayout endpoints={{ ENDPOINTS, PATH }}>
<TransitionGroup>
<Switch>
{ENDPOINTS?.map(({ routes, ...endpoint }) =>
endpoint.path ? renderRoute(endpoint) : routes?.map(renderRoute)
{ENDPOINTS?.map(
({ path = '', authenticated = true, Component, ...route }, index) =>
<Route
key={index}
exact
path={path}
component={() => (
authenticated ? (
<InternalLayout label={title} authRoute={authenticated}>
<Component />
</InternalLayout>
) : <Component />
)}
{...route}
/>
)}
<Route component={() => <Redirect to={PATH.LOGIN} />} />
</Switch>
</TransitionGroup>
</MainLayout>
)
}
})
Router.propTypes = {
app: PropTypes.oneOf([undefined, ...APPS])
title: PropTypes.string,
routes: PropTypes.shape({
PATH: PropTypes.object,
ENDPOINTS: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
authenticated: PropTypes.bool.isRequired,
sidebar: PropTypes.bool,
icon: PropTypes.object,
Component: PropTypes.func.isRequired
})
)
})
}
Router.defaultProps = {
app: undefined
title: undefined,
routes: {
PATH: {},
ENDPOINTS: []
}
}
Router.displayName = 'Router'
export default Router

View File

@ -36,7 +36,7 @@ export const ENDPOINTS = [
label: 'Login',
path: PATH.LOGIN,
authenticated: false,
component: Login
Component: Login
},
{
label: 'Dashboard',
@ -44,14 +44,14 @@ export const ENDPOINTS = [
authenticated: true,
sidebar: true,
icon: DashboardIcon,
component: Dashboard
Component: Dashboard
},
{
label: 'Settings',
path: PATH.SETTINGS,
authenticated: true,
header: true,
component: Settings
Component: Settings
},
{
label: 'Providers',
@ -59,19 +59,19 @@ export const ENDPOINTS = [
authenticated: true,
sidebar: true,
icon: ProvidersIcon,
component: Providers
Component: Providers
},
{
label: 'Create Provider',
path: PATH.PROVIDERS.CREATE,
authenticated: true,
component: ProvidersCreateForm
Component: ProvidersCreateForm
},
{
label: 'Edit Provider template',
path: PATH.PROVIDERS.EDIT,
authenticated: true,
component: ProvidersCreateForm
Component: ProvidersCreateForm
},
{
label: 'Provisions',
@ -79,18 +79,20 @@ export const ENDPOINTS = [
authenticated: true,
sidebar: true,
icon: ProvisionsIcon,
component: Provisions
Component: Provisions
},
{
label: 'Create Provision',
path: PATH.PROVISIONS.CREATE,
authenticated: true,
component: ProvisionCreateForm
Component: ProvisionCreateForm
},
{
label: 'Edit Provision template',
path: PATH.PROVISIONS.EDIT,
authenticated: true,
component: ProvisionCreateForm
Component: ProvisionCreateForm
}
]
export default { PATH, ENDPOINTS }

View File

@ -5,17 +5,6 @@ import { requestData } from 'client/utils'
const { GET, POST, PUT, DELETE } = httpMethod
export const getProvidersTemplates = ({ filter }) =>
requestData(`/api/${PROVIDER}/defaults`, {
data: { filter },
method: GET,
error: err => err?.message
}).then(res => {
if (!res?.id || res?.id !== httpCodes.ok.id) throw res
return res?.data ?? []
})
export const getProvider = ({ id }) =>
requestData(`/api/${PROVIDER}/list/${id}`, {
method: GET,
@ -70,7 +59,6 @@ export const deleteProvider = ({ id }) =>
})
export default {
getProvidersTemplates,
getProvider,
getProviders,
createProvider,

View File

@ -6,7 +6,7 @@ import { requestData } from 'client/utils'
const { GET, POST, PUT, DELETE } = httpMethod
// --------------------------------------------
// PROVISIONS TEMPLATES REQUESTS
// ALL PROVISION TEMPLATES REQUESTS
// --------------------------------------------
export const getProvisionsTemplates = ({ filter }) =>
@ -137,6 +137,7 @@ export const configureHost = ({ id }) =>
export default {
getProvisionsTemplates,
getProvision,
getProvisions,
createProvision,

View File

@ -0,0 +1,25 @@
import root from 'window-or-global'
import { createStore, compose, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import rootReducer from 'client/reducers'
const preloadedState = root.__PRELOADED_STATE__
delete root.__PRELOADED_STATE__
const composeEnhancer =
(root && root.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose
// The store now has the ability to accept thunk functions in `dispatch`
const store = createStore(
rootReducer(),
preloadedState,
composeEnhancer(applyMiddleware(thunkMiddleware))
)
const element = document.getElementById('preloadState')
if (element) {
element.remove()
}
export default store

View File

@ -6,15 +6,6 @@ import {
import defaultTheme from 'client/theme/defaults'
import { defaultApps } from 'server/utils/constants/defaults'
import flowTheme from 'client/theme/flow'
import provisionTheme from 'client/theme/provision'
export const THEMES = {
[defaultApps.flow.theme]: flowTheme,
[defaultApps.provision.theme]: provisionTheme
}
export const generateClassName = createGenerateClassName({
productionPrefix: 'one-'
})

View File

@ -35,12 +35,6 @@ export const filterBy = (arr, predicate) => {
.values()
]
}
export const groupBy = (array, key) =>
array.reduce((objectsByKeyValue, obj) => {
const value = obj[key]
objectsByKeyValue[value] = (objectsByKeyValue[value] || []).concat(obj)
return objectsByKeyValue
}, {})
export const get = (obj, path, defaultValue = undefined) => {
const travel = regexp =>
@ -74,3 +68,13 @@ export const set = (obj, path, value) => {
return result
}
export const groupBy = (array, key) =>
array.reduce((objectsByKeyValue, obj) => {
const keyValue = get(obj, key)
const newValue = (objectsByKeyValue[keyValue] || []).concat(obj)
set(objectsByKeyValue, keyValue, newValue)
return objectsByKeyValue
}, {})

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */

View File

@ -60,30 +60,30 @@ const port = appConfig.port || defaultPort
const userLog = appConfig.log || 'dev'
if (env && env.NODE_ENV && env.NODE_ENV === defaultWebpackMode) {
// eslint-disable-next-line global-require
const config = require('../../webpack.config.dev.client')
const compiler = webpack(config)
app.use(
// eslint-disable-next-line global-require
require('webpack-dev-middleware')(compiler, {
noInfo: true,
publicPath: config.output.publicPath,
stats: {
assets: false,
colors: true,
version: false,
hash: false,
timings: false,
chunks: false,
chunkModules: false
}
})
)
// eslint-disable-next-line import/no-extraneous-dependencies
// eslint-disable-next-line global-require
app.use(require('webpack-hot-middleware')(compiler))
const webpackHotMiddleware = require('webpack-hot-middleware')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpackConfig = require('../../webpack.config.dev.client')
const compiler = webpack(webpackConfig)
app.use(webpackDevMiddleware(compiler, {
noInfo: true,
serverSideRender: true,
publicPath: webpackConfig.output.publicPath,
stats: {
assets: false,
colors: true,
version: false,
hash: false,
timings: false,
chunks: false,
chunkModules: false
}
})).use(webpackHotMiddleware(compiler))
frontPath = '../client'
}
let log = morgan('dev')
if (userLog === defaultTypeLog && global && global.FIREEDGE_LOG) {
try {
@ -101,6 +101,7 @@ if (userLog === defaultTypeLog && global && global.FIREEDGE_LOG) {
messageTerminal(config)
}
}
app.use(helmet.hidePoweredBy())
app.use(compression())
app.use(`${basename}/client`, express.static(path.resolve(__dirname, frontPath)))
@ -121,7 +122,7 @@ frontApps.map(frontApp => {
app.get(`${basename}/${frontApp}`, entrypointApp)
app.get(`${basename}/${frontApp}/*`, entrypointApp)
})
app.get('/*', (req, res) => res.send('index'))
app.get('/*', (req, res) => res.redirect(`/${defaultAppName}/provision`))
// 404 - public
app.get('*', entrypoint404)

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */

View File

@ -1,4 +1,4 @@
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */

View File

@ -1,5 +1,5 @@
/* eslint-disable import/no-dynamic-require */
/* Copyright 2002-2019, OpenNebula Project, OpenNebula Systems */
/* Copyright 2002-2021, 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 */

Some files were not shown because too many files have changed in this diff Show More