From e5c737cba73b075e0a6f8866d3621d4549b4c1aa Mon Sep 17 00:00:00 2001 From: "Rajaa.BARHTAOUI" Date: Thu, 21 Apr 2022 16:22:49 +0200 Subject: [PATCH] feat(lite/pool): dashboard Status card (#6112) --- .../lite/src/App/Infrastructure.tsx | 31 ++-- .../src/App/Pool/dashboard/ObjectStatus.tsx | 145 ++++++++++++++++++ .../lite/src/App/Pool/dashboard/index.tsx | 79 ++++++++++ @xen-orchestra/lite/src/App/Pool/index.tsx | 46 ++++++ @xen-orchestra/lite/src/App/TreeView.tsx | 1 + @xen-orchestra/lite/src/App/index.tsx | 82 +++++++--- @xen-orchestra/lite/src/lang/en.json | 8 + 7 files changed, 365 insertions(+), 27 deletions(-) create mode 100644 @xen-orchestra/lite/src/App/Pool/dashboard/ObjectStatus.tsx create mode 100644 @xen-orchestra/lite/src/App/Pool/dashboard/index.tsx create mode 100644 @xen-orchestra/lite/src/App/Pool/index.tsx diff --git a/@xen-orchestra/lite/src/App/Infrastructure.tsx b/@xen-orchestra/lite/src/App/Infrastructure.tsx index b8d25c27c..0c8e5722b 100644 --- a/@xen-orchestra/lite/src/App/Infrastructure.tsx +++ b/@xen-orchestra/lite/src/App/Infrastructure.tsx @@ -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 | undefined) => + nodes === undefined ? undefined : Array.isArray(nodes) ? nodes : [nodes] + const Infrastructure = withState( { 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 } }) => ( - + - - Select a VM + + ( + { + computed: { + nInactive: (state, { nTotal = 0, nActive = 0 }) => nTotal - nActive, + }, + }, + ({ state: { nInactive }, nActive = 0, nTotal = 0, type }) => { + if (nTotal === 0) { + return ( + + + + ) + } + + return ( + + + + + + + {Math.round((nActive * 100) / nTotal)}% + + + + + + + + + + + + + +   + + + + + + + {nActive} + + + + +   + + + + + + + {nInactive} + + + + + + + + + + {nTotal} + + + + + + ) + } +) + +export default ObjectStatus diff --git a/@xen-orchestra/lite/src/App/Pool/dashboard/index.tsx b/@xen-orchestra/lite/src/App/Pool/dashboard/index.tsx new file mode 100644 index 000000000..dbdab0d6b --- /dev/null +++ b/@xen-orchestra/lite/src/App/Pool/dashboard/index.tsx @@ -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 + nRunningHosts?: number + nRunningVms?: number + vms?: Map +} + +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( + { + 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 } }) => ( + + + + + + + + + + + ) +) + +export default Dashboard diff --git a/@xen-orchestra/lite/src/App/Pool/index.tsx b/@xen-orchestra/lite/src/App/Pool/index.tsx new file mode 100644 index 000000000..74a60fedd --- /dev/null +++ b/@xen-orchestra/lite/src/App/Pool/index.tsx @@ -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( + { + computed: { + pool: (state, props) => state.objectsByType?.get('pool')?.get(props.id), + }, + }, + ({ state: { pool } }) => ( + <> + + + {pool?.name_label} + + + + + ) +) + +export default Pool diff --git a/@xen-orchestra/lite/src/App/TreeView.tsx b/@xen-orchestra/lite/src/App/TreeView.tsx index dd357879f..b1c0faf14 100644 --- a/@xen-orchestra/lite/src/App/TreeView.tsx +++ b/@xen-orchestra/lite/src/App/TreeView.tsx @@ -102,6 +102,7 @@ const TreeView = withState {pool.name_label} ), + to: `/infrastructure/pool/${pool.$id}/dashboard`, }) }) diff --git a/@xen-orchestra/lite/src/App/index.tsx b/@xen-orchestra/lite/src/App/index.tsx index f327140c7..155a7883d 100644 --- a/@xen-orchestra/lite/src/App/index.tsx +++ b/@xen-orchestra/lite/src/App/index.tsx @@ -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 } @@ -297,6 +342,7 @@ const App = withState 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.vms !== undefined && ( diff --git a/@xen-orchestra/lite/src/lang/en.json b/@xen-orchestra/lite/src/lang/en.json index 7e7e93a11..5c6e4fc3c 100644 --- a/@xen-orchestra/lite/src/lang/en.json +++ b/@xen-orchestra/lite/src/lang/en.json @@ -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"