1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-21 14:50:08 +03:00

F #3951: Redux toolkit migration (#1252)

This commit is contained in:
Sergio Betanzos 2021-05-27 11:13:38 +02:00 committed by GitHub
parent cbc6b21822
commit bb7ea25185
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
163 changed files with 3792 additions and 3506 deletions

File diff suppressed because it is too large Load Diff

View File

@ -60,6 +60,7 @@
"@material-ui/core": "4.11.0",
"@material-ui/icons": "4.11.2",
"@material-ui/lab": "4.0.0-alpha.58",
"@reduxjs/toolkit": "1.5.1",
"ace-builds": "1.4.12",
"atob": "2.1.2",
"axios": "0.21.1",
@ -108,7 +109,7 @@
"react-router": "5.2.0",
"react-router-dom": "5.2.0",
"react-transition-group": "4.4.1",
"redux": "4.0.4",
"redux": "4.1.0",
"redux-thunk": "2.3.0",
"rimraf": "3.0.2",
"socket.io": "4.1.2",

View File

@ -16,13 +16,46 @@
import * as React from 'react'
import Router from 'client/router'
import routes from 'client/router/flow'
import routes from 'client/apps/flow/routes'
import { _APPS } from 'client/constants'
import { useGeneralApi } from 'client/features/General'
import { useAuth, useAuthApi } from 'client/features/Auth'
import LoadingScreen from 'client/components/LoadingScreen'
import { fakeDelay } from 'client/utils'
import { _APPS, TIME_HIDE_LOGO } from 'client/constants'
const APP_NAME = _APPS.flow.name
const FlowApp = () => <Router title={APP_NAME} routes={routes} />
const FlowApp = () => {
const [firstRender, setFirstRender] = React.useState(() => true)
const { jwt } = useAuth()
const { getAuthUser } = useAuthApi()
const { changeTitle } = useGeneralApi()
React.useEffect(() => {
if (firstRender) {
jwt && (async () => {
await getAuthUser()
})()
fakeDelay(TIME_HIDE_LOGO).then(() => setFirstRender(false))
}
}, [firstRender, jwt])
React.useEffect(() => {
changeTitle(APP_NAME)
}, [])
if (firstRender) {
return <LoadingScreen />
}
return (
<Router routes={routes} />
)
}
FlowApp.displayName = '_FlowApp'

View File

