Merge branch 'f-3951'
@ -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",
|
||||
|
41
src/fireedge/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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 */
|
||||
|
@ -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 */
|
||||
|
@ -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
|
||||
}),
|
||||
|
@ -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
|
29
src/fireedge/src/client/apps/flow/_app.js
Normal 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
|
71
src/fireedge/src/client/apps/flow/index.js
Normal 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
|
@ -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
|
71
src/fireedge/src/client/apps/provision/index.js
Normal 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
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.7 KiB |
BIN
src/fireedge/src/client/assets/favicon/fireedge/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/fireedge/src/client/assets/favicon/flow/favicon-16x16.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/fireedge/src/client/assets/favicon/flow/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/fireedge/src/client/assets/favicon/flow/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.6 KiB |
BIN
src/fireedge/src/client/assets/favicon/provision/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
@ -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
|
@ -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
|
@ -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
|
||||
}
|
||||
|
||||
|
79
src/fireedge/src/client/components/Cards/WavesCard.js
Normal 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
|
@ -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
|
@ -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
|
||||
}
|
||||
|
85
src/fireedge/src/client/components/Charts/SingleBar.js
Normal 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
|
7
src/fireedge/src/client/components/Charts/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
import SimpleCircle from 'client/components/Charts/SimpleCircle'
|
||||
import SingleBar from 'client/components/Charts/SingleBar'
|
||||
|
||||
export {
|
||||
SimpleCircle,
|
||||
SingleBar
|
||||
}
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 */
|
||||
|
@ -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]
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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 */
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
@ -1,5 +1,7 @@
|
||||
import DevTypography from 'client/components/Typography/DevTypography'
|
||||
import TypographyWithPoint from 'client/components/Typography/TypographyWithPoint'
|
||||
|
||||
export {
|
||||
DevTypography
|
||||
DevTypography,
|
||||
TypographyWithPoint
|
||||
}
|
||||
|
@ -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
|
@ -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
|
@ -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
|
||||
}
|
||||
}))
|
@ -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
|
@ -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
|
@ -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
|
@ -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'
|
||||
}
|
||||
}))
|
@ -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
|
||||
}
|
||||
|
@ -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`
|
||||
|
@ -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: ''
|
||||
}
|
||||
]
|
||||
|
@ -24,6 +24,10 @@ module.exports = {
|
||||
SelectGroup: 'Select a group',
|
||||
SelectRequest: 'Select request',
|
||||
|
||||
/* dashboard */
|
||||
InTotal: 'In Total',
|
||||
Used: 'Used',
|
||||
|
||||
/* login */
|
||||
Username: 'Username',
|
||||
Password: 'Password',
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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))
|
||||
)
|
@ -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
|
@ -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([])
|
@ -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
|
@ -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([])
|
@ -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
|
@ -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([])
|
@ -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()
|
||||
|
@ -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])
|
||||
|
@ -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} />
|
||||
)
|
||||
}, [])
|
||||
})
|
||||
|
@ -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({
|
||||
|
@ -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 */
|
||||
|
51
src/fireedge/src/client/dev/_app.js
Normal 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
|
36
src/fireedge/src/client/dev/index.js
Normal 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)
|
||||
})
|
||||
}
|
@ -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'))
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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'))
|
||||
|
@ -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 */
|
||||
|
@ -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 */
|
||||
|
@ -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 */
|
||||
|
@ -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: []
|
||||
}
|
||||
|
||||
|
@ -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 */
|
||||
|
@ -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 }
|
||||
|
@ -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'
|
@ -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 }
|
||||
|
@ -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
|
||||
|
@ -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 }
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
25
src/fireedge/src/client/store.js
Normal 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
|
@ -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-'
|
||||
})
|
||||
|
@ -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
|
||||
}, {})
|
||||
|
@ -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 */
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 */
|
||||
|
@ -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 */
|
||||
|
@ -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 */
|
||||
|
@ -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 */
|
||||
|
@ -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 */
|
||||
|
@ -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 */
|
||||
|
@ -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 */
|
||||
|
@ -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 */
|
||||
|