feat(lite/pool): dashboard Status card (#6112)
This commit is contained in:
@@ -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'
|
||||
|
||||
145
@xen-orchestra/lite/src/App/Pool/dashboard/ObjectStatus.tsx
Normal file
145
@xen-orchestra/lite/src/App/Pool/dashboard/ObjectStatus.tsx
Normal 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' />
|
||||
|
||||
<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' />
|
||||
|
||||
<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
|
||||
79
@xen-orchestra/lite/src/App/Pool/dashboard/index.tsx
Normal file
79
@xen-orchestra/lite/src/App/Pool/dashboard/index.tsx
Normal 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
|
||||
46
@xen-orchestra/lite/src/App/Pool/index.tsx
Normal file
46
@xen-orchestra/lite/src/App/Pool/index.tsx
Normal 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
|
||||
@@ -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`,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user