@ -6,12 +6,25 @@ import {
import loadable from '@loadable/component'
const Login = loadable(() => import('client/containers/Login'), { ssr: false })
const Dashboard = loadable(
() => import('client/containers/Dashboard'),
{ ssr: false }
)
const Dashboard = loadable(() => import('client/containers/Dashboard'), { ssr: false })
const Settings = loadable(() => import('client/containers/Settings'), { ssr: false })
const ApplicationsTemplates = loadable(() => import('client/containers/ApplicationsTemplates'), { ssr: false })
const ApplicationsInstances = loadable(() => import('client/containers/ApplicationsInstances'), { ssr: false })
const Settings = loadable(
() => import('client/containers/Settings'),
{ ssr: false }
)
const ApplicationsTemplates = loadable(
() => import('client/containers/ApplicationsTemplates'),
{ ssr: false }
)
const ApplicationsInstances = loadable(
() => import('client/containers/ApplicationsInstances'),
{ ssr: false }
)
const ApplicationsTemplatesFormCreate = loadable(
() => import('client/containers/ApplicationsTemplates/Form/Create'),
@ -19,7 +32,6 @@ const ApplicationsTemplatesFormCreate = loadable(
)
export const PATH = {
LOGIN: '/',
DASHBOARD: '/dashboard',
APPLICATIONS_TEMPLATES: {
LIST: '/applications-templates',
@ -33,16 +45,9 @@ export const PATH = {
}
export const ENDPOINTS = [
{
label: 'Login',
path: PATH.LOGIN,
authenticated: false,
Component: Login
},
{
label: 'Dashboard',
path: PATH.DASHBOARD,
authenticated: true,
sidebar: true,
icon: DashboardIcon,
Component: Dashboard
@ -50,14 +55,12 @@ export const ENDPOINTS = [
{
label: 'Settings',
path: PATH.SETTINGS,
authenticated: true,
header: true,
Component: Settings
},
{
label: 'Templates',
path: PATH.APPLICATIONS_TEMPLATES.LIST,
authenticated: true,
sidebar: true,
icon: TemplatesIcons,
Component: ApplicationsTemplates
@ -65,19 +68,16 @@ export const ENDPOINTS = [
{
label: 'Create Application template',
path: PATH.APPLICATIONS_TEMPLATES.CREATE,
authenticated: true,
Component: ApplicationsTemplatesFormCreate
},
{
label: 'Edit Application template',
path: PATH.APPLICATIONS_TEMPLATES.EDIT,
authenticated: true,
Component: ApplicationsTemplatesFormCreate
},
{
label: 'Instances',
path: PATH.APPLICATIONS.LIST,
authenticated: true,
sidebar: true,
icon: InstancesIcons,
Component: ApplicationsInstances

View File

@ -15,25 +15,52 @@
import * as React from 'react'
import { useAuth, useProvision } from 'client/hooks'
import Router from 'client/router'
import routes from 'client/router/provision'
import routes from 'client/apps/provision/routes'
import { useGeneralApi } from 'client/features/General'
import { useAuth, useAuthApi } from 'client/features/Auth'
import { useProvisionTemplate, useProvisionApi } from 'client/features/One'
import LoadingScreen from 'client/components/LoadingScreen'
import { _APPS } from 'client/constants'
const APP_NAME = _APPS.provision.name
const ProvisionApp = () => {
const { isLogged, isLoginInProcess } = useAuth()
const { getProvisionsTemplates } = useProvision()
const { jwt } = useAuth()
const { getAuthUser, logout } = useAuthApi()
const provisionTemplate = useProvisionTemplate()
const { getProvisionsTemplates } = useProvisionApi()
const { changeTitle } = useGeneralApi()
const [firstRender, setFirstRender] = React.useState(() => !!jwt)
React.useEffect(() => {
if (isLogged && !isLoginInProcess) {
getProvisionsTemplates()
}
}, [isLogged])
(async () => {
try {
if (jwt) {
getAuthUser()
!provisionTemplate?.length && await getProvisionsTemplates()
}
return <Router title={APP_NAME} routes={routes} />
firstRender && setFirstRender(false)
} catch {
logout()
}
})()
}, [firstRender, jwt])
React.useEffect(() => {
changeTitle(APP_NAME)
}, [])
if (firstRender) {
return <LoadingScreen />
}
return <Router routes={routes} />
}
ProvisionApp.displayName = '_ProvisionApp'

View File

@ -7,7 +7,6 @@ import {
import loadable from '@loadable/component'
const Login = loadable(() => import('client/containers/Login'), { ssr: false })
const Dashboard = loadable(() => import('client/containers/Dashboard/Provision'), { ssr: false })
const Providers = loadable(() => import('client/containers/Providers'), { ssr: false })
@ -19,7 +18,6 @@ const ProvisionsFormCreate = loadable(() => import('client/containers/Provisions
const Settings = loadable(() => import('client/containers/Settings'), { ssr: false })
export const PATH = {
LOGIN: '/',
DASHBOARD: '/dashboard',
PROVIDERS: {
LIST: '/providers',
@ -36,16 +34,9 @@ export const PATH = {
}
export const ENDPOINTS = [
{
label: 'Login',
path: PATH.LOGIN,
authenticated: false,
Component: Login
},
{
label: 'Dashboard',
path: PATH.DASHBOARD,
authenticated: true,
sidebar: true,
icon: DashboardIcon,
Component: Dashboard
@ -53,7 +44,6 @@ export const ENDPOINTS = [
{
label: 'Providers',
path: PATH.PROVIDERS.LIST,
authenticated: true,
sidebar: true,
icon: ProvidersIcon,
Component: Providers
@ -61,19 +51,16 @@ export const ENDPOINTS = [
{
label: 'Create Provider',
path: PATH.PROVIDERS.CREATE,
authenticated: true,
Component: ProvidersFormCreate
},
{
label: 'Edit Provider template',
path: PATH.PROVIDERS.EDIT,
authenticated: true,
Component: ProvidersFormCreate
},
{
label: 'Provisions',
path: PATH.PROVISIONS.LIST,
authenticated: true,
sidebar: true,
icon: ProvisionsIcon,
Component: Provisions
@ -81,19 +68,16 @@ export const ENDPOINTS = [
{
label: 'Create Provision',
path: PATH.PROVISIONS.CREATE,
authenticated: true,
Component: ProvisionsFormCreate
},
{
label: 'Edit Provision template',
path: PATH.PROVISIONS.EDIT,
authenticated: true,
Component: ProvisionsFormCreate
},
{
label: 'Settings',
path: PATH.SETTINGS,
authenticated: true,
sidebar: true,
icon: SettingsIcon,
Component: Settings

View File

@ -5,7 +5,7 @@ import useFetch from 'client/hooks/useFetch'
import { SubmitButton } from 'client/components/FormControl'
const Action = memo(({ handleClick, icon, cy, ...props }) => {
const { fetchRequest, loading } = useFetch(
const { fetchRequest, data, loading } = useFetch(
() => Promise.resolve(handleClick())
)
@ -15,7 +15,8 @@ const Action = memo(({ handleClick, icon, cy, ...props }) => {
icon
isSubmitting={loading}
label={icon}
onClick={() => fetchRequest()}
onClick={fetchRequest}
disabled={!!data}
{...props}
/>
)

View File

@ -54,7 +54,7 @@ export default makeStyles(theme => ({
},
headerRoot: {
// align header icon to top
alignItems: 'end'
alignItems: 'start'
},
headerContent: { overflow: 'auto' },
headerAvatar: {

View File

@ -1,31 +1,31 @@
import ClusterCard from 'client/components/Cards/ClusterCard'
import DatastoreCard from 'client/components/Cards/DatastoreCard'
import HostCard from 'client/components/Cards/HostCard'
import NetworkCard from 'client/components/Cards/NetworkCard'
import TierCard from 'client/components/Cards/TierCard'
import EmptyCard from 'client/components/Cards/EmptyCard'
import SelectCard from 'client/components/Cards/SelectCard'
import ApplicationTemplateCard from 'client/components/Cards/ApplicationTemplateCard'
import ApplicationCard from 'client/components/Cards/ApplicationCard'
import ApplicationNetworkCard from 'client/components/Cards/ApplicationNetworkCard'
import ApplicationTemplateCard from 'client/components/Cards/ApplicationTemplateCard'
import ClusterCard from 'client/components/Cards/ClusterCard'
import DatastoreCard from 'client/components/Cards/DatastoreCard'
import EmptyCard from 'client/components/Cards/EmptyCard'
import HostCard from 'client/components/Cards/HostCard'
import NetworkCard from 'client/components/Cards/NetworkCard'
import PolicyCard from 'client/components/Cards/PolicyCard'
import ProvisionCard from 'client/components/Cards/ProvisionCard'
import ProvisionTemplateCard from 'client/components/Cards/ProvisionTemplateCard'
import SelectCard from 'client/components/Cards/SelectCard'
import TierCard from 'client/components/Cards/TierCard'
import WavesCard from 'client/components/Cards/WavesCard'
export {
ClusterCard,
DatastoreCard,
HostCard,
NetworkCard,
TierCard,
EmptyCard,
SelectCard,
ApplicationTemplateCard,
ApplicationCard,
ApplicationNetworkCard,
ApplicationTemplateCard,
ClusterCard,
DatastoreCard,
EmptyCard,
HostCard,
NetworkCard,
PolicyCard,
ProvisionTemplateCard,
ProvisionCard,
ProvisionTemplateCard,
SelectCard,
TierCard,
WavesCard
}

View File

@ -14,7 +14,7 @@ const Count = ({ start, number, duration }) => {
const timer = setInterval(() => {
start += 1
setCount(String(start) + number.substring(3))
setCount(String(start) + String(number).substring(3))
if (start === end) clearInterval(timer)
}, incrementTime)

View File

@ -19,7 +19,6 @@ const debugLogStyles = makeStyles(theme => ({
overflow: 'auto',
borderRadius: 5,
backgroundColor: '#1d1f21',
fontSize: '1.1em',
wordBreak: 'break-word',
'&::-webkit-scrollbar': {
width: 14

View File

@ -13,25 +13,23 @@ const MAX_CHARS = 80
const useStyles = makeStyles(theme => ({
root: {
display: 'flex',
alignItems: 'center',
marginBottom: '0.3em',
padding: '0.5em 0',
cursor: ({ isMoreThanMaxChars }) => isMoreThanMaxChars ? 'pointer' : 'default',
fontSize: '14px',
fontFamily: 'monospace',
color: '#fafafa',
'&:hover': {
background: '#333537'
}
},
arrow: {
padding: '0 0.5em',
width: '32px'
},
time: {
minWidth: '220px'
},
display: 'grid',
gridTemplateColumns: '32px 220px 1fr',
gap: '1em',
alignItems: 'center',
cursor: ({ isMoreThanMaxChars }) =>
isMoreThanMaxChars ? 'pointer' : 'default'
},
message: {
transition: 'all 0.3s ease-out',
whiteSpace: 'pre-line'
},
[DEBUG_LEVEL.ERROR]: { borderLeft: `0.3em solid ${theme.palette.error.light}` },
@ -56,14 +54,14 @@ const Message = memo(({ timestamp, severity, message }) => {
className={clsx(classes.root, classes[severity])}
onClick={() => setCollapse(prev => !prev)}
>
<div className={classes.arrow}>
<span>
{isMoreThanMaxChars && (isCollapsed ? (
<ChevronRightIcon fontSize='small' />
) : (
<ExpandMoreIcon fontSize='small' />
))}
</div>
<div className={classes.time}>{timestamp}</div>
</span>
<div>{timestamp}</div>
<div className={classes.message}
dangerouslySetInnerHTML={{ __html: html }} />
</div>

View File

@ -21,8 +21,8 @@ const useStyles = makeStyles(theme => ({
const DialogRequest = ({ withTabs, request, dialogProps, children }) => {
const classes = useStyles()
const methods = useFetch(request)
const { data, fetchRequest, loading, error } = methods
const fetchProps = useFetch(request)
const { data, fetchRequest, loading, error } = fetchProps
useEffect(() => { fetchRequest() }, [])
@ -47,7 +47,7 @@ const DialogRequest = ({ withTabs, request, dialogProps, children }) => {
return (
<DialogConfirmation {...dialogProps}>
{children(methods)}
{children({ fetchProps })}
</DialogConfirmation>
)
}

View File

@ -1,24 +1,28 @@
import * as React from 'react'
import PropTypes from 'prop-types'
import {
makeStyles, CircularProgress, Button, IconButton
} from '@material-ui/core'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
import { makeStyles, CircularProgress, Button, IconButton } from '@material-ui/core'
import clsx from 'clsx'
const useStyles = makeStyles(() => ({
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
const useStyles = makeStyles(theme => ({
root: {
transition: 'disabled 0.5s ease',
boxShadow: 'none'
},
disabled: {
'& svg': {
color: theme.palette.action.disabled
}
}
}))
const ButtonComponent = ({ icon, children, ...props }) => icon ? (
<IconButton {...props}>{children}</IconButton>
) : (
<Button type="submit" variant="contained" {...props}>
<Button type='submit' variant='contained' {...props}>
{children}
</Button>
)
@ -29,25 +33,28 @@ ButtonComponent.propTypes = {
}
const SubmitButton = React.memo(
({ isSubmitting, label, icon, color, size, className, ...props }) => {
({ isSubmitting, disabled, label, icon, color, size, className, ...props }) => {
const classes = useStyles()
return (
<ButtonComponent
className={clsx(classes.root, className)}
className={clsx(classes.root, className, {
[classes.disabled]: disabled
})}
color={color}
disabled={isSubmitting}
disabled={disabled || isSubmitting}
icon={icon}
size={size}
{...props}
>
{isSubmitting && <CircularProgress color="secondary" size={24} />}
{isSubmitting && <CircularProgress color='secondary' size={24} />}
{!isSubmitting && (label ?? Tr(T.Submit))}
</ButtonComponent>
)
},
(prev, next) =>
prev.isSubmitting === next.isSubmitting &&
prev.disabled === next.disabled &&
prev.label === next.label &&
prev.onClick === next.onClick
)
@ -56,6 +63,7 @@ SubmitButton.propTypes = {
icon: PropTypes.bool,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
isSubmitting: PropTypes.bool,
disabled: PropTypes.bool,
className: PropTypes.string,
color: PropTypes.oneOf(['default', 'inherit', 'primary', 'secondary']),
size: PropTypes.oneOf(['large', 'medium', 'small'])
@ -65,6 +73,7 @@ SubmitButton.defaultProps = {
icon: false,
label: undefined,
isSubmitting: false,
disabled: false,
className: undefined,
color: 'default'
}

View File

@ -4,7 +4,7 @@ import PropTypes from 'prop-types'
import { useFormContext } from 'react-hook-form'
import { useMediaQuery } from '@material-ui/core'
import { useGeneral } from 'client/hooks'
import { useGeneral } from 'client/features/General'
import CustomMobileStepper from 'client/components/FormStepper/MobileStepper'
import CustomStepper from 'client/components/FormStepper/Stepper'
import { groupBy } from 'client/utils'
@ -14,7 +14,7 @@ const FIRST_STEP = 0
const FormStepper = ({ steps, schema, onSubmit }) => {
const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs'))
const { watch, reset, errors, setError } = useFormContext()
const { isLoading, changeLoading } = useGeneral()
const { isLoading } = useGeneral()
const [formData, setFormData] = useState(() => watch())
const [activeStep, setActiveStep] = useState(FIRST_STEP)
@ -38,7 +38,6 @@ const FormStepper = ({ steps, schema, onSubmit }) => {
}
const setErrors = ({ inner = [], ...rest }) => {
changeLoading(false)
const errorsByPath = groupBy(inner, 'path') ?? {}
const totalErrors = Object.keys(errorsByPath).length

View File

@ -20,19 +20,19 @@ import clsx from 'clsx'
import { Box, Container } from '@material-ui/core'
import { CSSTransition } from 'react-transition-group'
import { useGeneral } from 'client/hooks'
import { useGeneral } from 'client/features/General'
import Header from 'client/components/Header'
import Footer from 'client/components/Footer'
import internalStyles from 'client/components/HOC/InternalLayout/styles'
const InternalLayout = ({ label, children }) => {
const InternalLayout = ({ children }) => {
const classes = internalStyles()
const scroll = React.useRef()
const container = React.useRef()
const { isFixMenu } = useGeneral()
return (
<Box className={clsx(classes.root, { [classes.isDrawerFixed]: isFixMenu })}>
<Header title={label} scrollableContainer={scroll?.current} />
<Header scrollContainer={container.current} />
<Box component="main" className={classes.main}>
<CSSTransition
in
@ -49,7 +49,7 @@ const InternalLayout = ({ label, children }) => {
timeout={300}
unmountOnExit
>
<Container ref={scroll} maxWidth={false} className={classes.scrollable}>
<Container ref={container} maxWidth={false} className={classes.scrollable}>
{children}
</Container>
</CSSTransition>
@ -60,18 +60,15 @@ const InternalLayout = ({ label, children }) => {
}
InternalLayout.propTypes = {
endpoints: PropTypes.arrayOf(PropTypes.object),
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.string
]),
label: PropTypes.string
])
}
InternalLayout.defaultProps = {
children: [],
label: null
children: []
}
export default InternalLayout

View File

@ -1,103 +0,0 @@
/* 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 React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import { useLocation, Redirect, matchPath } from 'react-router-dom'
import { useAuth, useOpennebula } from 'client/hooks'
import Sidebar from 'client/components/Sidebar'
import Notifier from 'client/components/Notifier'
import LoadingScreen from 'client/components/LoadingScreen'
const findRouteByPathname = (endpoints = [], pathname = '') => {
const route = endpoints?.find(({ path }) =>
matchPath(pathname, { path, exact: true })
)
return route ?? {}
}
const MainLayout = ({ endpoints, children }) => {
const { PATH, ENDPOINTS } = endpoints
const { pathname } = useLocation()
const { groups } = useOpennebula()
const {
isLogged,
isLoginInProcess,
getAuthInfo,
authUser,
firstRender,
isLoading
} = useAuth()
useEffect(() => {
if (isLogged && !isLoginInProcess && !isLoading) {
getAuthInfo()
}
}, [isLogged, isLoginInProcess, pathname])
const { authenticated } = findRouteByPathname(ENDPOINTS, pathname)
const authRoute = Boolean(authenticated)
// PENDING TO AUTHENTICATING OR FIRST RENDERING
if (firstRender || (isLogged && authRoute && !authUser && !groups?.length)) {
return <LoadingScreen />
}
// PROTECTED ROUTE
if (authRoute && !isLogged && !isLoginInProcess) {
return <Redirect to={PATH.LOGIN} />
}
// PUBLIC ROUTE
if (!authRoute && isLogged && !isLoginInProcess) {
return <Redirect to={PATH.DASHBOARD} />
}
return (
<>
{authRoute && isLogged && (
<>
<Sidebar endpoints={ENDPOINTS} />
<Notifier />
</>
)}
{children}
</>
)
}
MainLayout.propTypes = {
endpoints: PropTypes.shape({
PATH: PropTypes.object,
ENDPOINTS: PropTypes.array
}),
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.string
])
}
MainLayout.defaultProps = {
endpoints: {
PATH: {},
ENDPOINTS: []
},
children: ''
}
export default MainLayout

View File

@ -18,7 +18,7 @@ import PropTypes from 'prop-types'
import root from 'window-or-global'
import { sprintf } from 'sprintf-js'
import { useAuth } from 'client/hooks'
import { useAuth } from 'client/features/Auth'
import { DEFAULT_LANGUAGE, LANGUAGES_URL } from 'client/constants'
const TranslateContext = createContext()

View File

@ -14,5 +14,4 @@
/* -------------------------------------------------------------------------- */
export { default as InternalLayout } from 'client/components/HOC/InternalLayout'
export { default as MainLayout } from 'client/components/HOC/MainLayout'
export * from 'client/components/HOC/Translate'

View File

@ -4,7 +4,7 @@ import { Button } from '@material-ui/core'
import FilterIcon from '@material-ui/icons/FilterDrama'
import SelectedIcon from '@material-ui/icons/FilterVintage'
import { useAuth, useOpennebula } from 'client/hooks'
import { useAuth, useAuthApi } from 'client/features/Auth'
import Search from 'client/components/Search'
import { FILTER_POOL } from 'client/constants'
@ -15,17 +15,17 @@ const { ALL_RESOURCES, PRIMARY_GROUP_RESOURCES } = FILTER_POOL
const Group = () => {
const classes = headerStyles()
const { authUser, filterPool, setPrimaryGroup } = useAuth()
const { groups } = useOpennebula()
const { user, groups, filterPool } = useAuth()
const { changeGroup } = useAuthApi()
const handleChangeGroup = group => {
group && setPrimaryGroup({ group })
group && changeGroup({ id: user.ID, group })
}
const renderResult = ({ ID, NAME }, handleClose) => {
const isSelected =
(filterPool === ALL_RESOURCES && ALL_RESOURCES === ID) ||
(filterPool === PRIMARY_GROUP_RESOURCES && authUser?.GID === ID)
(filterPool === PRIMARY_GROUP_RESOURCES && user?.GID === ID)
return (
<Button
@ -44,9 +44,9 @@ const Group = () => {
}
const sortGroupAsMainFirst = (a, b) => {
if (a.ID === authUser?.GUID) {
if (a.ID === user?.GUID) {
return -1
} else if (b.ID === authUser?.GUID) {
} else if (b.ID === user?.GUID) {
return 1
}
return 0

View File

@ -10,11 +10,10 @@ import {
Button
} from '@material-ui/core'
import CloseIcon from '@material-ui/icons/Close'
import clsx from 'clsx'
import { Tr } from 'client/components/HOC'
import headerStyles from 'client/components/Header/styles'
import clsx from 'clsx'
const HeaderPopover = ({
id,
@ -39,17 +38,17 @@ const HeaderPopover = ({
return (
<>
<Button
color="inherit"
color='inherit'
aria-controls={anchorId}
aria-describedby={anchorId}
aria-haspopup="true"
aria-haspopup='true'
onClick={handleOpen}
{...buttonProps}
style={{ margin: '0 2px' }}
>
{icon}
{buttonLabel && (
<span className={classes.buttonLabel} data-cy="header-username">{buttonLabel}</span>
<span className={classes.buttonLabel}>{buttonLabel}</span>
)}
</Button>
<Popover
@ -75,7 +74,7 @@ const HeaderPopover = ({
{(headerTitle || isMobile) && (
<Box className={classes.header}>
{headerTitle && (
<Typography className={classes.title} variant="body1">
<Typography className={classes.title} variant='body1'>
{Tr(headerTitle)}
</Typography>
)}

View File

@ -3,31 +3,33 @@ import * as React from 'react'
import { MenuItem, MenuList, Link } from '@material-ui/core'
import AccountCircleIcon from '@material-ui/icons/AccountCircle'
import { useAuth } from 'client/hooks'
import { useAuth, useAuthApi } from 'client/features/Auth'
import HeaderPopover from 'client/components/Header/Popover'
import { DevTypography } from 'client/components/Typography'
import { Tr } from 'client/components/HOC'
import { isDevelopment } from 'client/utils'
import { T, APPS, APP_URL } from 'client/constants'
const User = React.memo(() => {
const { logout, authUser } = useAuth()
const { user } = useAuth()
const { logout } = useAuthApi()
const handleLogout = () => logout()
return (
<HeaderPopover
id="user-menu"
buttonLabel={authUser?.NAME}
id='user-menu'
buttonLabel={user?.NAME}
icon={<AccountCircleIcon />}
buttonProps={{ 'data-cy': 'header-user-button', variant: 'outlined' }}
disablePadding
>
{() => (
<MenuList>
<MenuItem onClick={handleLogout} data-cy="header-logout-button">
<MenuItem onClick={handleLogout} data-cy='header-logout-button'>
{Tr(T.SignOut)}
</MenuItem>
{process?.env?.NODE_ENV === 'development' &&
{isDevelopment() &&
APPS?.map(appName => (
<MenuItem key={appName}>
<Link color='secondary' href={`${APP_URL}/${appName}`} style={{ width: '100%' }}>

View File

@ -13,7 +13,7 @@
/* limitations under the License. */
/* -------------------------------------------------------------------------- */
import React, { useMemo } from 'react'
import * as React from 'react'
import PropTypes from 'prop-types'
import {
@ -22,73 +22,73 @@ import {
Toolbar,
Typography,
IconButton,
useMediaQuery,
useScrollTrigger
useMediaQuery
// useScrollTrigger
} from '@material-ui/core'
import MenuIcon from '@material-ui/icons/Menu'
import { useAuth, useGeneral } from 'client/hooks'
import { useAuth } from 'client/features/Auth'
import { useGeneral, useGeneralApi } from 'client/features/General'
import User from 'client/components/Header/User'
import Group from 'client/components/Header/Group'
import Zone from 'client/components/Header/Zone'
import headerStyles from 'client/components/Header/styles'
const Header = ({ title, scrollableContainer }) => {
const Header = ({ scrollContainer }) => {
const { isOneAdmin } = useAuth()
const { theme, isFixMenu, fixMenu } = useGeneral()
const { title } = useGeneral()
const { fixMenu } = useGeneralApi()
const isUpLg = useMediaQuery(theme => theme.breakpoints.up('lg'))
const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs'))
const isScroll = useScrollTrigger({
/* const isScroll = scrollContainer && useScrollTrigger({
disableHysteresis: true,
threshold: 100,
target: scrollableContainer ?? undefined
})
target: scrollContainer
}) */
const classes = headerStyles({ isScroll })
const classes = headerStyles({ isScroll: false })
const handleFixMenu = () => fixMenu(true)
const handleFixMenu = () => {
fixMenu(true)
}
return useMemo(
() => (
<AppBar className={classes.appbar} data-cy="header" elevation={1}>
<Toolbar>
{!isUpLg && (
<IconButton onClick={handleFixMenu} edge="start" color="inherit">
<MenuIcon />
</IconButton>
)}
{!isMobile && (
<Typography
variant="h6"
className={classes.title}
data-cy="header-title"
>
{'One'}
<span className={classes.app}>
{title}
</span>
</Typography>
)}
<Box flexGrow={isMobile ? 1 : 0} textAlign="end">
<User />
{!isOneAdmin && <Group />}
<Zone />
</Box>
</Toolbar>
</AppBar>
),
[theme, isFixMenu, fixMenu, isUpLg, isMobile, isOneAdmin, classes]
return (
<AppBar className={classes.appbar} data-cy="header" elevation={1}>
<Toolbar>
{!isUpLg && (
<IconButton onClick={handleFixMenu} edge="start" color="inherit">
<MenuIcon />
</IconButton>
)}
{!isMobile && (
<Typography
variant="h6"
className={classes.title}
data-cy="header-title"
>
{'One'}
<span className={classes.app}>{title}</span>
</Typography>
)}
<Box flexGrow={isMobile ? 1 : 0} textAlign="end">
<User />
{!isOneAdmin && <Group />}
<Zone />
</Box>
</Toolbar>
</AppBar>
)
}
Header.propTypes = {
title: PropTypes.string
scrollContainer: PropTypes.object
}
Header.defaultProps = {
title: ''
scrollContainer: null
}
export default Header

View File

@ -12,7 +12,6 @@ export default makeStyles(theme => ({
userSelect: 'none',
flexGrow: 1,
display: 'inline-flex',
color: theme.palette.primary.contrastText,
'& span': { textTransform: 'capitalize' }
},
app: {

View File

@ -27,57 +27,58 @@ const ListCards = ({
const classes = listCardsStyles()
const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs'))
if (isLoading) {
return <LinearProgress color='secondary' className={classes.loading} />
}
return (
<Grid container spacing={3} {...gridProps}>
{/* CREATE CARD COMPONENT */}
{handleCreate && (ButtonCreateComponent ? (
<ButtonCreateComponent onClick={handleCreate} />
) : (
isMobile ? (
<FloatingActionButton icon={<AddIcon />} onClick={handleCreate} />
) : (
<Grid item {...breakpoints}>
<Card className={classes.cardPlus} raised>
<CardActionArea onClick={handleCreate}>
<AddIcon />
</CardActionArea>
</Card>
</Grid>
)
))}
{/* LIST */}
{list.length > 0 ? (
<TransitionGroup component={null}>
{list?.map((value, index) => {
const key = value[keyProp] ?? value[keyProp.toUpperCase()]
return (
<CSSTransition
// use key to render transition (default: id or ID)
key={`card-${key.replace(/\s/g, '')}`}
classNames={classes.item}
timeout={400}
>
<Grid item {...breakpoints} {...value?.breakpoints}>
<CardComponent value={value} {...cardsProps({ index, value })} />
</Grid>
</CSSTransition>
)
})}
</TransitionGroup>
) : (
(displayEmpty || EmptyComponent) && (
<Grid item {...breakpoints}>
{EmptyComponent ?? <EmptyCard title={'Your list is empty'} />}
</Grid>
)
<>
{isLoading && (
<LinearProgress color='secondary' className={classes.loading} />
)}
</Grid>
<Grid container spacing={3} {...gridProps}>
{/* CREATE CARD COMPONENT */}
{handleCreate && (ButtonCreateComponent ? (
<ButtonCreateComponent onClick={handleCreate} />
) : (
isMobile ? (
<FloatingActionButton icon={<AddIcon />} onClick={handleCreate} />
) : (
<Grid item {...breakpoints}>
<Card className={classes.cardPlus} raised>
<CardActionArea onClick={handleCreate}>
<AddIcon />
</CardActionArea>
</Card>
</Grid>
)
))}
{/* LIST */}
{list.length > 0 ? (
<TransitionGroup component={null}>
{list?.map((value, index) => {
const key = value[keyProp] ?? value[keyProp.toUpperCase()]
return (
<CSSTransition
// use key to render transition (default: id or ID)
key={`card-${key.replace(/\s/g, '')}`}
classNames={classes.item}
timeout={400}
>
<Grid item {...breakpoints} {...value?.breakpoints}>
<CardComponent value={value} {...cardsProps({ index, value })} />
</Grid>
</CSSTransition>
)
})}
</TransitionGroup>
) : (
(displayEmpty || EmptyComponent) && (
<Grid item {...breakpoints}>
{EmptyComponent ?? <EmptyCard title={'Your list is empty'} />}
</Grid>
)
)}
</Grid>
</>
)
}

View File

@ -7,7 +7,10 @@ export default makeStyles(() => ({
display: 'flex',
textAlign: 'center'
},
loading: { width: '100%' },
loading: {
width: '100%',
marginBottom: '1em'
},
item: {
'&-enter': { opacity: 0 },
'&-enter-active': {

View File

@ -1,12 +1,11 @@
import React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import { useDispatch, useSelector } from 'react-redux'
import { useSnackbar } from 'notistack'
import { IconButton } from '@material-ui/core'
import CloseIcon from '@material-ui/icons/Close'
import { removeSnackbar } from 'client/actions/general'
import { useGeneral, useGeneralApi } from 'client/features/General'
const CloseButton = ({ handleClick }) => (
<IconButton onClick={handleClick} component="span">
@ -17,8 +16,9 @@ const CloseButton = ({ handleClick }) => (
let displayed = []
const Notifier = () => {
const dispatch = useDispatch()
const notifications = useSelector(store => store.General.notifications || [])
const { notifications } = useGeneral()
const { deleteSnackbar } = useGeneralApi()
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
const storeDisplayed = id => {
@ -43,13 +43,8 @@ const Notifier = () => {
key,
...options,
action: CloseButton({ handleClick: () => closeSnackbar(key) }),
onClose: (event, reason, myKey) => {
if (options.onClose) {
options.onClose(event, reason, myKey)
}
},
onExited: (_, myKey) => {
dispatch(removeSnackbar(myKey))
deleteSnackbar(myKey)
removeDisplayed(myKey)
}
})
@ -58,7 +53,7 @@ const Notifier = () => {
storeDisplayed(key)
}
)
}, [notifications, closeSnackbar, enqueueSnackbar, dispatch])
}, [notifications, closeSnackbar, enqueueSnackbar, deleteSnackbar])
return null
}

View File

@ -0,0 +1,28 @@
/* 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 { Redirect, Route } from 'react-router-dom'
import { useAuth } from 'client/features/Auth'
const NoAuthRoute = props => {
const { isLogged, isLoginInProgress, isLoading } = useAuth()
const isAuthenticated = isLogged && !isLoginInProgress && !isLoading
return isAuthenticated ? <Redirect to='/dashboard' /> : <Route exact {...props}/>
}
export default NoAuthRoute

View File

@ -0,0 +1,32 @@
/* 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 { Redirect, Route } from 'react-router-dom'
import { useAuth, useAuthApi } from 'client/features/Auth'
const ProtectedRoute = props => {
const { isLogged, jwt } = useAuth()
const { getAuthUser } = useAuthApi()
React.useEffect(() => {
jwt && getAuthUser()
}, [])
return isLogged ? <Route {...props}/> : <Redirect to='/' />
}
export default ProtectedRoute

View File

@ -0,0 +1,4 @@
import NoAuthRoute from 'client/components/Route/NoAuthRoute'
import ProtectedRoute from 'client/components/Route/ProtectedRoute'
export { NoAuthRoute, ProtectedRoute }

View File

@ -10,7 +10,7 @@ import {
useMediaQuery
} from '@material-ui/core'
import { useGeneral } from 'client/hooks'
import { useGeneralApi } from 'client/features/General'
import sidebarStyles from 'client/components/Sidebar/styles'
import { DevTypography } from 'client/components/Typography'
@ -18,10 +18,11 @@ const STATIC_LABEL_PROPS = { 'data-cy': 'main-menu-item-text' }
const SidebarLink = ({ label, path, icon: Icon, devMode, isSubItem }) => {
const classes = sidebarStyles()
const isUpLg = useMediaQuery(theme => theme.breakpoints.up('lg'), { noSsr: true })
const history = useHistory()
const { pathname } = useLocation()
const { fixMenu } = useGeneral()
const isUpLg = useMediaQuery(theme => theme.breakpoints.up('lg'), { noSsr: true })
const { fixMenu } = useGeneralApi()
const handleClick = () => {
history.push(path)

View File

@ -27,7 +27,7 @@ import {
} from '@material-ui/core'
import { Menu as MenuIcon, Close as CloseIcon } from '@material-ui/icons'
import { useGeneral } from 'client/hooks'
import { useGeneral, useGeneralApi } from 'client/features/General'
import sidebarStyles from 'client/components/Sidebar/styles'
import SidebarLink from 'client/components/Sidebar/SidebarLink'
import SidebarCollapseItem from 'client/components/Sidebar/SidebarCollapseItem'
@ -35,15 +35,17 @@ import Logo from 'client/icons/logo'
const Sidebar = memo(({ endpoints }) => {
const classes = sidebarStyles()
const { isFixMenu, fixMenu } = useGeneral()
const isUpLg = useMediaQuery(theme => theme.breakpoints.up('lg'), { noSsr: true })
const { isFixMenu } = useGeneral()
const { fixMenu } = useGeneralApi()
const handleSwapMenu = () => fixMenu(!isFixMenu)
const SidebarEndpoints = useMemo(
() =>
endpoints
?.filter(({ authenticated, sidebar = false }) => authenticated && sidebar)
?.filter(({ sidebar = false }) => sidebar)
?.map((endpoint, index) =>
endpoint.routes ? (
<SidebarCollapseItem key={`item-${index}`} {...endpoint} />

View File

@ -3,7 +3,7 @@ import * as React from 'react'
import { PieChart } from 'react-minimal-pie-chart'
import { Typography, Paper } from '@material-ui/core'
import { useProvision } from 'client/hooks'
import { useOne } from 'client/features/One'
import { TypographyWithPoint } from 'client/components/Typography'
import Count from 'client/components/Count'
import { groupBy } from 'client/utils'
@ -12,9 +12,8 @@ import { T, PROVIDERS_TYPES } from 'client/constants'
import useStyles from 'client/components/Widgets/TotalProviders/styles'
const TotalProviders = () => {
const { providers } = useProvision()
const classes = useStyles()
const { providers } = useOne()
const totalProviders = React.useMemo(() => providers.length, [providers.length])

View File

@ -7,14 +7,14 @@ import {
AccountTree as NetworkIcon
} from '@material-ui/icons'
import { useProvision } from 'client/hooks'
import { useOne } from 'client/features/One'
import Count from 'client/components/Count'
import { WavesCard } from 'client/components/Cards'
import { get } from 'client/utils'
import { T } from 'client/constants'
const TotalProvisionInfrastructures = () => {
const { provisions } = useProvision()
const { provisions } = useOne()
const provisionsByProvider = React.useMemo(() =>
provisions

View File

@ -2,7 +2,7 @@ import * as React from 'react'
import { Paper, Typography } from '@material-ui/core'
import { useProvision } from 'client/hooks'
import { useOne } from 'client/features/One'
import { SingleBar } from 'client/components/Charts'
import Count from 'client/components/Count'
import { groupBy } from 'client/utils'
@ -11,8 +11,8 @@ import { T, PROVISIONS_STATES } from 'client/constants'
import useStyles from 'client/components/Widgets/TotalProvisionsByState/styles'
const TotalProvisionsByState = () => {
const { provisions } = useProvision()
const classes = useStyles()
const { provisions } = useOne()
const chartData = React.useMemo(() => {
const groups = groupBy(provisions, 'TEMPLATE.BODY.state')

View File

@ -1,28 +1,34 @@
import React, { useEffect, useMemo, useState } from 'react'
import React, { useEffect, useState } from 'react'
import { Container, Box } from '@material-ui/core'
import { useApplication, useFetch } from 'client/hooks'
import { useFetch } from 'client/hooks'
import { useApplication, useApplicationApi } from 'client/features/One'
import DialogInfo from 'client/containers/ApplicationsInstances/DialogInfo'
import { ListHeader, ListCards } from 'client/components/List'
import AlertError from 'client/components/Alerts/Error'
import { ApplicationCard, EmptyCard } from 'client/components/Cards'
import { T, DONE, APPLICATION_STATES } from 'client/constants'
import { ApplicationCard } from 'client/components/Cards'
import { T } from 'client/constants'
function ApplicationsInstances () {
const { applications, getApplications } = useApplication()
const [showDialog, setShowDialog] = useState(false)
const applications = useApplication()
const { getApplications } = useApplicationApi()
const { error, fetchRequest, loading, reloading } = useFetch(getApplications)
useEffect(() => { fetchRequest() }, [])
const list = useMemo(() => (
applications.length > 0
? applications?.filter(({ TEMPLATE: { BODY: { state } } }) =>
APPLICATION_STATES[state]?.name !== DONE
)
: applications
), [applications])
// const list = useMemo(() => (
// applications.length > 0
// ? applications?.filter(({ TEMPLATE: { BODY: { state } } }) =>
// APPLICATION_STATES[state]?.name !== DONE
// )
// : applications
// ), [applications])
return (
<Container disableGutters>
@ -30,6 +36,7 @@ function ApplicationsInstances () {
title={T.ApplicationsInstances}
hasReloadButton
reloadButtonProps={{
'data-cy': 'refresh-application-list',
onClick: () => fetchRequest(undefined, { reload: true, delay: 500 }),
isSubmitting: Boolean(loading || reloading)
}}
@ -39,24 +46,22 @@ function ApplicationsInstances () {
<AlertError>{T.CannotConnectOneFlow}</AlertError>
) : (
<ListCards
list={list}
isLoading={list.length === 0 && loading}
EmptyComponent={
<EmptyCard title={'Your applications instances list is empty'} />
}
list={applications}
isLoading={applications.length === 0 && loading}
gridProps={{ 'data-cy': 'applications' }}
CardComponent={ApplicationCard}
cardsProps={({ value }) => ({
handleShow: () => setShowDialog(value)
})}
/>
)}
{showDialog !== false && (
<DialogInfo
info={showDialog}
handleClose={() => setShowDialog(false)}
/>
)}
</Box>
{showDialog !== false && (
<DialogInfo
info={showDialog}
handleClose={() => setShowDialog(false)}
/>
)}
</Container>
)
}

View File

@ -1,10 +1,11 @@
import React, { useEffect, useCallback } from 'react'
import { useOpennebula, useListForm, useFetch } from 'client/hooks'
import { useListForm, useFetch } from 'client/hooks'
import { useCluster, useClusterApi } from 'client/features/One'
import { ListCards } from 'client/components/List'
import { ClusterCard, EmptyCard } from 'client/components/Cards'
import { STEP_FORM_SCHEMA } from './schema'
import { STEP_FORM_SCHEMA } from 'client/containers/ApplicationsTemplates/Form/Create/Steps/Clusters/schema'
import { T } from 'client/constants'
export const STEP_ID = 'clusters'
@ -14,8 +15,11 @@ const Clusters = () => ({
label: T.WhereWillItRun,
resolver: STEP_FORM_SCHEMA,
content: useCallback(({ data, setFormData }) => {
const { clusters, getClusters } = useOpennebula()
const clusters = useCluster()
const { getClusters } = useClusterApi()
const { fetchRequest, loading } = useFetch(getClusters)
const { handleSelect, handleUnselect } = useListForm({
key: STEP_ID,
setList: setFormData

View File

@ -2,7 +2,9 @@ import React, { useState, useEffect, useCallback } from 'react'
import { useWatch } from 'react-hook-form'
import { useOpennebula, useListForm } from 'client/hooks'
import { useListForm } from 'client/hooks'
import { useVNetworkApi, useVNetworkTemplateApi } from 'client/features/One'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { ListCards } from 'client/components/List'
import { DialogForm } from 'client/components/Dialogs'
@ -21,7 +23,10 @@ const Networks = () => ({
content: useCallback(({ data, setFormData }) => {
const form = useWatch({})
const [showDialog, setShowDialog] = useState(false)
const { getVNetworks, getVNetworksTemplates } = useOpennebula()
const { getVNetworks } = useVNetworkApi()
const { getVNetworksTemplates } = useVNetworkTemplateApi()
const {
editingData,
handleSave,

View File

@ -1,6 +1,6 @@
import * as yup from 'yup'
import { v4 as uuidv4 } from 'uuid'
import { useOpennebula } from 'client/hooks'
import { useVNetwork, useVNetworkTemplate } from 'client/features/One'
import { INPUT_TYPES } from 'client/constants'
import { getValidationFromFields } from 'client/utils'
@ -87,7 +87,8 @@ const ID_VNET = {
type: INPUT_TYPES.AUTOCOMPLETE,
dependOf: TYPE.name,
values: dependValue => {
const { vNetworks, vNetworksTemplates } = useOpennebula()
const vNetworks = useVNetwork()
const vNetworksTemplates = useVNetworkTemplate()
const values = isNetworkSelector(dependValue) ? vNetworks : vNetworksTemplates

View File

@ -1,14 +1,15 @@
import React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import { useOpennebula } from 'client/hooks'
import { useMarketApp, useMarketAppApi } from 'client/features/One'
import Search from 'client/components/Search'
import { SelectCard } from 'client/components/Cards'
const sortByID = (a, b) => a.ID - b.ID
const ListMarketApp = ({ backButton, currentValue, handleSetData }) => {
const { apps, getMarketApps } = useOpennebula()
const apps = useMarketApp()
const { getMarketApps } = useMarketAppApi()
useEffect(() => {
getMarketApps()

View File

@ -1,14 +1,15 @@
import React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import { useOpennebula } from 'client/hooks'
import { useVmTemplate, useVmTemplateApi } from 'client/features/One'
import Search from 'client/components/Search'
import { SelectCard } from 'client/components/Cards'
const sortByID = (a, b) => a.ID - b.ID
const ListTemplates = ({ backButton, currentValue, handleSetData }) => {
const { templates, getTemplates } = useOpennebula()
const templates = useVmTemplate()
const { getTemplates } = useVmTemplateApi()
useEffect(() => {
getTemplates()

View File

@ -8,19 +8,22 @@ import { yupResolver } from '@hookform/resolvers'
import FormStepper from 'client/components/FormStepper'
import Steps from 'client/containers/ApplicationsTemplates/Form/Create/Steps'
import { PATH } from 'client/router/flow'
import { useApplication, useFetch } from 'client/hooks'
import { PATH } from 'client/apps/flow/routes'
import { useFetch } from 'client/hooks'
import { useApplicationTemplateApi } from 'client/features/One'
import { parseApplicationToForm, parseFormToApplication } from 'client/utils'
function ApplicationsTemplatesCreateForm () {
const history = useHistory()
const { id } = useParams()
const { steps, defaultValues, resolvers } = Steps()
const {
getApplicationTemplate,
createApplicationTemplate,
updateApplicationTemplate
} = useApplication()
} = useApplicationTemplateApi()
const { data, fetchRequest, loading, error } = useFetch(
getApplicationTemplate
)
@ -35,13 +38,11 @@ function ApplicationsTemplatesCreateForm () {
const application = parseFormToApplication(formData)
if (id) {
updateApplicationTemplate({ id, data: application }).then(
res => res && history.push(PATH.APPLICATIONS_TEMPLATES.LIST)
)
updateApplicationTemplate(id, application)
.then(res => res && history.push(PATH.APPLICATIONS_TEMPLATES.LIST))
} else {
createApplicationTemplate({ data: application }).then(
res => res && history.push(PATH.APPLICATIONS_TEMPLATES.LIST)
)
createApplicationTemplate(application)
.then(res => res && history.push(PATH.APPLICATIONS_TEMPLATES.LIST))
}
}
@ -51,6 +52,7 @@ function ApplicationsTemplatesCreateForm () {
useEffect(() => {
const formData = data ? parseApplicationToForm(data) : {}
methods.reset(resolvers().cast(formData), { errors: false })
}, [data])

View File

@ -2,7 +2,7 @@ import React, { useEffect, useCallback } from 'react'
import { Divider, Paper, Typography } from '@material-ui/core'
import { useOpennebula } from 'client/hooks'
import { useVNetworkApi, useVNetworkTemplateApi } from 'client/features/One'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { T } from 'client/constants'
@ -16,7 +16,8 @@ const Networks = () => ({
resolver: STEP_FORM_SCHEMA,
optionsValidate: { abortEarly: false },
content: useCallback(({ data }) => {
const { getVNetworks, getVNetworksTemplates } = useOpennebula()
const { getVNetworks } = useVNetworkApi()
const { getVNetworksTemplates } = useVNetworkTemplateApi()
useEffect(() => {
getVNetworks()

View File

@ -1,6 +1,6 @@
import * as yup from 'yup'
import { INPUT_TYPES } from 'client/constants'
import { useOpennebula } from 'client/hooks'
import { useVNetwork, useVNetworkTemplate } from 'client/features/One'
import { getValidationFromFields } from 'client/utils'
const SELECT = {
@ -36,7 +36,9 @@ const ID_VNET = {
type: INPUT_TYPES.AUTOCOMPLETE,
dependOf: TYPE.name,
values: dependValue => {
const { vNetworks, vNetworksTemplates } = useOpennebula()
const vNetworks = useVNetwork()
const vNetworksTemplates = useVNetworkTemplate()
const type = TYPES_NETWORKS.find(({ value }) => value === dependValue)
const values =

View File

@ -3,12 +3,14 @@ import PropTypes from 'prop-types'
import { makeStyles, CircularProgress, Backdrop } from '@material-ui/core'
import { useFetchAll, useOpennebula, useApplication } from 'client/hooks'
import { useFetchAll } from 'client/hooks'
import { useApplicationTemplateApi } from 'client/features/One'
import { useGeneralApi } from 'client/features/General'
import { DialogForm } from 'client/components/Dialogs'
import FormStepper from 'client/components/FormStepper'
import { parseApplicationToForm, parseFormToDeployApplication } from 'client/utils'
import Steps from 'client/containers/ApplicationsTemplates/Form/Deploy/Steps'
import { parseApplicationToForm, parseFormToDeployApplication } from 'client/utils'
const useStyles = makeStyles(theme => ({
backdrop: {
@ -20,8 +22,9 @@ const useStyles = makeStyles(theme => ({
const DeployForm = ({ applicationTemplate, handleCancel }) => {
const classes = useStyles()
const [vmTemplates, setVmTemplates] = useState([])
const { instantiateApplicationTemplate } = useApplication()
const { getTemplate } = useOpennebula()
const { enqueueInfo } = useGeneralApi()
const { getApplicationsTemplates, instantiateApplicationTemplate } = useApplicationTemplateApi()
const { data, fetchRequestAll, loading } = useFetchAll()
const applicationParsed = useMemo(() =>
@ -42,7 +45,7 @@ const DeployForm = ({ applicationTemplate, handleCancel }) => {
list.includes(templateId) ? list : [...list, templateId]
, [])
?.map(templateId =>
getTemplate({ id: templateId }).then(vmTemplate =>
getApplicationsTemplates(templateId).then(vmTemplate =>
setVmTemplates(prev => [...prev, vmTemplate])
)
)
@ -56,11 +59,12 @@ const DeployForm = ({ applicationTemplate, handleCancel }) => {
...application
} = parseFormToDeployApplication(values, applicationParsed)
return instantiateApplicationTemplate({
id: applicationTemplate.ID,
data: application,
instances
}).then(() => handleCancel())
return Promise
.all([...new Array(instances)]
.map(() => instantiateApplicationTemplate(applicationTemplate.ID, application))
)
.then(() => handleCancel())
.then(() => enqueueInfo(`Template instantiate - x${instances}`))
}
if ((applicationTemplate && !data) || loading) {

View File

@ -3,8 +3,9 @@ import React, { useEffect, useState } from 'react'
import { useHistory } from 'react-router-dom'
import { Container, Box } from '@material-ui/core'
import { PATH } from 'client/router/flow'
import { useApplication, useFetch } from 'client/hooks'
import { PATH } from 'client/apps/flow/routes'
import { useFetch } from 'client/hooks'
import { useApplicationTemplate, useApplicationTemplateApi } from 'client/features/One'
import DeployForm from 'client/containers/ApplicationsTemplates/Form/Deploy'
import { ListHeader, ListCards } from 'client/components/List'
@ -15,14 +16,12 @@ import { T } from 'client/constants'
function ApplicationsTemplates () {
const history = useHistory()
const [showDialog, setShowDialog] = useState(false)
const { applicationsTemplates, getApplicationsTemplates } = useApplication()
const {
error,
fetchRequest,
loading,
reloading
} = useFetch(getApplicationsTemplates)
const applicationsTemplates = useApplicationTemplate()
const { getApplicationsTemplates } = useApplicationTemplateApi()
const { error, fetchRequest, loading, reloading } = useFetch(getApplicationsTemplates)
console.log({ error })
useEffect(() => { fetchRequest() }, [])
return (
@ -31,9 +30,14 @@ function ApplicationsTemplates () {
title={T.ApplicationsTemplates}
hasReloadButton
reloadButtonProps={{
'data-cy': 'refresh-application-template-list',
onClick: () => fetchRequest(undefined, { reload: true, delay: 500 }),
isSubmitting: Boolean(loading || reloading)
}}
addButtonProps={{
'data-cy': 'create-application-template',
onClick: () => history.push(PATH.APPLICATIONS_TEMPLATES.CREATE)
}}
/>
<Box p={3}>
{error ? (
@ -42,24 +46,24 @@ function ApplicationsTemplates () {
<ListCards
list={applicationsTemplates}
isLoading={applicationsTemplates?.length === 0 && loading}
handleCreate={() => history.push(PATH.APPLICATIONS_TEMPLATES.CREATE)}
gridProps={{ 'data-cy': 'applications-templates' }}
CardComponent={ApplicationTemplateCard}
cardsProps={({ value }) => ({
handleEdit: () => history.push(
PATH.APPLICATIONS_TEMPLATES.EDIT.replace(':id', value?.ID)
),
handleDeploy: () => setShowDialog(value),
handleRemove: undefined
handleRemove: undefined // TODO
})}
/>
)}
{showDialog !== false && (
<DeployForm
applicationTemplate={showDialog}
handleCancel={() => setShowDialog(false)}
/>
)}
</Box>
{showDialog !== false && (
<DeployForm
applicationTemplate={showDialog}
handleCancel={() => setShowDialog(false)}
/>
)}
</Container>
)
}

View File

@ -3,19 +3,21 @@ import * as React from 'react'
import clsx from 'clsx'
import { Container, Box, Grid } from '@material-ui/core'
import { useAuth, useFetchAll, useProvision } from 'client/hooks'
import { useAuth } from 'client/features/Auth'
import { useProvisionApi, useProviderApi } from 'client/features/One'
import * as Widgets from 'client/components/Widgets'
import dashboardStyles from 'client/containers/Dashboard/Provision/styles'
function Dashboard () {
const { settings: { disableanimations } = {} } = useAuth()
const { getProviders, getProvisions } = useProvision()
const { fetchRequestAll } = useFetchAll()
const { getProvisions } = useProvisionApi()
const { getProviders } = useProviderApi()
const classes = dashboardStyles({ disableanimations })
React.useEffect(() => {
fetchRequestAll([getProviders(), getProvisions()])
getProviders()
getProvisions()
}, [])
const withoutAnimations = String(disableanimations).toUpperCase() === 'YES'

View File

@ -44,7 +44,7 @@ const Form = ({ onBack, onSubmit, resolver, fields, error, isLoading, transition
</FormProvider>
<Box>
{onBack && (
<Button onClick={onBack} color="primary" disabled={isLoading}>
<Button onClick={onBack} disabled={isLoading}>
{Tr(T.Back)}
</Button>
)}

View File

@ -2,18 +2,10 @@ import React, { useMemo, useState } from 'react'
import { Paper, Box, Container, LinearProgress, useMediaQuery } from '@material-ui/core'
import { useAuth } from 'client/hooks'
import Logo from 'client/icons/logo'
import { ONEADMIN_ID } from 'client/constants'
import {
FORM_USER_FIELDS,
FORM_USER_SCHEMA,
FORM_2FA_FIELDS,
FORM_2FA_SCHEMA,
FORM_GROUP_FIELDS,
FORM_GROUP_SCHEMA
} from 'client/containers/Login/schema'
import * as FORMS from 'client/containers/Login/schema'
import { useAuth, useAuthApi } from 'client/features/Auth'
import Form from 'client/containers/Login/Form'
import loginStyles from 'client/containers/Login/styles'
@ -26,35 +18,29 @@ const STEPS = {
function Login () {
const classes = loginStyles()
const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs'))
const [user, setUser] = useState(undefined)
const [dataUserForm, setDataUserForm] = useState(undefined)
const [step, setStep] = useState(STEPS.USER_FORM)
const {
isLoading,
error,
login,
logout,
getAuthInfo,
setPrimaryGroup
} = useAuth()
const { isLoading, error } = useAuth()
const { login, getAuthUser, changeGroup, logout } = useAuthApi()
const handleSubmitUser = dataForm => {
login({ ...user, ...dataForm }).then(data => {
if (data?.token) {
getAuthInfo().then(() => {
data?.id !== ONEADMIN_ID && setStep(STEPS.GROUP_FORM)
})
} else {
setStep(data ? STEPS.FA2_FORM : step)
setUser(data ? dataForm : user)
}
})
login({ ...dataUserForm, ...dataForm })
.then(({ jwt, user, isLoginInProgress }) => {
if (jwt && isLoginInProgress) {
getAuthUser().then(() => setStep(STEPS.GROUP_FORM))
} else if (!jwt && user?.ID) {
setStep(STEPS.FA2_FORM)
setDataUserForm(dataForm)
}
})
}
const handleSubmitGroup = dataForm => setPrimaryGroup(dataForm)
const handleSubmitGroup = dataForm => changeGroup(dataForm)
const handleBack = () => {
logout()
setUser(undefined)
setDataUserForm(undefined)
setStep(STEPS.USER_FORM)
}
@ -75,11 +61,11 @@ function Login () {
transitionProps={{
direction: 'right',
in: step === STEPS.USER_FORM,
appear: false
enter: false
}}
onSubmit={handleSubmitUser}
resolver={FORM_USER_SCHEMA}
fields={FORM_USER_FIELDS}
resolver={FORMS.FORM_USER_SCHEMA}
fields={FORMS.FORM_USER_FIELDS}
error={error}
isLoading={isLoading}
/>}
@ -90,8 +76,8 @@ function Login () {
}}
onBack={handleBack}
onSubmit={handleSubmitUser}
resolver={FORM_2FA_SCHEMA}
fields={FORM_2FA_FIELDS}
resolver={FORMS.FORM_2FA_SCHEMA}
fields={FORMS.FORM_2FA_FIELDS}
error={error}
isLoading={isLoading}
/>}
@ -102,8 +88,8 @@ function Login () {
}}
onBack={handleBack}
onSubmit={handleSubmitGroup}
resolver={FORM_GROUP_SCHEMA}
fields={FORM_GROUP_FIELDS}
resolver={FORMS.FORM_GROUP_SCHEMA}
fields={FORMS.FORM_GROUP_FIELDS}
error={error}
isLoading={isLoading}
/>}

View File

@ -3,9 +3,9 @@ import * as React from 'react'
import SelectedIcon from '@material-ui/icons/FilterVintage'
import * as yup from 'yup'
import { useAuth, useOpennebula } from 'client/hooks'
import { T, INPUT_TYPES, FILTER_POOL } from 'client/constants'
import { useAuth } from 'client/features/Auth'
import { getValidationFromFields } from 'client/utils'
import { T, INPUT_TYPES, FILTER_POOL } from 'client/constants'
export const USERNAME = {
name: 'user',
@ -75,17 +75,16 @@ export const GROUP = {
label: T.SelectGroup,
type: INPUT_TYPES.SELECT,
values: () => {
const { authUser } = useAuth()
const { groups } = useOpennebula()
const { user, groups } = useAuth()
return [{ text: T.ShowAll, value: FILTER_POOL.ALL_RESOURCES }]
.concat(groups
.concat([...groups]
.sort((a, b) => a.ID - b.ID)
.map(({ ID, NAME }) => ({
text: (
<>
{`${ID} - ${String(NAME)}`}
{authUser?.GID === ID && (
{user?.GID === ID && (
<SelectedIcon style={{ fontSize: '1rem', marginLeft: 16 }} />
)}
</>

View File

@ -47,7 +47,7 @@ const Connection = ({ isUpdate }) => ({
}, [data])
return (fields?.length === 0) ? (
<EmptyCard title={"✔️ There aren't connections to fill"} />
<EmptyCard title={"There aren't connections to fill"} />
) : (
<FormWithSchema cy="form-provider" fields={fields} id={STEP_ID} />
)

View File

@ -3,7 +3,8 @@ import { Divider, Select, Breadcrumbs, InputLabel, FormControl } from '@material
import ArrowIcon from '@material-ui/icons/ArrowForwardIosRounded'
import Marked from 'marked'
import { useProvision, useListForm } from 'client/hooks'
import { useListForm } from 'client/hooks'
import { useOne } from 'client/features/One'
import { ListCards } from 'client/components/List'
import { ProvisionTemplateCard } from 'client/components/Cards'
import { sanitize, groupBy } from 'client/utils'
@ -23,7 +24,8 @@ const Template = () => ({
resolver: () => STEP_FORM_SCHEMA,
content: useCallback(
({ data, setFormData }) => {
const { provisionsTemplates } = useProvision()
const { provisionsTemplates } = useOne()
const templateSelected = data?.[0]
const [provisionSelected, setProvision] = React.useState(

View File

@ -8,9 +8,11 @@ import { yupResolver } from '@hookform/resolvers'
import FormStepper from 'client/components/FormStepper'
import Steps from 'client/containers/Providers/Form/Create/Steps'
import { useFetchAll, useGeneral, useProvision } from 'client/hooks'
import { useFetchAll } from 'client/hooks'
import { useProviderApi } from 'client/features/One'
import { useGeneralApi } from 'client/features/General'
import * as ProviderTemplateModel from 'client/models/ProviderTemplate'
import { PATH } from 'client/router/provision'
import { PATH } from 'client/apps/provision/routes'
function ProviderCreateForm () {
const history = useHistory()
@ -19,14 +21,15 @@ function ProviderCreateForm () {
const {
getProvider,
getProviderConnection,
createProvider,
updateProvider
} = useProvision()
updateProvider,
getProviderConnection
} = useProviderApi()
const { enqueueError, enqueueSuccess, changeLoading } = useGeneralApi()
const { data, fetchRequestAll, loading, error } = useFetchAll()
const { steps, defaultValues, resolvers } = Steps({ isUpdate })
const { showError, changeLoading } = useGeneral()
const methods = useForm({
mode: 'onSubmit',
@ -35,7 +38,7 @@ function ProviderCreateForm () {
})
const redirectWithError = (message = 'Error') => {
showError({ message })
enqueueError(message)
history.push(PATH.PROVIDERS.LIST)
}
@ -47,8 +50,8 @@ function ProviderCreateForm () {
const isValid = ProviderTemplateModel.isValidProviderTemplate(templateSelected)
!isValid && redirectWithError(`
The template selected has a bad format.
Ask your cloud administrator`
The template selected has a bad format.
Ask your cloud administrator`
)
const { name, description } = configuration
@ -61,7 +64,8 @@ function ProviderCreateForm () {
name
}
createProvider({ data: formatData })
createProvider(formatData)
.then(id => enqueueSuccess(`Provider created - ID: ${id}`))
.then(() => history.push(PATH.PROVIDERS.LIST))
}
@ -78,7 +82,8 @@ function ProviderCreateForm () {
connection: { ...connection, ...connectionEditable }
}
updateProvider({ id, data: formatData })
updateProvider(id, formatData)
.then(() => enqueueSuccess(`Provider updated - ID: ${id}`))
.then(() => history.push(PATH.PROVIDERS.LIST))
}
@ -89,8 +94,8 @@ function ProviderCreateForm () {
useEffect(() => {
isUpdate && fetchRequestAll([
getProvider({ id }),
getProviderConnection({ id })
getProvider(id),
getProviderConnection(id)
])
}, [isUpdate])

View File

@ -5,20 +5,21 @@ import { List, ListItem, Typography, Grid, Paper, Divider } from '@material-ui/c
import { CheckBox, CheckBoxOutlineBlank, Visibility } from '@material-ui/icons'
import clsx from 'clsx'
import { useProvision } from 'client/hooks'
import { useProviderApi } from 'client/features/One'
import { Action } from 'client/components/Cards/SelectCard'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
import * as Types from 'client/types/provision'
import useStyles from 'client/containers/Providers/Sections/styles'
const Info = memo(({ data }) => {
const Info = memo(({ fetchProps }) => {
const classes = useStyles()
const { getProviderConnection } = useProvision()
const { getProviderConnection } = useProviderApi()
const [showConnection, setShowConnection] = useState(undefined)
const { ID, NAME, GNAME, UNAME, PERMISSIONS, TEMPLATE } = data
const { ID, NAME, GNAME, UNAME, PERMISSIONS, TEMPLATE } = fetchProps?.data
const {
connection,
description,
@ -35,8 +36,7 @@ const Info = memo(({ data }) => {
<Action
icon={<Visibility />}
cy='provider-connection'
handleClick={() => getProviderConnection({ id: ID })
.then(setShowConnection)}
handleClick={() => getProviderConnection(ID).then(setShowConnection)}
/>
)
@ -148,11 +148,15 @@ const Info = memo(({ data }) => {
})
Info.propTypes = {
data: PropTypes.object.isRequired
fetchProps: PropTypes.shape({
data: Types.Provision.isRequired
}).isRequired
}
Info.defaultProps = {
data: {}
fetchProps: {
data: {}
}
}
Info.displayName = 'Info'

View File

@ -5,12 +5,14 @@ import { Container, Box } from '@material-ui/core'
import EditIcon from '@material-ui/icons/Settings'
import DeleteIcon from '@material-ui/icons/Delete'
import { PATH } from 'client/router/provision'
import { useProvision, useFetch, useSearch } from 'client/hooks'
import { PATH } from 'client/apps/provision/routes'
import { useProvider, useProviderApi } from 'client/features/One'
import { useGeneralApi } from 'client/features/General'
import { useFetch, useSearch } from 'client/hooks'
import { ListHeader, ListCards } from 'client/components/List'
import AlertError from 'client/components/Alerts/Error'
import { ProvisionCard } from 'client/components/Cards'
import { DialogRequest } from 'client/components/Dialogs'
import Information from 'client/containers/Providers/Sections/info'
import { T } from 'client/constants'
@ -19,9 +21,12 @@ function Providers () {
const history = useHistory()
const [showDialog, setShowDialog] = useState(false)
const { providers, getProviders, getProvider, deleteProvider } = useProvision()
const providers = useProvider()
const { getProviders, getProvider, deleteProvider } = useProviderApi()
const { enqueueSuccess } = useGeneralApi()
const { error, fetchRequest, loading, reloading } = useFetch(getProviders)
const { result, handleChange } = useSearch({
list: providers,
listOptions: { shouldSort: true, keys: ['ID', 'NAME'] }
@ -36,10 +41,14 @@ function Providers () {
<ListHeader
title={T.Providers}
reloadButtonProps={{
'data-cy': 'refresh-provider-list',
onClick: () => fetchRequest(undefined, { reload: true }),
isSubmitting: Boolean(loading || reloading)
}}
addButtonProps={{ 'data-cy': 'create-provider', onClick: () => history.push(PATH.PROVIDERS.CREATE) }}
addButtonProps={{
'data-cy': 'create-provider',
onClick: () => history.push(PATH.PROVIDERS.CREATE)
}}
searchProps={{ handleChange }}
/>
<Box p={3}>
@ -68,10 +77,14 @@ function Providers () {
{
handleClick: () => setShowDialog({
id: ID,
title: `DELETE provider - #${ID} - ${NAME}`,
title: `DELETE - ${NAME}`,
subheader: `#${ID}`,
handleAccept: () => {
deleteProvider({ id: ID })
setShowDialog(false)
return deleteProvider(ID)
.then(() => enqueueSuccess(`Provider deleted - ID: ${ID}`))
.then(() => fetchRequest(undefined, { reload: true }))
}
}),
icon: <DeleteIcon color='error' />,
@ -84,10 +97,10 @@ function Providers () {
</Box>
{showDialog !== false && (
<DialogRequest
request={() => getProvider({ id: showDialog.id })}
request={() => getProvider(showDialog.id)}
dialogProps={{ handleCancel, ...showDialog }}
>
{({ data }) => <Information data={data} />}
{fetchProps => <Information {...fetchProps} />}
</DialogRequest>
)}
</Container>

View File

@ -3,64 +3,70 @@ import PropTypes from 'prop-types'
import DeleteIcon from '@material-ui/icons/Delete'
import { useProvision, useOpennebula, useFetchAll } from 'client/hooks'
import { useFetchAll } from 'client/hooks'
import { useDatastoreApi, useProvisionApi } from 'client/features/One'
import { useGeneralApi } from 'client/features/General'
import { ListCards } from 'client/components/List'
import { DatastoreCard } from 'client/components/Cards'
import * as Types from 'client/types/provision'
const Datastores = memo(({ hidden, data, fetchRequest }) => {
const {
datastores
} = data?.TEMPLATE?.BODY?.provision?.infrastructure
const Datastores = memo(
({ hidden, data, reloading, refetchProvision, disableAllActions }) => {
const {
datastores = []
} = data?.TEMPLATE?.BODY?.provision?.infrastructure
const { deleteDatastore } = useProvision()
const { getDatastore } = useOpennebula()
const { data: list, fetchRequestAll, loading } = useFetchAll()
const { enqueueSuccess } = useGeneralApi()
const { deleteDatastore } = useProvisionApi()
const { getDatastore } = useDatastoreApi()
useEffect(() => {
if (!hidden) {
const requests = datastores?.map(getDatastore) ?? []
fetchRequestAll(requests)
}
}, [datastores])
const { data: list, fetchRequestAll, loading } = useFetchAll()
const fetchDatastores = () => fetchRequestAll(datastores?.map(({ id }) => getDatastore(id)))
useEffect(() => {
if (!list && !hidden) {
const requests = datastores?.map(getDatastore) ?? []
fetchRequestAll(requests)
}
}, [hidden])
useEffect(() => {
!hidden && !list && fetchDatastores()
}, [hidden])
return (
<ListCards
list={list}
isLoading={!list && loading}
CardComponent={DatastoreCard}
cardsProps={({ value: { ID } }) => ({
actions: [{
handleClick: () => deleteDatastore({ id: ID })
.then(() => fetchRequest(undefined, { reload: true })),
icon: <DeleteIcon color='error' />,
cy: `provision-datastore-delete-${ID}`
}]
})}
displayEmpty
breakpoints={{ xs: 12, md: 6 }}
/>
)
}, (prev, next) =>
prev.hidden === next.hidden && prev.data === next.data)
useEffect(() => {
!reloading && !loading && fetchDatastores()
}, [reloading])
return (
<ListCards
list={list}
isLoading={!list && loading}
CardComponent={DatastoreCard}
cardsProps={({ value: { ID } }) => !disableAllActions && ({
actions: [{
handleClick: () => deleteDatastore(ID)
.then(refetchProvision)
.then(() => enqueueSuccess(`Datastore deleted - ID: ${ID}`)),
icon: <DeleteIcon color='error' />,
cy: `provision-datastore-delete-${ID}`
}]
})}
displayEmpty
breakpoints={{ xs: 12, md: 6 }}
/>
)
}, (prev, next) =>
prev.hidden === next.hidden && prev.reloading === next.reloading
)
Datastores.propTypes = {
data: Types.Provision.isRequired,
hidden: PropTypes.bool,
fetchRequest: PropTypes.func
refetchProvision: PropTypes.func,
reloading: PropTypes.bool,
disableAllActions: PropTypes.bool
}
Datastores.defaultProps = {
data: {},
hidden: false,
fetchRequest: () => undefined
refetchProvision: () => undefined,
reloading: false,
disableAllActions: false
}
Datastores.displayName = 'Datastores'

View File

@ -4,71 +4,78 @@ import PropTypes from 'prop-types'
import DeleteIcon from '@material-ui/icons/Delete'
import ConfigureIcon from '@material-ui/icons/Settings'
import { useProvision, useOpennebula, useFetchAll } from 'client/hooks'
import { useFetchAll } from 'client/hooks'
import { useHostApi, useProvisionApi } from 'client/features/One'
import { useGeneralApi } from 'client/features/General'
import { ListCards } from 'client/components/List'
import { HostCard } from 'client/components/Cards'
import * as Types from 'client/types/provision'
const Hosts = memo(({ hidden, data, fetchRequest }) => {
const {
hosts
} = data?.TEMPLATE?.BODY?.provision?.infrastructure
const Hosts = memo(
({ hidden, data, reloading, refetchProvision, disableAllActions }) => {
const {
hosts = []
} = data?.TEMPLATE?.BODY?.provision?.infrastructure
const { configureHost, deleteHost } = useProvision()
const { getHost } = useOpennebula()
const { data: list, fetchRequestAll, loading } = useFetchAll()
const { enqueueSuccess, enqueueInfo } = useGeneralApi()
const { configureHost, deleteHost } = useProvisionApi()
const { getHost } = useHostApi()
useEffect(() => {
if (!hidden) {
const requests = hosts?.map(getHost) ?? []
fetchRequestAll(requests)
}
}, [hosts])
const { data: list, fetchRequestAll, loading } = useFetchAll()
const fetchHosts = () => fetchRequestAll(hosts?.map(({ id }) => getHost(id)))
useEffect(() => {
if (!list && !hidden) {
const requests = hosts?.map(getHost) ?? []
fetchRequestAll(requests)
}
}, [hidden])
useEffect(() => {
!hidden && !list && fetchHosts()
}, [hidden])
return (
<ListCards
list={list}
isLoading={!list && loading}
CardComponent={HostCard}
cardsProps={({ value: { ID } }) => ({
actions: [
{
handleClick: () => configureHost({ id: ID }),
icon: <ConfigureIcon />,
cy: `provision-host-configure-${ID}`
},
{
handleClick: () => deleteHost({ id: ID })
.then(() => fetchRequest(undefined, { reload: true })),
icon: <DeleteIcon color='error' />,
cy: `provision-host-delete-${ID}`
}
]
})}
displayEmpty
breakpoints={{ xs: 12, md: 6 }}
/>
)
}, (prev, next) =>
prev.hidden === next.hidden && prev.data === next.data)
useEffect(() => {
!reloading && !loading && fetchHosts()
}, [reloading])
return (
<ListCards
list={list}
isLoading={!list && loading}
CardComponent={HostCard}
cardsProps={({ value: { ID } }) => !disableAllActions && ({
actions: [
{
handleClick: () => configureHost(ID)
.then(() => enqueueInfo(`Configuring host - ID: ${ID}`))
.then(refetchProvision),
icon: <ConfigureIcon />,
cy: `provision-host-configure-${ID}`
},
{
handleClick: () => deleteHost(ID)
.then(refetchProvision)
.then(() => enqueueSuccess(`Host deleted - ID: ${ID}`)),
icon: <DeleteIcon color='error' />,
cy: `provision-host-delete-${ID}`
}
]
})}
displayEmpty
breakpoints={{ xs: 12, md: 6 }}
/>
)
}, (prev, next) =>
prev.hidden === next.hidden && prev.reloading === next.reloading)
Hosts.propTypes = {
data: Types.Provision.isRequired,
hidden: PropTypes.bool,
fetchRequest: PropTypes.func
refetchProvision: PropTypes.func,
reloading: PropTypes.bool,
disableAllActions: PropTypes.bool
}
Hosts.defaultProps = {
data: {},
hidde: false,
fetchRequest: () => undefined
hidden: false,
refetchProvision: () => undefined,
reloading: false,
disableAllActions: false
}
Hosts.displayName = 'Hosts'

View File

@ -24,15 +24,16 @@ const TABS = [
{ name: 'log', icon: LogIcon, content: LogTab }
]
const DialogInfo = ({ data, ...methods }) => {
const DialogInfo = ({ disableAllActions, fetchProps }) => {
const [tabSelected, setTab] = useState(0)
const { data, fetchRequest, reloading } = fetchProps
const renderTabs = useMemo(() => (
<AppBar position="static">
<AppBar position='static'>
<Tabs
value={tabSelected}
variant="scrollable"
scrollButtons="auto"
variant='scrollable'
scrollButtons='auto'
onChange={(_, tab) => setTab(tab)}
>
{TABS.map(({ name, icon: Icon }, idx) =>
@ -57,27 +58,37 @@ const DialogInfo = ({ data, ...methods }) => {
height={1}
hidden={tabSelected !== idx}
key={`tab-${name}`}
overflow="auto"
overflow='auto'
>
<Content
data={data}
hidden={tabSelected !== idx}
{...methods}
disableAllActions={disableAllActions}
refetchProvision={() => fetchRequest(undefined, { reload: true })}
reloading={reloading}
/>
</Box>
)), [tabSelected, data])}
)), [tabSelected, reloading])}
</>
)
}
DialogInfo.propTypes = {
data: Types.Provision.isRequired,
fetchRequest: PropTypes.func
disableAllActions: PropTypes.bool,
fetchProps: PropTypes.shape({
data: Types.Provision.isRequired,
fetchRequest: PropTypes.func,
reloading: PropTypes.bool
}).isRequired
}
DialogInfo.defaultProps = {
data: {},
fetchRequest: undefined
disableAllActions: false,
fetchProps: {
data: {},
fetchRequest: undefined,
reloading: false
}
}
export default DialogInfo

View File

@ -31,7 +31,7 @@ const Info = memo(({ data }) => {
return (
<Grid container spacing={1}>
<Grid item xs={12} md={6}>
<Paper variant="outlined">
<Paper variant='outlined'>
<List className={clsx(classes.list, 'w-50')}>
<ListItem className={classes.title}>
<Typography>{Tr(T.Information)}</Typography>
@ -43,19 +43,19 @@ const Info = memo(({ data }) => {
</ListItem>
<ListItem>
<Typography>{Tr(T.Name)}</Typography>
<Typography data-cy="provision-name">{name}</Typography>
<Typography data-cy='provision-name'>{name}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.Description)}</Typography>
<Typography data-cy="provision-description" noWrap>{description}</Typography>
<Typography data-cy='provision-description' noWrap>{description}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.Provider)}</Typography>
<Typography data-cy="provider-provider-name">{providerName}</Typography>
<Typography data-cy='provider-name'>{providerName}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.Cluster)}</Typography>
<Typography data-cy="provider-cluster">{`${clusterId} - ${clusterName}`}</Typography>
<Typography data-cy='provider-cluster'>{`${clusterId} - ${clusterName}`}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.StartTime)}</Typography>
@ -73,7 +73,7 @@ const Info = memo(({ data }) => {
</Paper>
</Grid>
<Grid item xs={12} md={6}>
<Paper variant="outlined" className={classes.permissions}>
<Paper variant='outlined' className={classes.permissions}>
<List className={clsx(classes.list, 'w-25')}>
<ListItem className={classes.title}>
<Typography>{Tr(T.Permissions)}</Typography>
@ -102,7 +102,7 @@ const Info = memo(({ data }) => {
</ListItem>
</List>
</Paper>
<Paper variant="outlined">
<Paper variant='outlined'>
<List className={clsx(classes.list, 'w-50')}>
<ListItem className={classes.title}>
<Typography>{Tr(T.Ownership)}</Typography>

View File

@ -3,26 +3,23 @@ import PropTypes from 'prop-types'
import { LinearProgress } from '@material-ui/core'
import { useProvision, useFetch, useSocket } from 'client/hooks'
import { useFetch, useSocket } from 'client/hooks'
import { useProvisionApi } from 'client/features/One'
import DebugLog, { LogUtils } from 'client/components/DebugLog'
import * as Types from 'client/types/provision'
const Log = React.memo(({ hidden, data: { ID: id } }) => {
const Log = React.memo(({ hidden, data: { ID } }) => {
const { getProvision } = useSocket()
const { getProvisionLog } = useProvision()
const { getProvisionLog } = useProvisionApi()
const {
data: { uuid = id, log } = {},
data: { uuid = ID, log } = {},
fetchRequest,
loading
} = useFetch(getProvisionLog)
React.useEffect(() => {
!hidden && fetchRequest({ id })
}, [id])
React.useEffect(() => {
(!log && !hidden) && fetchRequest({ id })
(!log && !hidden) && fetchRequest(ID)
}, [hidden])
const parsedLog = React.useMemo(() =>
@ -39,7 +36,8 @@ const Log = React.memo(({ hidden, data: { ID: id } }) => {
<DebugLog uuid={uuid} socket={getProvision} logDefault={parsedLog} />
)
}, (prev, next) =>
prev.hidden === next.hidden && prev.data === next.data)
prev.hidden === next.hidden && prev.reloading === next.reloading
)
Log.propTypes = {
data: Types.Provision.isRequired,

View File

@ -3,64 +3,70 @@ import PropTypes from 'prop-types'
import DeleteIcon from '@material-ui/icons/Delete'
import { useProvision, useOpennebula, useFetchAll } from 'client/hooks'
import { useFetchAll } from 'client/hooks'
import { useVNetworkApi, useProvisionApi } from 'client/features/One'
import { useGeneralApi } from 'client/features/General'
import { ListCards } from 'client/components/List'
import { NetworkCard } from 'client/components/Cards'
import * as Types from 'client/types/provision'
const Networks = memo(({ hidden, data, fetchRequest }) => {
const {
networks
} = data?.TEMPLATE?.BODY?.provision?.infrastructure
const Networks = memo(
({ hidden, data, reloading, refetchProvision, disableAllActions }) => {
const {
networks = []
} = data?.TEMPLATE?.BODY?.provision?.infrastructure
const { deleteVNetwork } = useProvision()
const { getVNetwork } = useOpennebula()
const { data: list, fetchRequestAll, loading } = useFetchAll()
const { enqueueSuccess } = useGeneralApi()
const { deleteVNetwork } = useProvisionApi()
const { getVNetwork } = useVNetworkApi()
useEffect(() => {
if (!hidden) {
const requests = networks?.map(getVNetwork) ?? []
fetchRequestAll(requests)
}
}, [networks])
const { data: list, fetchRequestAll, loading } = useFetchAll()
const fetchVNetworks = () => fetchRequestAll(networks?.map(({ id }) => getVNetwork(id)))
useEffect(() => {
if (!list && !hidden) {
const requests = networks?.map(getVNetwork) ?? []
fetchRequestAll(requests)
}
}, [hidden])
useEffect(() => {
!hidden && !list && fetchVNetworks()
}, [hidden])
return (
<ListCards
list={list}
isLoading={!list && loading}
CardComponent={NetworkCard}
cardsProps={({ value: { ID } }) => ({
actions: [{
handleClick: () => deleteVNetwork({ id: ID })
.then(() => fetchRequest(undefined, { reload: true })),
icon: <DeleteIcon color='error' />,
cy: `provision-vnet-delete-${ID}`
}]
})}
displayEmpty
breakpoints={{ xs: 12, md: 6 }}
/>
)
}, (prev, next) =>
prev.hidden === next.hidden && prev.data === next.data)
useEffect(() => {
!reloading && !loading && fetchVNetworks()
}, [reloading])
return (
<ListCards
list={list}
isLoading={!list && loading}
CardComponent={NetworkCard}
cardsProps={({ value: { ID } }) => !disableAllActions && ({
actions: [{
handleClick: () => deleteVNetwork(ID)
.then(refetchProvision)
.then(() => enqueueSuccess(`VNetwork deleted - ID: ${ID}`)),
icon: <DeleteIcon color='error' />,
cy: `provision-vnet-delete-${ID}`
}]
})}
displayEmpty
breakpoints={{ xs: 12, md: 6 }}
/>
)
}, (prev, next) =>
prev.hidden === next.hidden && prev.reloading === next.reloading
)
Networks.propTypes = {
data: Types.Provision.isRequired,
hidden: PropTypes.bool,
fetchRequest: PropTypes.func
refetchProvision: PropTypes.func,
reloading: PropTypes.bool,
disableAllActions: PropTypes.bool
}
Networks.defaultProps = {
data: {},
hidden: false,
fetchRequest: () => undefined
refetchProvision: () => undefined,
reloading: false,
disableAllActions: false
}
Networks.displayName = 'Networks'

View File

@ -3,7 +3,9 @@ import React, { useCallback, useEffect, useState } from 'react'
import { useFormContext } from 'react-hook-form'
import { LinearProgress } from '@material-ui/core'
import { useProvision, useFetch, useGeneral } from 'client/hooks'
import { useFetch } from 'client/hooks'
import { useGeneralApi } from 'client/features/General'
import { useProviderApi } from 'client/features/One'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { EmptyCard } from 'client/components/Cards'
import { T } from 'client/constants'
@ -25,8 +27,8 @@ const Inputs = () => ({
optionsValidate: { abortEarly: false },
content: useCallback(() => {
const [fields, setFields] = useState(undefined)
const { changeLoading } = useGeneral()
const { getProvider } = useProvision()
const { changeLoading } = useGeneralApi()
const { getProvider } = useProviderApi()
const { data: fetchData, fetchRequest, loading } = useFetch(getProvider)
const { watch, reset } = useFormContext()
@ -35,7 +37,7 @@ const Inputs = () => ({
if (!currentInputs) {
changeLoading(true) // disable finish button until provider is fetched
fetchRequest({ id: providerSelected[0]?.ID })
fetchRequest(providerSelected[0]?.ID)
} else {
setFields(FORM_FIELDS(inputs))
}

View File

@ -1,7 +1,8 @@
import React, { useCallback } from 'react'
import { useWatch } from 'react-hook-form'
import { useProvision, useListForm } from 'client/hooks'
import { useListForm } from 'client/hooks'
import { useOne } from 'client/features/One'
import { ListCards } from 'client/components/List'
import { EmptyCard, ProvisionCard } from 'client/components/Cards'
import { T } from 'client/constants'
@ -17,7 +18,7 @@ const Provider = () => ({
label: T.Provider,
resolver: () => STEP_FORM_SCHEMA,
content: useCallback(({ data, setFormData }) => {
const { providers } = useProvision()
const { providers } = useOne()
const provisionTemplate = useWatch({ name: TEMPLATE_ID })
const provisionTemplateSelected = provisionTemplate?.[0] ?? {}

View File

@ -3,7 +3,8 @@ import { Divider, Select, Breadcrumbs, InputLabel, FormControl } from '@material
import ArrowIcon from '@material-ui/icons/ArrowForwardIosRounded'
import Marked from 'marked'
import { useProvision, useListForm } from 'client/hooks'
import { useListForm } from 'client/hooks'
import { useOne } from 'client/features/One'
import { ListCards } from 'client/components/List'
import { ProvisionTemplateCard } from 'client/components/Cards'
import { sanitize, groupBy } from 'client/utils'
@ -22,7 +23,8 @@ const Template = () => ({
label: T.ProvisionTemplate,
resolver: () => STEP_FORM_SCHEMA,
content: useCallback(({ data, setFormData }) => {
const { provisionsTemplates, providers } = useProvision()
const { provisionsTemplates, providers } = useOne()
const templateSelected = data?.[0]
const [provisionSelected, setProvision] = React.useState(

View File

@ -11,8 +11,10 @@ import Steps from 'client/containers/Provisions/Form/Create/Steps'
import formCreateStyles from 'client/containers/Provisions/Form/Create/styles'
import DebugLog from 'client/components/DebugLog'
import { useProvision, useSocket, useFetch } from 'client/hooks'
import { PATH } from 'client/router/provision'
import { useSocket, useFetch } from 'client/hooks'
import { useProviderApi, useProvisionApi } from 'client/features/One'
import { useGeneralApi } from 'client/features/General'
import { PATH } from 'client/apps/provision/routes'
import { set, cloneObject, mapUserInputs } from 'client/utils'
import { Translate } from 'client/components/HOC'
@ -23,9 +25,11 @@ function ProvisionCreateForm () {
const history = useHistory()
const [uuid, setUuid] = useState(undefined)
const { getProvision } = useSocket()
const { getProviders, createProvision } = useProvision()
const { getProvision } = useSocket()
const { getProviders } = useProviderApi()
const { createProvision } = useProvisionApi()
const { enqueueInfo } = useGeneralApi()
const { data, fetchRequest, loading, error } = useFetch(getProviders)
@ -64,7 +68,9 @@ function ProvisionCreateForm () {
?.map(input => ({ ...input, value: `${parseInputs[input?.name]}` }))
}
createProvision({ data: formatData }).then(res => res && setUuid(res))
createProvision(formatData)
.then(res => res && setUuid(res))
.then(() => enqueueInfo('Creating provision'))
}
useEffect(() => { fetchRequest() }, [])

View File

@ -5,8 +5,11 @@ import { Container, Box } from '@material-ui/core'
import EditIcon from '@material-ui/icons/Settings'
import DeleteIcon from '@material-ui/icons/Delete'
import { PATH } from 'client/router/provision'
import { useProvision, useFetch, useSearch } from 'client/hooks'
import { PATH } from 'client/apps/provision/routes'
import { useFetch, useSearch } from 'client/hooks'
import { useProvision, useProvisionApi } from 'client/features/One'
import { useGeneralApi } from 'client/features/General'
import { ListHeader, ListCards } from 'client/components/List'
import AlertError from 'client/components/Alerts/Error'
import { ProvisionCard } from 'client/components/Cards'
@ -17,17 +20,21 @@ import { T } from 'client/constants'
function Provisions () {
const history = useHistory()
const [showDialog, setShowDialog] = useState(false)
const [{ content, ...showDialog } = {}, setShowDialog] = useState()
const handleCloseDialog = () => setShowDialog()
const { enqueueInfo } = useGeneralApi()
const provisions = useProvision()
const {
provisions,
getProvisions,
getProvision,
configureProvision,
deleteProvision
} = useProvision()
} = useProvisionApi()
const { error, fetchRequest, loading, reloading } = useFetch(getProvisions)
const { result, handleChange } = useSearch({
list: provisions,
listOptions: { shouldSort: true, keys: ['ID', 'NAME'] }
@ -35,17 +42,19 @@ function Provisions () {
useEffect(() => { fetchRequest() }, [])
const handleCancel = () => setShowDialog(false)
return (
<Container disableGutters>
<ListHeader
title={T.Provisions}
reloadButtonProps={{
'data-cy': 'refresh-provision-list',
onClick: () => fetchRequest(undefined, { reload: true }),
isSubmitting: Boolean(loading || reloading)
}}
addButtonProps={{ 'data-cy': 'create-provision', onClick: () => history.push(PATH.PROVISIONS.CREATE) }}
addButtonProps={{
'data-cy': 'create-provision',
onClick: () => history.push(PATH.PROVISIONS.CREATE)
}}
searchProps={{ handleChange }}
/>
<Box p={3}>
@ -62,23 +71,36 @@ function Provisions () {
id: ID,
title: NAME,
subheader: `#${ID}`,
content: DialogInfo
content: props => createElement(DialogInfo, {
...props,
displayName: 'DialogDetailProvision'
})
}),
actions: [
{
handleClick: () => configureProvision({ id: ID }),
handleClick: () => configureProvision(ID)
.then(() => enqueueInfo(`Configuring provision - ID: ${ID}`))
.then(() => fetchRequest(undefined, { reload: true })),
icon: <EditIcon />,
cy: 'provision-configure'
},
{
handleClick: () => setShowDialog({
id: ID,
title: `DELETE provision - #${ID} - ${NAME}`,
content: props => createElement(DialogInfo, {
...props,
disableAllActions: true,
displayName: 'DialogDeleteProvision'
}),
title: `DELETE - ${NAME}`,
subheader: `#${ID}`,
handleAccept: () => {
deleteProvision({ id: ID })
setShowDialog(false)
},
content: DialogInfo
handleCloseDialog()
return deleteProvision(ID)
.then(() => enqueueInfo(`Deleting provision - ID: ${ID}`))
.then(() => fetchRequest(undefined, { reload: true }))
}
}),
icon: <DeleteIcon color='error' />,
cy: 'provision-delete'
@ -88,13 +110,13 @@ function Provisions () {
/>
)}
</Box>
{showDialog !== false && (
{content && (
<DialogRequest
withTabs
request={() => getProvision({ id: showDialog.id })}
dialogProps={{ handleCancel, ...showDialog }}
request={() => getProvision(showDialog.id)}
dialogProps={{ handleCancel: handleCloseDialog, ...showDialog }}
>
{props => createElement(showDialog.content, props)}
{props => content(props)}
</DialogRequest>
)}
</Container>

View File

@ -21,9 +21,10 @@ import { useForm, FormProvider } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { SubmitButton } from 'client/components/FormControl'
import SubmitButton from 'client/components/FormControl/SubmitButton'
import { useAuth } from 'client/hooks'
import { useAuth, useAuthApi } from 'client/features/Auth'
import { useUserApi } from 'client/features/One'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
@ -55,8 +56,9 @@ const useStyles = makeStyles(theme => ({
const Settings = () => {
const classes = useStyles()
const { updateUser, settings = {} } = useAuth()
const { user, settings } = useAuth()
const { getAuthUser } = useAuthApi()
const { updateUser } = useUserApi()
const { handleSubmit, setError, reset, formState, ...methods } = useForm({
reValidateMode: 'onSubmit',
@ -79,7 +81,8 @@ const Settings = () => {
.map(([key, value]) => `\n ${String(key).toUpperCase()} = "${value}"`)
.join(',')
return updateUser({ template: `FIREEDGE = [${values}]\n` })
return updateUser({ id: user.ID, template: `FIREEDGE = [${values}]\n` })
.then(getAuthUser)
}
return (

View File

@ -11,7 +11,7 @@ import {
} from '@material-ui/core'
import { SubmitButton } from 'client/components/FormControl'
import { requestData, requestParams } from 'client/utils'
import { RestClient, requestParams } from 'client/utils'
const ResponseForm = ({
handleChangeResponse,
@ -20,13 +20,10 @@ const ResponseForm = ({
const { control, handleSubmit, errors, formState } = useForm()
const onSubmit = dataForm => {
const { url, options } = requestParams(dataForm, {
name,
httpMethod,
params
})
const command = { name, httpMethod, params }
const { url, options: { method, data } } = requestParams(dataForm, command)
requestData(url, options).then(({ id, ...res }) => {
RestClient[method](url, data).then(({ id, ...res }) => {
id === 401 && console.log('ERROR')
id === 200 && handleChangeResponse(JSON.stringify(res, null, '\t'))
})

View File

@ -18,15 +18,13 @@ import * as React from 'react'
import FlowApp from 'client/apps/flow'
import ProvisionApp from 'client/apps/provision'
import { isDevelopment, isBackend } from 'client/utils'
import { _APPS, APPS } from 'client/constants'
const DevelopmentApp = props => {
let appName = ''
if (
process?.env?.NODE_ENV === 'development' &&
typeof window !== 'undefined'
) {
if (isDevelopment() && !isBackend()) {
const parseUrl = window.location.pathname
.split(/\//gi)
.filter(sub => sub?.length > 0)

View File

@ -16,13 +16,12 @@
import * as React from 'react'
import { render } from 'react-dom'
import store from 'client/store'
import { createStore } from 'client/store'
import App from 'client/dev/_app'
render(
<App store={store} />,
document.getElementById('root')
)
const { store } = createStore({ initState: window.REDUX_DATA })
render(<App store={store} />, document.getElementById('root'))
if (process.env.NODE_ENV === 'development' && module.hot) {
module.hot.accept('./_app', () => {

View File

@ -0,0 +1,103 @@
import { createAsyncThunk, createAction } from '@reduxjs/toolkit'
import { T, JWT_NAME, ONEADMIN_ID, FILTER_POOL } from 'client/constants'
import { authService } from 'client/features/Auth/services'
import { userService } from 'client/features/One/user/services'
import { getGroups } from 'client/features/One/group/actions'
import { dismissSnackbar } from 'client/features/General/actions'
import { storage, removeStoreData } from 'client/utils'
const login = createAsyncThunk(
'auth/login',
async ({ remember, ...user }, { rejectWithValue, dispatch }) => {
try {
const response = await authService.login(user)
const { id, token } = response
const isOneAdmin = id === ONEADMIN_ID
if (token) {
storage(JWT_NAME, token, remember)
dispatch(dismissSnackbar({ dismissAll: true }))
}
return {
jwt: token,
user: { ID: id },
isOneAdmin,
isLoginInProgress: !!token && !isOneAdmin
}
} catch (error) {
return rejectWithValue({ error })
}
}
)
const getUser = createAsyncThunk(
'auth/user',
async (_, { dispatch, getState }) => {
try {
const user = await authService.getUser()
await dispatch(getGroups())
const isOneAdmin = user?.ID === ONEADMIN_ID
const userSettings = user?.TEMPLATE?.FIREEDGE ?? {}
const settings = {
...getState().auth?.settings,
...Object.entries(userSettings).reduce((res, [key, value]) =>
({ ...res, [String(key).toLowerCase()]: value })
, {})
}
return { user, settings, isOneAdmin }
} catch (error) {
dispatch(logout(T.SessionExpired))
}
}, {
condition: (_, { getState }) => {
const { isLoading } = getState().auth
return !isLoading
}
}
)
const logout = createAction('logout', errorMessage => {
removeStoreData([JWT_NAME])
return { error: errorMessage }
})
const changeFilter = createAction(
'auth/change-filter',
filterPool => ({ payload: { filterPool, isLoginInProgress: false } })
)
const changeGroup = createAsyncThunk(
'auth/change-group',
async ({ group }, { getState, dispatch, rejectWithValue }) => {
try {
if (group === FILTER_POOL.ALL_RESOURCES) {
dispatch(changeFilter(FILTER_POOL.ALL_RESOURCES))
} else {
const { user } = getState().auth
const data = { id: user?.ID, group }
await userService.changeGroup({ data })
dispatch(changeFilter(FILTER_POOL.PRIMARY_GROUP_RESOURCES))
return {
user: {
...user,
GID: group
}
}
}
} catch (error) {
return rejectWithValue({ error })
}
}
)
export { login, getUser, logout, changeFilter, changeGroup }

View File

@ -0,0 +1,37 @@
import { useCallback } from 'react'
import { useDispatch, useSelector, shallowEqual } from 'react-redux'
import { unwrapResult } from '@reduxjs/toolkit'
import * as actions from 'client/features/Auth/actions'
export const useAuth = () => {
const auth = useSelector(state => state.auth, shallowEqual)
const groups = useSelector(state => state.one.groups, shallowEqual)
const { user, jwt } = auth
const userGroups = [user?.GROUPS?.ID]
.flat()
.map(id => groups.find(({ ID }) => ID === id))
.filter(Boolean)
const isLogged = !!jwt && !!userGroups?.length
return { ...auth, groups: userGroups, isLogged }
}
export const useAuthApi = () => {
const dispatch = useDispatch()
const unwrapDispatch = useCallback(
action => dispatch(action).then(unwrapResult)
, [dispatch]
)
return {
login: user => unwrapDispatch(actions.login(user)),
getAuthUser: () => dispatch(actions.getUser()),
changeGroup: data => unwrapDispatch(actions.changeGroup(data)),
logout: () => dispatch(actions.logout())
}
}

View File

@ -0,0 +1,4 @@
export * from 'client/features/Auth/slice'
export * from 'client/features/Auth/actions'
export * from 'client/features/Auth/hooks'
export * from 'client/features/Auth/services'

View File

@ -0,0 +1,18 @@
import { httpCodes } from 'server/utils/constants'
import { RestClient } from 'client/utils'
export const authService = ({
login: user => RestClient.post('/api/auth', user).then(res => {
if (!res?.id || res?.id !== httpCodes.ok.id) {
if (res?.id === httpCodes.accepted.id) return res
throw res
}
return res?.data
}),
getUser: () => RestClient.get('/api/user/info').then(res => {
if (!res?.id || res?.id !== httpCodes.ok.id) throw res
return res?.data?.USER ?? {}
})
})

View File

@ -0,0 +1,66 @@
import { createSlice } from '@reduxjs/toolkit'
import { login, getUser, logout, changeFilter, changeGroup } from 'client/features/Auth/actions'
import { JWT_NAME, FILTER_POOL, DEFAULT_SCHEME, DEFAULT_LANGUAGE } from 'client/constants'
import { isBackend } from 'client/utils'
const initial = () => ({
jwt: !isBackend()
? window.localStorage.getItem(JWT_NAME) ??
window.sessionStorage.getItem(JWT_NAME) ??
null
: null,
user: null,
error: null,
filterPool: FILTER_POOL.ALL_RESOURCES,
settings: {
scheme: DEFAULT_SCHEME,
lang: DEFAULT_LANGUAGE,
disableAnimations: 'NO'
},
isLoginInProgress: false,
isLoading: false
})
const { actions, reducer } = createSlice({
name: 'auth',
initialState: initial(),
extraReducers: builder => {
builder
.addCase(
logout,
(_, { error }) => ({ ...initial(), error })
)
.addMatcher(
({ type }) => {
return [
changeFilter.type,
login.fulfilled.type,
getUser.fulfilled.type,
changeGroup.fulfilled.type
].includes(type)
},
(state, { payload }) => ({ ...state, ...payload })
)
.addMatcher(
({ type }) => type.startsWith('auth/') && type.endsWith('/pending'),
state => ({ ...state, isLoading: true, error: null })
)
.addMatcher(
({ type }) => type.startsWith('auth/') && type.endsWith('/fulfilled'),
state => ({ ...state, isLoading: false })
)
.addMatcher(
({ type }) => type.startsWith('auth/') && type.endsWith('/rejected'),
(state, { payload }) => ({
...state,
...payload,
isLoginInProgress: false,
isLoading: false,
jwt: null
})
)
}
})
export { actions, reducer }

View File

@ -0,0 +1,10 @@
import { createAction } from '@reduxjs/toolkit'
export const fixMenu = createAction('Fix menu')
export const changeZone = createAction('Change zone')
export const changeLoading = createAction('Change loading')
export const changeTitle = createAction('Change title')
export const enqueueSnackbar = createAction('Enqueue snackbar')
export const dismissSnackbar = createAction('Dismiss snackbar')
export const deleteSnackbar = createAction('Delete snackbar')

View File

@ -0,0 +1,59 @@
import { useDispatch, useSelector } from 'react-redux'
import * as actions from 'client/features/General/actions'
const generateKey = () => new Date().getTime() + Math.random()
export const useGeneral = () => (
useSelector(state => state.general)
)
export const useGeneralApi = () => {
const dispatch = useDispatch()
return {
fixMenu: isFixed => dispatch(actions.fixMenu(isFixed)),
changeLoading: isLoading => dispatch(actions.changeLoading(isLoading)),
changeTitle: title => dispatch(actions.changeTitle(title)),
changeZone: zone => dispatch(actions.changeZone(zone)),
enqueueSnackbar: notification => {
const key = notification.options && notification.options.key
return dispatch(actions.enqueueSnackbar({
key: key || generateKey(),
message: String(notification.message) || '',
options: notification.options || {}
}))
},
// dismiss all if no key has been defined
dismissSnackbar: key => dispatch(
actions.dismissSnackbar({ key, dismissAll: !key })
),
deleteSnackbar: key => dispatch(
actions.deleteSnackbar({ key })
),
enqueueSuccess: message => dispatch(
actions.enqueueSnackbar({
key: generateKey(),
message,
options: { variant: 'success' }
})
),
enqueueError: message => dispatch(
actions.enqueueSnackbar({
key: generateKey(),
message,
options: { variant: 'success' }
})
),
enqueueInfo: message => dispatch(
actions.enqueueSnackbar({
key: generateKey(),
message,
options: { variant: 'success' }
})
)
}
}

View File

@ -0,0 +1,3 @@
export * from 'client/features/General/slice'
export * from 'client/features/General/actions'
export * from 'client/features/General/hooks'

View File

@ -0,0 +1,98 @@
import { createSlice } from '@reduxjs/toolkit'
import * as actions from 'client/features/General/actions'
const initial = {
zone: 0,
title: null,
isLoading: false,
isFixMenu: false,
notifications: []
}
const { reducer } = createSlice({
name: 'general',
initialState: initial,
extraReducers: builder => {
builder
.addCase('logout', ({ title }) => ({ ...initial, title }))
/* UI ACTIONS */
.addCase(actions.fixMenu, (state, { payload }) => {
return { ...state, isFixMenu: !!payload }
})
.addCase(actions.changeLoading, (state, { payload }) => {
return { ...state, isLoading: !!payload }
})
.addCase(actions.changeTitle, (state, { payload }) => {
return { ...state, title: payload }
})
.addCase(actions.changeZone, (state, { payload }) => {
return { ...state, zone: payload }
})
/* NOTIFICATION ACTIONS */
.addCase(actions.enqueueSnackbar, (state, { payload }) => {
const { key, options, message } = payload
return {
...state,
notifications: [
...state.notifications,
{ key, options, message }
]
}
})
.addCase(actions.dismissSnackbar, (state, { payload }) => {
const { key, dismissAll } = payload
return {
...state,
notifications: state.notifications.map(notification =>
dismissAll || notification.key !== key
? { ...notification, dismissed: true }
: { ...notification }
)
}
})
.addCase(actions.deleteSnackbar, (state, { payload }) => {
const { key } = payload
return {
...state,
notifications: state.notifications.filter(
notification => notification.key !== key
)
}
})
/* REQUESTS API MATCHES */
.addMatcher(
({ type }) => type.endsWith('/pending') && !type.includes('auth'),
state => ({ ...state, isLoading: true })
)
.addMatcher(
({ type }) => type.endsWith('/fulfilled') && !type.includes('auth'),
state => ({ ...state, isLoading: false })
)
.addMatcher(
({ type, meta }) =>
!meta?.aborted && type.endsWith('/rejected') && !type.includes('auth'),
(state, { payload }) => ({
...state,
isLoading: false,
notifications: [
...state.notifications,
{
key: new Date().getTime() + Math.random(),
message: payload,
options: { variant: 'error' }
}
]
})
)
}
})
export { reducer }

View File

@ -0,0 +1,10 @@
import { createAction } from 'client/features/One/utils'
import { applicationService } from 'client/features/One/application/services'
export const getApplication = createAction('cluster', applicationService.getApplication)
export const getApplications = createAction(
'application/pool',
applicationService.getApplications,
response => ({ applications: response })
)

View File

@ -0,0 +1,23 @@
import { useCallback } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { unwrapResult } from '@reduxjs/toolkit'
import * as actions from 'client/features/One/application/actions'
export const useApplication = () => (
useSelector(state => state.one.applications)
)
export const useApplicationApi = () => {
const dispatch = useDispatch()
const unwrapDispatch = useCallback(
action => dispatch(action).then(unwrapResult)
, [dispatch]
)
return {
getApplication: id => unwrapDispatch(actions.getApplication({ id })),
getApplications: () => unwrapDispatch(actions.getApplications())
}
}

View File

@ -0,0 +1,19 @@
import { SERVICE } from 'server/routes/api/oneflow/string-routes'
import { httpCodes } from 'server/utils/constants'
import { RestClient } from 'client/utils'
import { poolRequest } from 'client/features/One/utils'
export const applicationService = ({
getApplication: ({ filter, id }) => RestClient
.get(`/api/${SERVICE}/list/${id}`, { filter })
.then(res => {
if (!res?.id || res?.id !== httpCodes.ok.id) throw res
return res?.data?.DOCUMENT ?? {}
}),
getApplications: data => {
const command = { name: `${SERVICE}.list`, params: {} }
return poolRequest(data, command, 'DOCUMENT')
}
})

View File

@ -0,0 +1,28 @@
import { createAction } from 'client/features/One/utils'
import { applicationTemplateService } from 'client/features/One/applicationTemplate/services'
export const getApplicationTemplate = createAction(
'application-template',
applicationTemplateService.getApplicationTemplate
)
export const getApplicationsTemplates = createAction(
'application-template/pool',
applicationTemplateService.getApplicationsTemplates,
response => ({ applicationsTemplates: response })
)
export const createApplicationTemplate = createAction(
'application-template/create',
applicationTemplateService.createApplicationTemplate
)
export const updateApplicationTemplate = createAction(
'application-template/update',
applicationTemplateService.updateApplicationTemplate
)
export const instantiateApplicationTemplate = createAction(
'application-template/instantiate',
applicationTemplateService.instantiateApplicationTemplate
)

View File

@ -0,0 +1,30 @@
import { useCallback } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { unwrapResult } from '@reduxjs/toolkit'
import * as actions from 'client/features/One/applicationTemplate/actions'
export const useApplicationTemplate = () => (
useSelector(state => state.one.applicationsTemplates)
)
export const useApplicationTemplateApi = () => {
const dispatch = useDispatch()
const unwrapDispatch = useCallback(
action => dispatch(action).then(unwrapResult)
, [dispatch]
)
return {
getApplicationTemplate: id => unwrapDispatch(actions.getApplicationTemplate({ id })),
getApplicationsTemplates: () => unwrapDispatch(actions.getApplicationsTemplates()),
createApplicationTemplate: data => unwrapDispatch(actions.createApplicationTemplate({ data })),
updateApplicationTemplate: (id, data) =>
unwrapDispatch(actions.updateApplicationTemplate({ id, data })),
instantiateApplicationTemplate: (id, data) =>
unwrapDispatch(actions.instantiateApplicationTemplate({ id, data }))
}
}

View File

@ -0,0 +1,50 @@
import { SERVICE_TEMPLATE } from 'server/routes/api/oneflow/string-routes'
import { httpCodes } from 'server/utils/constants'
import { RestClient } from 'client/utils'
import { poolRequest } from 'client/features/One/utils'
export const applicationTemplateService = ({
getApplicationTemplate: ({ filter, id }) => RestClient
.get(`/api/${SERVICE_TEMPLATE}/list/${id}`, { filter })
.then(res => {
if (!res?.id || res?.id !== httpCodes.ok.id) throw res
return res?.data?.DOCUMENT ?? {}
}),
getApplicationsTemplates: data => {
const command = { name: `${SERVICE_TEMPLATE}.list`, params: {} }
return poolRequest(data, command, 'DOCUMENT')
},
createApplicationTemplate: ({ data = {} }) => RestClient
.post(`/api/${SERVICE_TEMPLATE}/create`, data)
.then(res => {
if (!res?.id || res?.id !== httpCodes.ok.id) throw res
return res?.data?.DOCUMENT ?? {}
}),
updateApplicationTemplate: ({ id, data = {} }) => RestClient
.put(`/api/${SERVICE_TEMPLATE}/update/${id}`, data)
.then(res => {
if (!res?.id || res?.id !== httpCodes.ok.id) throw res
return res?.data?.DOCUMENT ?? {}
}),
instantiateApplicationTemplate: ({ id, data = {} }) => RestClient
.post(`/api/${SERVICE_TEMPLATE}/action/${id}`, {
data: {
action: {
perform: 'instantiate',
params: { merge_template: data }
}
}
})
.then(res => {
if (!res?.id || res?.id !== httpCodes.ok.id) throw res
return res?.data?.DOCUMENT ?? {}
})
})

View File

@ -0,0 +1,10 @@
import { createAction } from 'client/features/One/utils'
import { clusterService } from 'client/features/One/cluster/services'
export const getCluster = createAction('cluster', clusterService.getCluster)
export const getClusters = createAction(
'cluster/pool',
clusterService.getClusters,
response => ({ clusters: response })
)

View File

@ -0,0 +1,23 @@
import { useCallback } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { unwrapResult } from '@reduxjs/toolkit'
import * as actions from 'client/features/One/cluster/actions'
export const useCluster = () => (
useSelector(state => state.one.clusters)
)
export const useClusterApi = () => {
const dispatch = useDispatch()
const unwrapDispatch = useCallback(
action => dispatch(action).then(unwrapResult)
, [dispatch]
)
return {
getCluster: id => unwrapDispatch(actions.getCluster({ id })),
getClusters: () => unwrapDispatch(actions.getClusters())
}
}

View File

@ -0,0 +1,25 @@
import { Actions, Commands } from 'server/utils/constants/commands/cluster'
import { httpCodes } from 'server/utils/constants'
import { requestParams, RestClient } from 'client/utils'
import { poolRequest } from 'client/features/One/utils'
export const clusterService = ({
getCluster: ({ filter, id }) => {
const name = Actions.CLUSTER_INFO
const { url, options } = requestParams(
{ filter, id },
{ name, ...Commands[name] }
)
return RestClient.get(url, options).then(res => {
if (!res?.id || res?.id !== httpCodes.ok.id) throw res
return res?.data?.CLUSTER ?? {}
})
},
getClusters: data => {
const name = Actions.CLUSTER_POOL_INFO
const command = { name, ...Commands[name] }
return poolRequest(data, command, 'CLUSTER')
}
})

View File

@ -0,0 +1,10 @@
import { createAction } from 'client/features/One/utils'
import { datastoreService } from 'client/features/One/datastore/services'
export const getDatastore = createAction('datastore', datastoreService.getDatastore)
export const getDatastores = createAction(
'datastore',
datastoreService.getDatastores,
response => ({ datastores: response })
)

View File

@ -0,0 +1,23 @@
import { useCallback } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { unwrapResult } from '@reduxjs/toolkit'
import * as actions from 'client/features/One/datastore/actions'
export const useDatastore = () => (
useSelector(state => state.one.datastores)
)
export const useDatastoreApi = () => {
const dispatch = useDispatch()
const unwrapDispatch = useCallback(
action => dispatch(action).then(unwrapResult)
, [dispatch]
)
return {
getDatastore: id => unwrapDispatch(actions.getDatastore({ id })),
getDatastores: () => unwrapDispatch(actions.getDatastores())
}
}

View File

@ -0,0 +1,25 @@
import { Actions, Commands } from 'server/utils/constants/commands/datastore'
import { httpCodes } from 'server/utils/constants'
import { requestParams, RestClient } from 'client/utils'
import { poolRequest } from 'client/features/One/utils'
export const datastoreService = ({
getDatastore: ({ filter, id }) => {
const name = Actions.DATASTORE_INFO
const { url, options } = requestParams(
{ filter, id },
{ name, ...Commands[name] }
)
return RestClient.get(url, options).then(res => {
if (!res?.id || res?.id !== httpCodes.ok.id) throw res
return res?.data?.DATASTORE ?? {}
})
},
getDatastores: data => {
const name = Actions.DATASTORE_POOL_INFO
const command = { name, ...Commands[name] }
return poolRequest(data, command, 'DATASTORE')
}
})

View File

@ -0,0 +1,10 @@
import { createAction } from 'client/features/One/utils'
import { groupService } from 'client/features/One/group/services'
export const getGroup = createAction('group', groupService.getGroup)
export const getGroups = createAction(
'group/pool',
groupService.getGroups,
response => ({ groups: response })
)

View File

@ -0,0 +1,23 @@
import { useCallback } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { unwrapResult } from '@reduxjs/toolkit'
import * as actions from 'client/features/One/group/actions'
export const useGroup = () => (
useSelector(state => state.one.groups)
)
export const useGroupApi = () => {
const dispatch = useDispatch()
const unwrapDispatch = useCallback(
action => dispatch(action).then(unwrapResult)
, [dispatch]
)
return {
getGroup: id => unwrapDispatch(actions.getGroup({ id })),
getGroups: () => unwrapDispatch(actions.getGroups())
}
}

View File

@ -0,0 +1,25 @@
import { Actions, Commands } from 'server/utils/constants/commands/group'
import { httpCodes } from 'server/utils/constants'
import { requestParams, RestClient } from 'client/utils'
import { poolRequest } from 'client/features/One/utils'
export const groupService = ({
getGroup: ({ filter, id }) => {
const name = Actions.GROUP_INFO
const { url, options } = requestParams(
{ filter, id },
{ name, ...Commands[name] }
)
return RestClient.get(url, options).then(res => {
if (!res?.id || res?.id !== httpCodes.ok.id) throw res
return res?.data?.GROUP ?? {}
})
},
getGroups: data => {
const name = Actions.GROUP_POOL_INFO
const command = { name, ...Commands[name] }
return poolRequest(data, command, 'GROUP')
}
})

View File

@ -0,0 +1,20 @@
import { useSelector, shallowEqual } from 'react-redux'
export const useOne = () => (
useSelector(state => state.one, shallowEqual)
)
export * from 'client/features/One/application/hooks'
export * from 'client/features/One/applicationTemplate/hooks'
export * from 'client/features/One/cluster/hooks'
export * from 'client/features/One/datastore/hooks'
export * from 'client/features/One/group/hooks'
export * from 'client/features/One/host/hooks'
export * from 'client/features/One/marketApp/hooks'
export * from 'client/features/One/provider/hooks'
export * from 'client/features/One/provision/hooks'
export * from 'client/features/One/user/hooks'
export * from 'client/features/One/vm/hooks'
export * from 'client/features/One/vmTemplate/hooks'
export * from 'client/features/One/vnetwork/hooks'
export * from 'client/features/One/vnetworkTemplate/hooks'

View File

@ -0,0 +1,10 @@
import { createAction } from 'client/features/One/utils'
import { hostService } from 'client/features/One/host/services'
export const getHost = createAction('host', hostService.getHost)
export const getHosts = createAction(
'host/pool',
hostService.getHosts,
response => ({ hosts: response })
)

View File

@ -0,0 +1,23 @@
import { useCallback } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { unwrapResult } from '@reduxjs/toolkit'
import * as actions from 'client/features/One/host/actions'
export const useHost = () => (
useSelector(state => state.one.hosts)
)
export const useHostApi = () => {
const dispatch = useDispatch()
const unwrapDispatch = useCallback(
action => dispatch(action).then(unwrapResult)
, [dispatch]
)
return {
getHost: id => unwrapDispatch(actions.getHost({ id })),
getHosts: () => unwrapDispatch(actions.getHosts())
}
}

View File

@ -0,0 +1,25 @@
import { Actions, Commands } from 'server/utils/constants/commands/host'
import { httpCodes } from 'server/utils/constants'
import { requestParams, RestClient } from 'client/utils'
import { poolRequest } from 'client/features/One/utils'
export const hostService = ({
getHost: ({ filter, id }) => {
const name = Actions.HOST_INFO
const { url, options } = requestParams(
{ filter, id },
{ name, ...Commands[name] }
)
return RestClient.get(url, options).then(res => {
if (!res?.id || res?.id !== httpCodes.ok.id) throw res
return res?.data?.HOST ?? {}
})
},
getHosts: data => {
const name = Actions.HOST_POOL_INFO
const command = { name, ...Commands[name] }
return poolRequest(data, command, 'HOST')
}
})

View File

@ -0,0 +1,2 @@
export * from 'client/features/One/slice'
export * from 'client/features/One/hooks'

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