feat(lite/pool): dashboard Status card (#6112)

This commit is contained in:
Rajaa.BARHTAOUI
2022-04-21 16:22:49 +02:00
committed by GitHub
parent 9f0f38ef94
commit e5c737cba7
7 changed files with 365 additions and 27 deletions

View File

@@ -1,12 +1,15 @@
import React from 'react'
import styled from 'styled-components'
import { Switch, Route, RouteComponentProps } from 'react-router-dom'
import { withState } from 'reaclette'
import { withRouter } from 'react-router'
import { Switch, Route } from 'react-router-dom'
import Pool from './Pool'
import TabConsole from './TabConsole'
import TreeView from './TreeView'
import { ObjectsByType } from '../libs/xapi'
const Container = styled.div`
display: flex;
overflow: hidden;
@@ -26,15 +29,18 @@ const MainPanel = styled.div`
width: 80%;
`
interface ParentState {}
interface ParentState {
objectsByType: ObjectsByType
pool?: string
}
interface State {
selectedObject?: string
selectedVm?: string
}
interface Props {
location: object
}
// For compatibility with 'withRouter'
interface Props extends RouteComponentProps {}
interface ParentEffects {}
@@ -44,21 +50,28 @@ interface Effects {
interface Computed {}
const selectedNodesToArray = (nodes: Array<string> | string | undefined) =>
nodes === undefined ? undefined : Array.isArray(nodes) ? nodes : [nodes]
const Infrastructure = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
initialState: props => ({
selectedVm: props.location.pathname.split('/')[3],
}),
computed: {
selectedObject: (state, props) =>
props.location.pathname.startsWith('/infrastructure/pool') ? state.pool : state.selectedVm,
},
},
({ state: { selectedVm } }) => (
({ state: { pool, selectedObject } }) => (
<Container>
<LeftPanel>
<TreeView defaultSelectedNodes={selectedVm === undefined ? undefined : [selectedVm]} />
<TreeView defaultSelectedNodes={selectedNodesToArray(selectedObject)} />
</LeftPanel>
<MainPanel>
<Switch>
<Route exact path='/infrastructure'>
Select a VM
<Route exact path={`/infrastructure/pool/${pool}/dashboard`}>
<Pool id={pool} />
</Route>
<Route
path='/infrastructure/vms/:id/console'

View File

@@ -0,0 +1,145 @@
import Box from '@mui/material/Box'
import CircularProgress from '@mui/material/CircularProgress'
import Grid from '@mui/material/Grid'
import React from 'react'
import styled from 'styled-components'
import Typography from '@mui/material/Typography'
import { withState } from 'reaclette'
import Icon from '../../../components/Icon'
import IntlMessage from '../../../components/IntlMessage'
interface ParentState {}
interface State {}
interface Props {
nActive?: number
nTotal?: number
type: 'host' | 'VM'
}
interface ParentEffects {}
interface Effects {}
interface Computed {
nInactive?: number
}
const DEFAULT_CAPTION_STYLE = { textTransform: 'uppercase', mt: 2 }
const TYPOGRAPHY_SX = { mb: 2 }
const ObjectStatusContainer = styled.div`
display: flex;
overflow: hidden;
flex-direction: row;
align-content: space-between;
margin-bottom: 1em;
`
const CircularProgressPanel = styled.div`
margin-left: 2em;
`
const GridPanel = styled.div`
margin-left: 2em;
width: 100%;
height: 100%;
`
// TODO: use CircularProgress component when https://github.com/vatesfr/xen-orchestra/pull/6128 is merged.
// Add a loading page when data is not loaded as it is in the model(figma).
// FIXME: replace the hard-coded colors with the theme colors.
const ObjectStatus = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
computed: {
nInactive: (state, { nTotal = 0, nActive = 0 }) => nTotal - nActive,
},
},
({ state: { nInactive }, nActive = 0, nTotal = 0, type }) => {
if (nTotal === 0) {
return (
<span>
<IntlMessage id={type === 'VM' ? 'noVms' : 'noHosts'} />
</span>
)
}
return (
<ObjectStatusContainer>
<CircularProgressPanel>
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
<CircularProgress
variant='determinate'
value={(nActive * 100) / nTotal}
sx={{ color: '#00BA34' }}
size={100}
/>
<Box
sx={{
top: 0,
left: 0,
bottom: 0,
right: 0,
position: 'absolute',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Typography variant='h5' sx={{ color: '#00BA34' }}>
{Math.round((nActive * 100) / nTotal)}%
</Typography>
</Box>
</Box>
</CircularProgressPanel>
<GridPanel>
<Grid container>
<Grid item xs={12}>
<Typography sx={TYPOGRAPHY_SX} variant='h5' component='div'>
<IntlMessage id={type === 'VM' ? 'vms' : 'hosts'} />
</Typography>
</Grid>
<Grid item xs={10}>
<Icon icon='circle' htmlColor='#00BA34' />
&nbsp;
<Typography variant='body2' component='span'>
<IntlMessage id='active' />
</Typography>
</Grid>
<Grid item xs={2}>
<Typography variant='body2' component='div'>
{nActive}
</Typography>
</Grid>
<Grid item xs={10}>
<Icon icon='circle' htmlColor='#E8E8E8' />
&nbsp;
<Typography variant='body2' component='span'>
<IntlMessage id='inactive' />
</Typography>
</Grid>
<Grid item xs={2}>
<Typography variant='body2' component='div'>
{nInactive}
</Typography>
</Grid>
<Grid item xs={10}>
<Typography variant='caption' component='div' sx={DEFAULT_CAPTION_STYLE}>
<IntlMessage id='total' />
</Typography>
</Grid>
<Grid item xs={2}>
<Typography variant='caption' component='div' sx={DEFAULT_CAPTION_STYLE}>
{nTotal}
</Typography>
</Grid>
</Grid>
</GridPanel>
</ObjectStatusContainer>
)
}
)
export default ObjectStatus

View File

@@ -0,0 +1,79 @@
import Divider from '@mui/material/Divider'
import React from 'react'
import styled from 'styled-components'
import Typography from '@mui/material/Typography'
import { withState } from 'reaclette'
import ObjectStatus from './ObjectStatus'
import IntlMessage from '../../../components/IntlMessage'
import { Host, ObjectsByType, Vm } from '../../../libs/xapi'
interface ParentState {
objectsByType?: ObjectsByType
}
interface State {
hosts?: Map<string, Host>
nRunningHosts?: number
nRunningVms?: number
vms?: Map<string, Vm>
}
interface Props {}
interface ParentEffects {}
interface Effects {}
interface Computed {}
const DEFAULT_STYLE = { m: 2 }
const Container = styled.div`
display: flex;
overflow: hidden;
flex-direction: row;
align-content: space-between;
gap: 1.25em;
background: '#E8E8E8';
`
const Panel = styled.div`
background: #ffffff;
border-radius: 0.5em;
box-shadow: 0px 1px 1px 0px #00000014, 0px 2px 1px 0px #0000000f, 0px 1px 3px 0px #0000001a;
margin: 0.5em;
`
const getHostPowerState = (host: Host) => {
const { $metrics } = host
return $metrics ? ($metrics.live ? 'Running' : 'Halted') : 'Unknown'
}
const Dashboard = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
computed: {
hosts: state => state.objectsByType?.get('host'),
vms: state =>
state.objectsByType
?.get('VM')
?.filter((vm: Vm) => !vm.is_control_domain && !vm.is_a_snapshot && !vm.is_a_template),
nRunningHosts: state => (state.hosts?.filter((host: Host) => getHostPowerState(host) === 'Running')).size,
nRunningVms: state => (state.vms?.filter((vm: Vm) => vm.power_state === 'Running')).size,
},
},
({ state: { hosts, nRunningHosts, nRunningVms, vms } }) => (
<Container>
<Panel>
<Typography variant='h4' component='div' sx={DEFAULT_STYLE}>
<IntlMessage id='status' />
</Typography>
<ObjectStatus nActive={nRunningHosts} nTotal={hosts?.size} type='host' />
<Divider variant='middle' sx={DEFAULT_STYLE} />
<ObjectStatus nActive={nRunningVms} nTotal={vms?.size} type='VM' />
</Panel>
</Container>
)
)
export default Dashboard

View File

@@ -0,0 +1,46 @@
import React from 'react'
import { withState } from 'reaclette'
import Dashboard from './dashboard'
import Icon from '../../components/Icon'
import PanelHeader from '../../components/PanelHeader'
import { ObjectsByType, Pool as PoolType } from '../../libs/xapi'
interface ParentState {
objectsByType: ObjectsByType
}
interface State {}
interface Props {
id: string
}
interface ParentEffects {}
interface Effects {}
interface Computed {
pool?: PoolType
}
// TODO: add tabs when https://github.com/vatesfr/xen-orchestra/pull/6096 is merged.
const Pool = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
computed: {
pool: (state, props) => state.objectsByType?.get('pool')?.get(props.id),
},
},
({ state: { pool } }) => (
<>
<PanelHeader>
<span>
<Icon icon='warehouse' color='primary' /> {pool?.name_label}
</span>
</PanelHeader>
<Dashboard />
</>
)
)
export default Pool

View File

@@ -102,6 +102,7 @@ const TreeView = withState<State, Props, Effects, Computed, ParentState, ParentE
<Icon icon='warehouse' color='primary' /> {pool.name_label}
</span>
),
to: `/infrastructure/pool/${pool.$id}/dashboard`,
})
})

View File

@@ -35,9 +35,11 @@ import PoolTab from './PoolTab'
import Signin from './Signin/index'
import StyleGuide from './StyleGuide/index'
import TabConsole from './TabConsole'
import XapiConnection, { ObjectsByType, Vm } from '../libs/xapi'
import XapiConnection, { ObjectsByType, Pool, Vm } from '../libs/xapi'
const drawerWidth = 240
const redirectPaths = ['/', '/infrastructure']
interface AppBarProps extends MuiAppBarProps {
open?: boolean
@@ -172,21 +174,6 @@ const mdTheme = createTheme({
main: '#ffc107',
},
},
typography: {
fontFamily: 'inter',
h2: {
fontWeight: 500,
fontSize: '2.25em',
fontStyle: 'medium',
lineHeight: '3em',
},
h3: {
fontWeight: 500,
fontSize: '1.5em',
fontStyle: 'medium',
lineHeight: '2em',
},
},
components: {
MuiTab: {
styleOverrides: {
@@ -199,6 +186,63 @@ const mdTheme = createTheme({
},
},
},
typography: {
fontFamily: 'inter',
h1: {
fontWeight: 500,
fontSize: '3em',
fontStyle: 'medium',
lineHeight: '3.75em',
},
h2: {
fontWeight: 500,
fontSize: '2.25em',
fontStyle: 'medium',
},
h3: {
fontWeight: 500,
fontSize: '1.5em',
fontStyle: 'medium',
lineHeight: '2em',
},
h4: {
fontWeight: 500,
fontSize: '1.25em',
fontStyle: 'medium',
lineHeight: '1.75em',
},
h5: {
fontWeight: 500,
fontSize: '1em',
fontStyle: 'medium',
lineHeight: '1.50em',
},
h6: {
fontWeight: 500,
fontSize: '0.8em',
fontStyle: 'medium',
lineHeight: '1.25em',
},
caption: {
// styleName: Caps / Caps 1 - 14 Semi Bold
fontSize: '0.9em',
fontStyle: 'normal',
fontWeight: 600,
lineHeight: '1.25em',
verticalAlign: 'top',
letterSpacing: '0.04em',
textAlign: 'left',
},
body2: {
// styleName: Paragraph / P2 - 16
fontSize: '1em',
fontStyle: 'normal',
fontWeight: 400,
lineHeight: '1.5em',
letterSpacing: '0em',
textAlign: 'left',
},
},
})
const FullPage = styledComponent.div`
@@ -231,6 +275,7 @@ interface Effects {
interface Computed {
objectsFetched: boolean
pool?: Pool
url: string
vms?: Map<string, Vm>
}
@@ -297,6 +342,7 @@ const App = withState<State, Props, Effects, Computed, ParentState, ParentEffect
},
computed: {
objectsFetched: state => state.objectsByType !== undefined,
pool: state => (state.objectsFetched ? state.objectsByType?.get('pool')?.keySeq().first() : undefined),
vms: state =>
state.objectsFetched
? state.objectsByType
@@ -319,8 +365,8 @@ const App = withState<State, Props, Effects, Computed, ParentState, ParentEffect
<>
<Router>
<Switch>
<Route exact path='/'>
<Redirect to='/infrastructure' />
<Route exact path={redirectPaths}>
<Redirect to={`/infrastructure/pool/${state.pool.$id}/dashboard`} />
</Route>
<Route exact path='/vm-list'>
{state.vms !== undefined && (

View File

@@ -1,5 +1,6 @@
{
"about": "About",
"active": "Active",
"availableUpdates": "{nUpdates, number} available update{nUpdates, plural, one {} other {s}}",
"badCredentials": "Bad credentials",
"cancel": "Cancel",
@@ -16,17 +17,21 @@
"errorOccurred": "An error has occurred.",
"gateway": "Gateway",
"halted": "Halted",
"hosts": "Hosts",
"hostUnreachable": "Host unreachable",
"inactive": "Inactive",
"infrastructure": "Infrastructure",
"ip": "IP",
"loading": "Loading…",
"login": "Login",
"name": "Name",
"newFeaturesUnderConstruction": "New features are coming soon!",
"noHosts": "No hosts",
"noData": "No data",
"noImplemented": "Not implemented",
"noManagementPifs": "No management PIFs found",
"none": "None",
"noVms": "No VMs",
"notFound": "Not Found",
"pageNotFound": "This page doesn't exist.",
"xoLiteUnderConstruction": "XO Lite is under construction",
@@ -39,8 +44,11 @@
"rememberMe": "Remember me",
"running": "Running",
"size": "Size",
"status": "Status",
"suspended": "Suspended",
"total": "Total",
"unreachableHost": "Click here to make sure your host ({name}) is reachable. You may have to allow self-signed SSL certificates in your browser.",
"vms": "VMs",
"version": "Version",
"versionValue": "Version {version}",
"vmStartLabel": "Start"