diff --git a/striker-ui-api/src/lib/request_handlers/anvil/buildAnvilSummary.ts b/striker-ui-api/src/lib/request_handlers/anvil/buildAnvilSummary.ts index a0dfc1d9..99be97a0 100644 --- a/striker-ui-api/src/lib/request_handlers/anvil/buildAnvilSummary.ts +++ b/striker-ui-api/src/lib/request_handlers/anvil/buildAnvilSummary.ts @@ -34,6 +34,34 @@ export const buildAnvilSummary = async ({ hosts: [], }; + let scounts: [scount: number, hostUuid: string][]; + + try { + scounts = await query( + `SELECT + COUNT(a.server_name), + b.host_uuid + FROM servers AS a + JOIN hosts AS b + ON a.server_host_uuid = b.host_uuid + JOIN anvils AS c + ON b.host_uuid IN ( + c.anvil_node1_host_uuid, + c.anvil_node2_host_uuid + ) + WHERE c.anvil_uuid = '${anvilUuid}' + AND a.server_state = 'running' + GROUP BY b.host_uuid, b.host_name + ORDER BY b.host_name;`, + ); + } catch (error) { + stderr(`Failed to get subnodes' server count; CAUSE: ${error}`); + + throw error; + } + + if (!scounts.length) throw new Error(`No host server records found`); + for (const huuid of [n1uuid, n2uuid]) { const { host_uuid: { @@ -43,10 +71,21 @@ export const buildAnvilSummary = async ({ const { hosts: rhosts } = result; + const found = scounts.find((row) => { + if (row.length !== 2) return false; + + const { 1: uuid } = row; + + return uuid === huuid; + }); + + const scount = found ? found[0] : 0; + const hsummary: AnvilDetailHostSummary = { host_name: hname, host_uuid: huuid, maintenance_mode: false, + server_count: scount, state: 'offline', state_message: buildHostStateMessage(), state_percent: 0, diff --git a/striker-ui-api/src/lib/request_handlers/anvil/getAnvilCpu.ts b/striker-ui-api/src/lib/request_handlers/anvil/getAnvilCpu.ts index 4263e8b9..9a0d9133 100644 --- a/striker-ui-api/src/lib/request_handlers/anvil/getAnvilCpu.ts +++ b/striker-ui-api/src/lib/request_handlers/anvil/getAnvilCpu.ts @@ -1,6 +1,7 @@ import { RequestHandler } from 'express'; import { query } from '../../accessModule'; +import { getShortHostName } from '../../disassembleHostName'; import { stderr } from '../../shell'; export const getAnvilCpu: RequestHandler = async ( @@ -11,26 +12,42 @@ export const getAnvilCpu: RequestHandler = async ( params: { anvilUuid }, } = request; - let rCores: null | string; - let rThreads: null | string; + let rCpus: [ + hostUuid: string, + hostName: string, + cpuModel: string, + cpuCores: string, + cpuThreads: string, + cpuMinCores: string, + cpuMinThreads: string, + ][]; try { - [[rCores = '', rThreads = '']] = await query< - [[cpuCores: null | string, cpuThreads: null | string]] - >( + rCpus = await query( `SELECT + b.host_uuid, + b.host_name, + c.scan_hardware_cpu_model, + c.scan_hardware_cpu_cores, + c.scan_hardware_cpu_threads, MIN(c.scan_hardware_cpu_cores) AS cores, MIN(c.scan_hardware_cpu_threads) AS threads FROM anvils AS a JOIN hosts AS b ON b.host_uuid IN ( a.anvil_node1_host_uuid, - a.anvil_node2_host_uuid, - a.anvil_dr1_host_uuid + a.anvil_node2_host_uuid ) JOIN scan_hardware AS c ON b.host_uuid = c.scan_hardware_host_uuid - WHERE a.anvil_uuid = '${anvilUuid}';`, + WHERE a.anvil_uuid = '${anvilUuid}' + GROUP BY + b.host_uuid, + b.host_name, + c.scan_hardware_cpu_model, + c.scan_hardware_cpu_cores, + c.scan_hardware_cpu_threads + ORDER BY b.host_name;`, ); } catch (error) { stderr(`Failed to get anvil ${anvilUuid} cpu info; CAUSE: ${error}`); @@ -38,13 +55,12 @@ export const getAnvilCpu: RequestHandler = async ( return response.status(500).send(); } - const cores = Number.parseInt(rCores); - const threads = Number.parseInt(rThreads); + if (!rCpus.length) return response.status(404).send(); - let rAllocated: null | string; + let rAllocatedRow: [cpuAllocated: string][]; try { - [[rAllocated = '']] = await query<[[cpuAllocated: null | string]]>( + rAllocatedRow = await query( `SELECT SUM( CAST( @@ -64,11 +80,45 @@ export const getAnvilCpu: RequestHandler = async ( return response.status(500).send(); } - const allocated = Number.parseInt(rAllocated); + if (!rAllocatedRow.length) return response.status(404).send(); - response.status(200).send({ - allocated, - cores, - threads, - }); + const { + 0: { 5: rMinCores, 6: rMinThreads }, + } = rCpus; + + const minCores = Number(rMinCores); + const minThreads = Number(rMinThreads); + + const [[rAllocated]] = rAllocatedRow; + + const allocated = Number(rAllocated); + + const rsBody = rCpus.reduce( + (previous, current) => { + const { 0: uuid, 1: name, 2: model, 3: rCores, 4: rThreads } = current; + + const cores = Number(rCores); + const threads = Number(rThreads); + const vendor = model.replace(/^(\w+).*$/, '$1'); + + previous.hosts[uuid] = { + cores, + model, + name: getShortHostName(name), + threads, + uuid, + vendor, + }; + + return previous; + }, + { + allocated, + cores: minCores, + hosts: {}, + threads: minThreads, + }, + ); + + response.status(200).send(rsBody); }; diff --git a/striker-ui-api/src/lib/request_handlers/anvil/getAnvilStore.ts b/striker-ui-api/src/lib/request_handlers/anvil/getAnvilStore.ts index a5e07532..7f2d6480 100644 --- a/striker-ui-api/src/lib/request_handlers/anvil/getAnvilStore.ts +++ b/striker-ui-api/src/lib/request_handlers/anvil/getAnvilStore.ts @@ -27,7 +27,14 @@ export const getAnvilStore: RequestHandler< return response.status(400).send(); } - let rows: [uuid: string, name: string, size: string, free: string][]; + let rows: [ + uuid: string, + name: string, + size: string, + free: string, + totalSize: string, + totalFree: string, + ][]; try { rows = await query( @@ -35,7 +42,9 @@ export const getAnvilStore: RequestHandler< DISTINCT ON (b.storage_group_uuid) storage_group_uuid, b.storage_group_name, d.scan_lvm_vg_size, - d.scan_lvm_vg_free + d.scan_lvm_vg_free, + SUM(d.scan_lvm_vg_size) AS total_vg_size, + SUM(d.scan_lvm_vg_free) AS total_vg_free FROM anvils AS a JOIN storage_groups AS b ON a.anvil_uuid = b.storage_group_anvil_uuid @@ -44,6 +53,11 @@ export const getAnvilStore: RequestHandler< JOIN scan_lvm_vgs AS d ON c.storage_group_member_vg_uuid = d.scan_lvm_vg_internal_uuid WHERE a.anvil_uuid = '${anUuid}' + GROUP BY + b.storage_group_uuid, + b.storage_group_name, + d.scan_lvm_vg_size, + d.scan_lvm_vg_free ORDER BY b.storage_group_uuid, d.scan_lvm_vg_free;`, ); } catch (error) { @@ -52,6 +66,12 @@ export const getAnvilStore: RequestHandler< return response.status(500).send(); } + if (!rows.length) return response.status(404).send(); + + const { + 0: { 4: totalSize, 5: totalFree }, + } = rows; + const rsbody: AnvilDetailStoreSummary = { storage_groups: rows.map( ([sgUuid, sgName, sgSize, sgFree]) => ({ @@ -61,6 +81,8 @@ export const getAnvilStore: RequestHandler< storage_group_uuid: sgUuid, }), ), + total_free: totalFree, + total_size: totalSize, }; return response.json(rsbody); diff --git a/striker-ui-api/src/types/ApiAn.d.ts b/striker-ui-api/src/types/ApiAn.d.ts index 2885af63..41948e9e 100644 --- a/striker-ui-api/src/types/ApiAn.d.ts +++ b/striker-ui-api/src/types/ApiAn.d.ts @@ -1,3 +1,23 @@ +type AnvilDetailCpuHost = { + cores: number; + model: string; + name: string; + threads: number; + uuid: string; + vendor: string; +}; + +type AnvilDetailCpuHostList = { + [hostUuid: string]: AnvilDetailCpuHost; +}; + +type AnvilDetailCpuSummary = { + allocated: number; + cores: number; + hosts: AnvilDetailCpuHostList; + threads: number; +}; + type AnvilDetailFileForProvisionServer = { fileUUID: string; fileName: string; @@ -66,6 +86,7 @@ type AnvilDetailHostSummary = { host_name: string; host_uuid: string; maintenance_mode: boolean; + server_count: number; state: string; state_message: string; state_percent: number; @@ -109,6 +130,8 @@ type AnvilDetailParamsDictionary = { type AnvilDetailStoreSummary = { storage_groups: AnvilDetailStore[]; + total_free: string; + total_size: string; }; type AnvilOverview = { diff --git a/striker-ui/components/Anvils/AnvilSummary.tsx b/striker-ui/components/Anvils/AnvilSummary.tsx new file mode 100644 index 00000000..002ba2f6 --- /dev/null +++ b/striker-ui/components/Anvils/AnvilSummary.tsx @@ -0,0 +1,312 @@ +import { Grid, gridClasses } from '@mui/material'; +import { dSizeStr } from 'format-data-size'; +import { FC, ReactNode, useMemo } from 'react'; + +import { BLUE, GREY, PURPLE, RED } from '../../lib/consts/DEFAULT_THEME'; + +import { + toAnvilDetail, + toAnvilMemoryCalcable, + toAnvilSharedStorageOverview, +} from '../../lib/api_converters'; +import Divider from '../Divider'; +import FlexBox from '../FlexBox'; +import Spinner from '../Spinner'; +import StackBar from '../Bars/StackBar'; +import { BodyText, InlineMonoText, MonoText } from '../Text'; +import useFetch from '../../hooks/useFetch'; + +const N_100 = BigInt(100); + +const MAP_TO_ANVIL_STATE_COLOUR = { + degraded: RED, + not_ready: PURPLE, + optimal: BLUE, +}; + +const MAP_TO_HOST_STATE_COLOUR: Record = { + offline: PURPLE, + online: BLUE, +}; + +const AnvilSummary: FC = (props) => { + const { anvilUuid } = props; + + const { data: rAnvil, loading: loadingAnvil } = useFetch( + `/anvil/${anvilUuid}`, + ); + + const anvil = useMemo( + () => rAnvil && toAnvilDetail(rAnvil), + [rAnvil], + ); + + const { data: cpu, loading: loadingCpu } = useFetch( + `/anvil/${anvilUuid}/cpu`, + ); + + const cpuSubnodes = useMemo( + () => cpu && Object.values(cpu.hosts), + [cpu], + ); + + const { data: rMemory, loading: loadingMemory } = useFetch( + `/anvil/${anvilUuid}/memory`, + ); + + const memory = useMemo( + () => rMemory && toAnvilMemoryCalcable(rMemory), + [rMemory], + ); + + const { data: rStorages, loading: loadingStorages } = + useFetch(`/anvil/${anvilUuid}/store`); + + const storages = useMemo( + () => rStorages && toAnvilSharedStorageOverview(rStorages), + [rStorages], + ); + + const loading = useMemo( + () => + [loadingAnvil, loadingCpu, loadingMemory, loadingStorages].some( + (cond) => cond, + ), + [loadingAnvil, loadingCpu, loadingMemory, loadingStorages], + ); + + const anvilSummary = useMemo( + () => + anvil && ( + + {anvil.state} + + ), + [anvil], + ); + + const hostsSummary = useMemo( + () => + anvil && ( + .${gridClasses.item}:nth-child(-n + 4)`]: { + marginBottom: '-.6em', + }, + }} + > + {Object.values(anvil.hosts).map((host) => { + const { name, serverCount, state, stateProgress, uuid } = host; + + const stateColour: string = MAP_TO_HOST_STATE_COLOUR[state] ?? GREY; + + let stateValue: string = state; + let servers: ReactNode; + + if (['offline', 'online'].includes(state)) { + servers = {serverCount}; + } else { + stateValue = `${stateProgress}%`; + } + + return [ + + + {name} + + , + + + {stateValue} + + , + + + , + + {servers && Servers} + , + + {servers} + , + ]; + })} + + ), + [anvil], + ); + + const cpuSummary = useMemo( + () => + cpu && + cpuSubnodes && ( + + + + Vendor{' '} + + {cpuSubnodes[0].vendor} + + + + + .${gridClasses.item}:nth-child(-n + 2)`]: { + marginBottom: '-.6em', + }, + }} + > + + Cores + + + {cpu.cores} + + + Threads + + + {cpu.threads} + + + + ), + [cpu, cpuSubnodes], + ); + + const memorySummary = useMemo( + () => + memory && ( + + + + Free + + {dSizeStr(memory.total - (memory.reserved + memory.allocated), { + toUnit: 'ibyte', + })} + + / + + {dSizeStr(memory.total, { toUnit: 'ibyte' })} + + + + + + ), + [memory], + ); + + const storeSummary = useMemo( + () => + storages && ( + + + + Total free + + {dSizeStr(storages.totalFree, { toUnit: 'ibyte' })} + + / + + {dSizeStr(storages.totalSize, { toUnit: 'ibyte' })} + + + + + + ), + [storages], + ); + + return loading ? ( + + ) : ( + .${gridClasses.item}:nth-child(odd)`]: { + alignItems: 'center', + display: 'flex', + height: '2.2em', + }, + }} + > + + Node + + + {anvilSummary} + + + Subnodes + + + {hostsSummary} + + + CPU + + + {cpuSummary} + + + Memory + + + {memorySummary} + + + Storage + + + {storeSummary} + + + ); +}; + +export default AnvilSummary; diff --git a/striker-ui/components/Anvils/AnvilSummaryList.tsx b/striker-ui/components/Anvils/AnvilSummaryList.tsx new file mode 100644 index 00000000..e22a4be3 --- /dev/null +++ b/striker-ui/components/Anvils/AnvilSummaryList.tsx @@ -0,0 +1,84 @@ +import { gridClasses } from '@mui/material'; +import { FC, ReactNode, useMemo } from 'react'; + +import AnvilSummary from './AnvilSummary'; +import { toAnvilOverviewList } from '../../lib/api_converters'; +import Grid from '../Grid'; +import { + InnerPanel, + InnerPanelBody, + InnerPanelHeader, + Panel, + PanelHeader, +} from '../Panels'; +import Spinner from '../Spinner'; +import { BodyText, HeaderText } from '../Text'; +import useFetch from '../../hooks/useFetch'; + +const AnvilSummaryList: FC = () => { + const { data: rawAnvils, loading: loadingAnvils } = + useFetch('/anvil', { refreshInterval: 5000 }); + + const anvils = useMemo( + () => rawAnvils && toAnvilOverviewList(rawAnvils), + [rawAnvils], + ); + + const grid = useMemo( + () => + anvils && ( + ( + (previous, current) => { + const { description, name, uuid } = current; + + const key = `anvil-${uuid}`; + + previous[key] = { + children: ( + + + + {name}: {description} + + + + + + + ), + }; + + return previous; + }, + {}, + )} + spacing="1em" + sx={{ + alignContent: 'stretch', + + [`& > .${gridClasses.item}`]: { + minWidth: '20em', + }, + }} + /> + ), + [anvils], + ); + + return ( + + + Nodes + + {loadingAnvils ? : grid} + + ); +}; + +export default AnvilSummaryList; diff --git a/striker-ui/components/Bars/AllocationBar.tsx b/striker-ui/components/Bars/AllocationBar.tsx index 90b3df0f..ebbe6292 100644 --- a/striker-ui/components/Bars/AllocationBar.tsx +++ b/striker-ui/components/Bars/AllocationBar.tsx @@ -1,13 +1,9 @@ -import { LinearProgress } from '@mui/material'; -import { styled } from '@mui/material/styles'; +import { styled } from '@mui/material'; + +import { PURPLE, RED, BLUE } from '../../lib/consts/DEFAULT_THEME'; -import { - PURPLE, - RED, - BLUE, - BORDER_RADIUS, -} from '../../lib/consts/DEFAULT_THEME'; import BorderLinearProgress from './BorderLinearProgress'; +import Underline from './Underline'; const PREFIX = 'AllocationBar'; @@ -15,7 +11,6 @@ const classes = { barOk: `${PREFIX}-barOk`, barWarning: `${PREFIX}-barWarning`, barAlert: `${PREFIX}-barAlert`, - underline: `${PREFIX}-underline`, }; const StyledDiv = styled('div')(() => ({ @@ -30,10 +25,6 @@ const StyledDiv = styled('div')(() => ({ [`& .${classes.barAlert}`]: { backgroundColor: RED, }, - - [`& .${classes.underline}`]: { - borderRadius: BORDER_RADIUS, - }, })); const breakpointWarning = 70; @@ -54,11 +45,7 @@ const AllocationBar = ({ allocated }: { allocated: number }): JSX.Element => ( variant="determinate" value={allocated} /> - + ); diff --git a/striker-ui/components/Bars/BorderLinearProgress.tsx b/striker-ui/components/Bars/BorderLinearProgress.tsx index 50c1de44..57509a31 100644 --- a/striker-ui/components/Bars/BorderLinearProgress.tsx +++ b/striker-ui/components/Bars/BorderLinearProgress.tsx @@ -1,14 +1,15 @@ -import { LinearProgress } from '@mui/material'; -import { styled } from '@mui/material/styles'; -import { - PANEL_BACKGROUND, - BORDER_RADIUS, -} from '../../lib/consts/DEFAULT_THEME'; +import { LinearProgress, linearProgressClasses, styled } from '@mui/material'; + +import { BORDER_RADIUS } from '../../lib/consts/DEFAULT_THEME'; const BorderLinearProgress = styled(LinearProgress)({ - height: '1em', + backgroundColor: 'transparent', borderRadius: BORDER_RADIUS, - backgroundColor: PANEL_BACKGROUND, + height: '1em', + + [`& .${linearProgressClasses.bar}`]: { + borderRadius: BORDER_RADIUS, + }, }); export default BorderLinearProgress; diff --git a/striker-ui/components/Bars/ProgressBar.tsx b/striker-ui/components/Bars/ProgressBar.tsx index 9f0280ad..d122acf2 100644 --- a/striker-ui/components/Bars/ProgressBar.tsx +++ b/striker-ui/components/Bars/ProgressBar.tsx @@ -1,15 +1,15 @@ -import { LinearProgress } from '@mui/material'; -import { styled } from '@mui/material/styles'; +import { styled } from '@mui/material'; + +import { PURPLE, BLUE } from '../../lib/consts/DEFAULT_THEME'; -import { PURPLE, BLUE, BORDER_RADIUS } from '../../lib/consts/DEFAULT_THEME'; import BorderLinearProgress from './BorderLinearProgress'; +import Underline from './Underline'; const PREFIX = 'ProgressBar'; const classes = { barOk: `${PREFIX}-barOk`, barInProgress: `${PREFIX}-barInProgress`, - underline: `${PREFIX}-underline`, }; const StyledDiv = styled('div')(() => ({ @@ -20,10 +20,6 @@ const StyledDiv = styled('div')(() => ({ [`& .${classes.barInProgress}`]: { backgroundColor: PURPLE, }, - - [`& .${classes.underline}`]: { - borderRadius: BORDER_RADIUS, - }, })); const completed = 100; @@ -44,11 +40,7 @@ const ProgressBar = ({ variant="determinate" value={progressPercentage} /> - + ); diff --git a/striker-ui/components/Bars/StackBar.tsx b/striker-ui/components/Bars/StackBar.tsx new file mode 100644 index 00000000..b6e4e38e --- /dev/null +++ b/striker-ui/components/Bars/StackBar.tsx @@ -0,0 +1,93 @@ +import { Box, linearProgressClasses, styled } from '@mui/material'; +import { FC, ReactElement, createElement, useMemo } from 'react'; + +import { GREY } from '../../lib/consts/DEFAULT_THEME'; + +import RoundedLinearProgress from './BorderLinearProgress'; +import Underline from './Underline'; + +const ThinRoundedLinearProgress = styled(RoundedLinearProgress)({ + height: '.4em', +}); + +const ThinUnderline = styled(Underline)({ + height: '.2em', +}); + +const StackBar: FC = (props) => { + const { barProps = {}, thin, underlineProps, value } = props; + + const { sx: barSx, ...restBarProps } = barProps; + + const values = useMemo>( + () => ('value' in value ? { default: value as StackBarValue } : value), + [value], + ); + + const entries = useMemo<[string, StackBarValue][]>( + () => Object.entries(values).reverse(), + [values], + ); + + const creatableBar = useMemo( + () => (thin ? ThinRoundedLinearProgress : RoundedLinearProgress), + [thin], + ); + + const creatableUnderline = useMemo( + () => (thin ? ThinUnderline : Underline), + [thin], + ); + + const bars = useMemo( + () => + entries.map( + ([id, { colour = GREY, value: val }], index) => { + const backgroundColor = + typeof colour === 'string' + ? colour + : Object.entries(colour).findLast( + ([mark]) => val >= Number(mark), + )?.[1] ?? GREY; + + let position: 'absolute' | 'relative' = 'relative'; + let top: 0 | undefined; + let width: string | undefined; + + if (index) { + position = 'absolute'; + top = 0; + width = '100%'; + } + + return createElement(creatableBar, { + key: `stack-bar-${id}`, + sx: { + position, + top, + width, + + [`& .${linearProgressClasses.bar}`]: { + backgroundColor, + }, + + ...barSx, + }, + variant: 'determinate', + value: val, + ...restBarProps, + }); + }, + ), + [barSx, entries, creatableBar, restBarProps], + ); + + return ( + + {bars} + {createElement(creatableUnderline, underlineProps)} + + ); +}; + +export default StackBar; diff --git a/striker-ui/components/Bars/Underline.tsx b/striker-ui/components/Bars/Underline.tsx new file mode 100644 index 00000000..b2553a2a --- /dev/null +++ b/striker-ui/components/Bars/Underline.tsx @@ -0,0 +1,13 @@ +import { Box, styled } from '@mui/material'; + +import { BORDER_RADIUS, DISABLED } from '../../lib/consts/DEFAULT_THEME'; + +const Underline = styled(Box)({ + backgroundColor: DISABLED, + borderRadius: BORDER_RADIUS, + display: 'block', + height: '4px', + position: 'relative', +}); + +export default Underline; diff --git a/striker-ui/components/Bars/index.tsx b/striker-ui/components/Bars/index.tsx index 0863c898..d321b41e 100644 --- a/striker-ui/components/Bars/index.tsx +++ b/striker-ui/components/Bars/index.tsx @@ -1,4 +1,5 @@ import AllocationBar from './AllocationBar'; import ProgressBar from './ProgressBar'; +import StackBar from './StackBar'; -export { AllocationBar, ProgressBar }; +export { AllocationBar, ProgressBar, StackBar }; diff --git a/striker-ui/components/Files/ManageFilePanel.tsx b/striker-ui/components/Files/ManageFilePanel.tsx index b5c78483..663fa12f 100644 --- a/striker-ui/components/Files/ManageFilePanel.tsx +++ b/striker-ui/components/Files/ManageFilePanel.tsx @@ -6,6 +6,7 @@ import { UPLOAD_FILE_TYPES } from '../../lib/consts/UPLOAD_FILE_TYPES'; import AddFileForm from './AddFileForm'; import api from '../../lib/api'; +import { toAnvilOverviewList } from '../../lib/api_converters'; import ConfirmDialog from '../ConfirmDialog'; import { DialogWithHeader } from '../Dialog'; import Divider from '../Divider'; @@ -23,41 +24,6 @@ import useConfirmDialogProps from '../../hooks/useConfirmDialogProps'; import useFetch from '../../hooks/useFetch'; import useProtectedState from '../../hooks/useProtectedState'; -const toAnvilOverviewHostList = ( - data: APIAnvilOverviewArray[number]['hosts'], -) => - data.reduce( - (previous, { hostName: name, hostType: type, hostUUID: uuid }) => { - previous[uuid] = { name, type, uuid }; - - return previous; - }, - {}, - ); - -const toAnvilOverviewList = (data: APIAnvilOverviewArray) => - data.reduce( - ( - previous, - { - anvilDescription: description, - anvilName: name, - anvilUUID: uuid, - hosts, - }, - ) => { - previous[uuid] = { - description, - hosts: toAnvilOverviewHostList(hosts), - name, - uuid, - }; - - return previous; - }, - {}, - ); - const toFileOverviewList = (rows: string[][]) => rows.reduce((previous, row) => { const [uuid, name, size, type, checksum] = row; diff --git a/striker-ui/components/Memory.tsx b/striker-ui/components/Memory.tsx index 1a56a8de..d94befad 100644 --- a/striker-ui/components/Memory.tsx +++ b/striker-ui/components/Memory.tsx @@ -35,14 +35,18 @@ const Memory = (): JSX.Element => { - + diff --git a/striker-ui/lib/api_converters/index.ts b/striker-ui/lib/api_converters/index.ts new file mode 100644 index 00000000..73268108 --- /dev/null +++ b/striker-ui/lib/api_converters/index.ts @@ -0,0 +1,13 @@ +import toAnvilDetail from './toAnvilDetail'; +import toAnvilMemoryCalcable from './toAnvilMemoryCalcable'; +import toAnvilOverviewHostList from './toAnvilOverviewHostList'; +import toAnvilOverviewList from './toAnvilOverviewList'; +import toAnvilSharedStorageOverview from './toAnvilSharedStorageOverview'; + +export { + toAnvilDetail, + toAnvilMemoryCalcable, + toAnvilOverviewHostList, + toAnvilOverviewList, + toAnvilSharedStorageOverview, +}; diff --git a/striker-ui/lib/api_converters/toAnvilDetail.ts b/striker-ui/lib/api_converters/toAnvilDetail.ts new file mode 100644 index 00000000..69a26f0c --- /dev/null +++ b/striker-ui/lib/api_converters/toAnvilDetail.ts @@ -0,0 +1,39 @@ +const toAnvilDetail = (data: AnvilListItem): APIAnvilDetail => { + const { + anvil_name: anvilName, + anvil_state: anvilState, + anvil_uuid: anvilUuid, + hosts: rHosts, + } = data; + + const hosts = rHosts.reduce((previous, current) => { + const { + host_name: hostName, + host_uuid: hostUuid, + maintenance_mode: maintenance, + server_count: serverCount, + state, + state_percent: stateProgress, + } = current; + + previous[hostUuid] = { + name: hostName, + maintenance, + serverCount, + state, + stateProgress, + uuid: hostUuid, + }; + + return previous; + }, {}); + + return { + hosts, + name: anvilName, + state: anvilState, + uuid: anvilUuid, + }; +}; + +export default toAnvilDetail; diff --git a/striker-ui/lib/api_converters/toAnvilMemoryCalcable.ts b/striker-ui/lib/api_converters/toAnvilMemoryCalcable.ts new file mode 100644 index 00000000..f8fc37ef --- /dev/null +++ b/striker-ui/lib/api_converters/toAnvilMemoryCalcable.ts @@ -0,0 +1,15 @@ +const toAnvilMemoryCalcable = (data: AnvilMemory): AnvilMemoryCalcable => { + const { allocated: rAllocated, reserved: rReserved, total: rTotal } = data; + + const allocated = BigInt(rAllocated); + const reserved = BigInt(rReserved); + const total = BigInt(rTotal); + + return { + allocated, + reserved, + total, + }; +}; + +export default toAnvilMemoryCalcable; diff --git a/striker-ui/lib/api_converters/toAnvilOverviewHostList.ts b/striker-ui/lib/api_converters/toAnvilOverviewHostList.ts new file mode 100644 index 00000000..b2a79d02 --- /dev/null +++ b/striker-ui/lib/api_converters/toAnvilOverviewHostList.ts @@ -0,0 +1,13 @@ +const toAnvilOverviewHostList = ( + data: APIAnvilOverviewArray[number]['hosts'], +): APIAnvilOverview['hosts'] => + data.reduce( + (previous, { hostName: name, hostType: type, hostUUID: uuid }) => { + previous[uuid] = { name, type, uuid }; + + return previous; + }, + {}, + ); + +export default toAnvilOverviewHostList; diff --git a/striker-ui/lib/api_converters/toAnvilOverviewList.ts b/striker-ui/lib/api_converters/toAnvilOverviewList.ts new file mode 100644 index 00000000..576ce873 --- /dev/null +++ b/striker-ui/lib/api_converters/toAnvilOverviewList.ts @@ -0,0 +1,28 @@ +import toAnvilOverviewHostList from './toAnvilOverviewHostList'; + +const toAnvilOverviewList = ( + data: APIAnvilOverviewArray, +): APIAnvilOverviewList => + data.reduce( + ( + previous, + { + anvilDescription: description, + anvilName: name, + anvilUUID: uuid, + hosts, + }, + ) => { + previous[uuid] = { + description, + hosts: toAnvilOverviewHostList(hosts), + name, + uuid, + }; + + return previous; + }, + {}, + ); + +export default toAnvilOverviewList; diff --git a/striker-ui/lib/api_converters/toAnvilSharedStorageOverview.ts b/striker-ui/lib/api_converters/toAnvilSharedStorageOverview.ts new file mode 100644 index 00000000..74e2e099 --- /dev/null +++ b/striker-ui/lib/api_converters/toAnvilSharedStorageOverview.ts @@ -0,0 +1,29 @@ +const toAnvilSharedStorageOverview = ( + data: AnvilSharedStorage, +): APIAnvilSharedStorageOverview => { + const { storage_groups, total_free, total_size } = data; + + const totalFree = BigInt(total_free); + const totalSize = BigInt(total_size); + + return storage_groups.reduce( + (previous, current) => { + const { + storage_group_free: rFree, + storage_group_name: name, + storage_group_total: rSize, + storage_group_uuid: uuid, + } = current; + + const free = BigInt(rFree); + const size = BigInt(rSize); + + previous.storageGroups[uuid] = { free, name, size, uuid }; + + return previous; + }, + { storageGroups: {}, totalFree, totalSize }, + ); +}; + +export default toAnvilSharedStorageOverview; diff --git a/striker-ui/pages/index.tsx b/striker-ui/pages/index.tsx index af64bb03..b4d7986b 100644 --- a/striker-ui/pages/index.tsx +++ b/striker-ui/pages/index.tsx @@ -1,12 +1,13 @@ +import { Add as AddIcon } from '@mui/icons-material'; +import { Box, Divider, Grid } from '@mui/material'; import Head from 'next/head'; import { NextRouter, useRouter } from 'next/router'; import { FC, useEffect, useRef, useState } from 'react'; -import { Box, Divider } from '@mui/material'; -import { Add as AddIcon } from '@mui/icons-material'; import API_BASE_URL from '../lib/consts/API_BASE_URL'; import { DIVIDER } from '../lib/consts/DEFAULT_THEME'; +import AnvilSummaryList from '../components/Anvils/AnvilSummaryList'; import { Preview } from '../components/Display'; import fetchJSON from '../lib/fetchers/fetchJSON'; import Header from '../components/Header'; @@ -17,6 +18,7 @@ import { Panel, PanelHeader } from '../components/Panels'; import periodicFetch from '../lib/fetchers/periodicFetch'; import ProvisionServerDialog from '../components/ProvisionServerDialog'; import Spinner from '../components/Spinner'; +import { HeaderText } from '../components/Text'; import { last } from '../lib/time'; type ServerListItem = ServerOverviewMetadata & { @@ -30,20 +32,11 @@ const createServerPreviewContainer = ( servers: ServerListItem[], router: NextRouter, ) => ( - *': { - width: { xs: '20em', md: '24em' }, - }, - - '& > :not(:last-child)': { - marginRight: '2em', - }, - }} + {servers.map( ({ @@ -57,43 +50,57 @@ const createServerPreviewContainer = ( serverUUID, timestamp, }) => ( - - {serverName} - , - - {anvilName} - , - ]} - isExternalLoading={loading} - isExternalPreviewStale={isScreenshotStale} - isFetchPreview={false} - isShowControls={false} - isUseInnerPanel - key={`server-preview-${serverUUID}`} - onClickPreview={() => { - router.push( - `/server?uuid=${serverUUID}&server_name=${serverName}&server_state=${serverState}&vnc=1`, - ); + div': { + height: '100%', + marginBottom: 0, + marginTop: 0, + }, }} - serverState={serverState} - serverUUID={serverUUID} - /> + xs={1} + > + + {serverName} + , + + {anvilName} + , + ]} + isExternalLoading={loading} + isExternalPreviewStale={isScreenshotStale} + isFetchPreview={false} + isShowControls={false} + isUseInnerPanel + onClickPreview={() => { + router.push( + `/server?uuid=${serverUUID}&server_name=${serverName}&server_state=${serverState}&vnc=1`, + ); + }} + serverState={serverState} + serverUUID={serverUUID} + /> + ), )} - + ); const filterServers = (allServers: ServerListItem[], searchTerm: string) => @@ -222,19 +229,20 @@ const Dashboard: FC = () => { ) : ( <> - + + Servers + setIsOpenProvisionServerDialog(true)}> + + { setInputSearchTerm(value); updateServerList(allServers, value); }} - sx={{ marginRight: '.6em' }} + sx={{ minWidth: '16em' }} value={inputSearchTerm} /> - setIsOpenProvisionServerDialog(true)}> - - {createServerPreviewContainer(includeServers, router)} {includeServers.length > 0 && ( @@ -244,6 +252,7 @@ const Dashboard: FC = () => { )} + { diff --git a/striker-ui/types/APIAnvil.d.ts b/striker-ui/types/APIAnvil.d.ts index 216b035d..be4a7698 100644 --- a/striker-ui/types/APIAnvil.d.ts +++ b/striker-ui/types/APIAnvil.d.ts @@ -1,6 +1,16 @@ type AnvilCPU = { allocated: number; cores: number; + hosts: { + [hostUuid: string]: { + cores: number; + model: string; + name: string; + threads: number; + uuid: string; + vendor: string; + }; + }; threads: number; }; @@ -10,6 +20,12 @@ type AnvilMemory = { total: string; }; +type AnvilMemoryCalcable = { + allocated: bigint; + reserved: bigint; + total: bigint; +}; + type AnvilNetworkBondLink = { link_name: string; link_uuid: string; @@ -62,15 +78,18 @@ type AnvilSharedStorageGroup = { type AnvilSharedStorage = { storage_groups: AnvilSharedStorageGroup[]; + total_size: string; + total_free: string; }; type AnvilStatusHost = { - state: 'offline' | 'booted' | 'crmd' | 'in_ccm' | 'online'; - host_uuid: string; host_name: string; - state_percent: number; + host_uuid: string; + maintenance_mode: boolean; + server_count: number; + state: 'offline' | 'booted' | 'crmd' | 'in_ccm' | 'online'; state_message: string; - removable: boolean; + state_percent: number; }; type AnvilStatus = { @@ -111,6 +130,37 @@ type APIAnvilOverview = { uuid: string; }; +type APIAnvilDetail = { + hosts: { + [uuid: string]: { + maintenance: boolean; + name: string; + serverCount: number; + state: AnvilStatusHost['state']; + stateProgress: number; + uuid: string; + }; + }; + name: string; + state: AnvilStatus['anvil_state']; + uuid: string; +}; + type APIAnvilOverviewList = { [uuid: string]: APIAnvilOverview; }; + +type APIAnvilStorageGroupCalcable = { + free: bigint; + name: string; + size: bigint; + uuid: string; +}; + +type APIAnvilSharedStorageOverview = { + storageGroups: { + [uuid: string]: APIAnvilStorageGroupCalcable; + }; + totalFree: bigint; + totalSize: bigint; +}; diff --git a/striker-ui/types/AnvilSummary.d.ts b/striker-ui/types/AnvilSummary.d.ts new file mode 100644 index 00000000..2a00698d --- /dev/null +++ b/striker-ui/types/AnvilSummary.d.ts @@ -0,0 +1,3 @@ +type AnvilSummaryProps = { + anvilUuid: string; +}; diff --git a/striker-ui/types/StackBar.d.ts b/striker-ui/types/StackBar.d.ts new file mode 100644 index 00000000..468ac20e --- /dev/null +++ b/striker-ui/types/StackBar.d.ts @@ -0,0 +1,14 @@ +type StackBarValue = { + colour?: string | Record; + value: number; +}; + +type StackBarOptionalProps = { + barProps?: import('@mui/material').LinearProgressProps; + thin?: boolean; + underlineProps?: import('@mui/material').BoxProps; +}; + +type StackBarProps = StackBarOptionalProps & { + value: StackBarValue | Record; +};