Merge pull request #483 from ylei-tsubame/issues/442-dashboard-nodes
Web UI: add node list to dashboardmain
commit
dc5b01f087
24 changed files with 956 additions and 156 deletions
@ -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<string, string> = { |
||||
offline: PURPLE, |
||||
online: BLUE, |
||||
}; |
||||
|
||||
const AnvilSummary: FC<AnvilSummaryProps> = (props) => { |
||||
const { anvilUuid } = props; |
||||
|
||||
const { data: rAnvil, loading: loadingAnvil } = useFetch<AnvilListItem>( |
||||
`/anvil/${anvilUuid}`, |
||||
); |
||||
|
||||
const anvil = useMemo<APIAnvilDetail | undefined>( |
||||
() => rAnvil && toAnvilDetail(rAnvil), |
||||
[rAnvil], |
||||
); |
||||
|
||||
const { data: cpu, loading: loadingCpu } = useFetch<AnvilCPU>( |
||||
`/anvil/${anvilUuid}/cpu`, |
||||
); |
||||
|
||||
const cpuSubnodes = useMemo<AnvilCPU['hosts'][string][] | undefined>( |
||||
() => cpu && Object.values(cpu.hosts), |
||||
[cpu], |
||||
); |
||||
|
||||
const { data: rMemory, loading: loadingMemory } = useFetch<AnvilMemory>( |
||||
`/anvil/${anvilUuid}/memory`, |
||||
); |
||||
|
||||
const memory = useMemo<AnvilMemoryCalcable | undefined>( |
||||
() => rMemory && toAnvilMemoryCalcable(rMemory), |
||||
[rMemory], |
||||
); |
||||
|
||||
const { data: rStorages, loading: loadingStorages } = |
||||
useFetch<AnvilSharedStorage>(`/anvil/${anvilUuid}/store`); |
||||
|
||||
const storages = useMemo<APIAnvilSharedStorageOverview | undefined>( |
||||
() => rStorages && toAnvilSharedStorageOverview(rStorages), |
||||
[rStorages], |
||||
); |
||||
|
||||
const loading = useMemo<boolean>( |
||||
() => |
||||
[loadingAnvil, loadingCpu, loadingMemory, loadingStorages].some( |
||||
(cond) => cond, |
||||
), |
||||
[loadingAnvil, loadingCpu, loadingMemory, loadingStorages], |
||||
); |
||||
|
||||
const anvilSummary = useMemo( |
||||
() => |
||||
anvil && ( |
||||
<MonoText inheritColour color={MAP_TO_ANVIL_STATE_COLOUR[anvil.state]}> |
||||
{anvil.state} |
||||
</MonoText> |
||||
), |
||||
[anvil], |
||||
); |
||||
|
||||
const hostsSummary = useMemo( |
||||
() => |
||||
anvil && ( |
||||
<Grid |
||||
alignItems="center" |
||||
columns={20} |
||||
columnSpacing="0.5em" |
||||
container |
||||
sx={{ |
||||
[`& > .${gridClasses.item}:nth-child(-n + 4)`]: { |
||||
marginBottom: '-.6em', |
||||
}, |
||||
}} |
||||
> |
||||
{Object.values(anvil.hosts).map<ReactNode>((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 = <MonoText variant="caption">{serverCount}</MonoText>; |
||||
} else { |
||||
stateValue = `${stateProgress}%`; |
||||
} |
||||
|
||||
return [ |
||||
<Grid item key={`${uuid}-state-label`} xs={7}> |
||||
<BodyText variant="caption" whiteSpace="nowrap"> |
||||
{name} |
||||
</BodyText> |
||||
</Grid>, |
||||
<Grid item key={`${uuid}-state`} xs={5}> |
||||
<MonoText inheritColour color={stateColour}> |
||||
{stateValue} |
||||
</MonoText> |
||||
</Grid>, |
||||
<Grid item key={`${uuid}-divider`} xs> |
||||
<Divider sx={{ marginBottom: '-.4em' }} /> |
||||
</Grid>, |
||||
<Grid item key={`${uuid}-server-label`} width="2.2em"> |
||||
{servers && <BodyText variant="caption">Servers</BodyText>} |
||||
</Grid>, |
||||
<Grid |
||||
display="flex" |
||||
item |
||||
justifyContent="flex-end" |
||||
key={`${uuid}-server-count`} |
||||
width="2em" |
||||
> |
||||
{servers} |
||||
</Grid>, |
||||
]; |
||||
})} |
||||
</Grid> |
||||
), |
||||
[anvil], |
||||
); |
||||
|
||||
const cpuSummary = useMemo( |
||||
() => |
||||
cpu && |
||||
cpuSubnodes && ( |
||||
<FlexBox row spacing=".5em"> |
||||
<FlexBox spacing={0}> |
||||
<BodyText variant="caption" whiteSpace="nowrap"> |
||||
Vendor{' '} |
||||
<InlineMonoText sx={{ paddingRight: 0 }}> |
||||
{cpuSubnodes[0].vendor} |
||||
</InlineMonoText> |
||||
</BodyText> |
||||
</FlexBox> |
||||
<Divider sx={{ flexGrow: 1 }} /> |
||||
<Grid |
||||
alignItems="center" |
||||
columns={2} |
||||
container |
||||
sx={{ |
||||
width: '3.7em', |
||||
|
||||
[`& > .${gridClasses.item}:nth-child(-n + 2)`]: { |
||||
marginBottom: '-.6em', |
||||
}, |
||||
}} |
||||
> |
||||
<Grid item xs={1}> |
||||
<BodyText variant="caption">Cores</BodyText> |
||||
</Grid> |
||||
<Grid display="flex" item justifyContent="flex-end" xs={1}> |
||||
<MonoText variant="caption">{cpu.cores}</MonoText> |
||||
</Grid> |
||||
<Grid item xs={1}> |
||||
<BodyText variant="caption">Threads</BodyText> |
||||
</Grid> |
||||
<Grid display="flex" item justifyContent="flex-end" xs={1}> |
||||
<MonoText variant="caption">{cpu.threads}</MonoText> |
||||
</Grid> |
||||
</Grid> |
||||
</FlexBox> |
||||
), |
||||
[cpu, cpuSubnodes], |
||||
); |
||||
|
||||
const memorySummary = useMemo( |
||||
() => |
||||
memory && ( |
||||
<FlexBox spacing={0}> |
||||
<FlexBox row justifyContent="flex-end"> |
||||
<BodyText mb="-.3em" variant="caption"> |
||||
Free |
||||
<InlineMonoText> |
||||
{dSizeStr(memory.total - (memory.reserved + memory.allocated), { |
||||
toUnit: 'ibyte', |
||||
})} |
||||
</InlineMonoText> |
||||
/ |
||||
<InlineMonoText sx={{ paddingRight: 0 }}> |
||||
{dSizeStr(memory.total, { toUnit: 'ibyte' })} |
||||
</InlineMonoText> |
||||
</BodyText> |
||||
</FlexBox> |
||||
<StackBar |
||||
thin |
||||
value={{ |
||||
reserved: { |
||||
value: Number((memory.reserved * N_100) / memory.total), |
||||
}, |
||||
allocated: { |
||||
value: Number( |
||||
((memory.reserved + memory.allocated) * N_100) / memory.total, |
||||
), |
||||
colour: { 0: BLUE, 70: PURPLE, 90: RED }, |
||||
}, |
||||
}} |
||||
/> |
||||
</FlexBox> |
||||
), |
||||
[memory], |
||||
); |
||||
|
||||
const storeSummary = useMemo( |
||||
() => |
||||
storages && ( |
||||
<FlexBox spacing={0}> |
||||
<FlexBox row justifyContent="flex-end"> |
||||
<BodyText mb="-.3em" variant="caption"> |
||||
Total free |
||||
<InlineMonoText> |
||||
{dSizeStr(storages.totalFree, { toUnit: 'ibyte' })} |
||||
</InlineMonoText> |
||||
/ |
||||
<InlineMonoText sx={{ paddingRight: 0 }}> |
||||
{dSizeStr(storages.totalSize, { toUnit: 'ibyte' })} |
||||
</InlineMonoText> |
||||
</BodyText> |
||||
</FlexBox> |
||||
<StackBar |
||||
thin |
||||
value={{ |
||||
allocated: { |
||||
value: Number( |
||||
((storages.totalSize - storages.totalFree) * N_100) / |
||||
storages.totalSize, |
||||
), |
||||
colour: { 0: BLUE, 70: PURPLE, 90: RED }, |
||||
}, |
||||
}} |
||||
/> |
||||
</FlexBox> |
||||
), |
||||
[storages], |
||||
); |
||||
|
||||
return loading ? ( |
||||
<Spinner mt={0} /> |
||||
) : ( |
||||
<Grid |
||||
alignItems="center" |
||||
columns={4} |
||||
container |
||||
sx={{ |
||||
[`& > .${gridClasses.item}:nth-child(odd)`]: { |
||||
alignItems: 'center', |
||||
display: 'flex', |
||||
height: '2.2em', |
||||
}, |
||||
}} |
||||
> |
||||
<Grid item xs={1}> |
||||
<BodyText>Node</BodyText> |
||||
</Grid> |
||||
<Grid item xs={3}> |
||||
{anvilSummary} |
||||
</Grid> |
||||
<Grid item xs={1}> |
||||
<BodyText>Subnodes</BodyText> |
||||
</Grid> |
||||
<Grid item xs={3}> |
||||
{hostsSummary} |
||||
</Grid> |
||||
<Grid item xs={1}> |
||||
<BodyText>CPU</BodyText> |
||||
</Grid> |
||||
<Grid item xs={3}> |
||||
{cpuSummary} |
||||
</Grid> |
||||
<Grid item xs={1}> |
||||
<BodyText>Memory</BodyText> |
||||
</Grid> |
||||
<Grid item xs={3}> |
||||
{memorySummary} |
||||
</Grid> |
||||
<Grid item xs={1}> |
||||
<BodyText>Storage</BodyText> |
||||
</Grid> |
||||
<Grid item xs={3}> |
||||
{storeSummary} |
||||
</Grid> |
||||
</Grid> |
||||
); |
||||
}; |
||||
|
||||
export default AnvilSummary; |
@ -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<APIAnvilOverviewArray>('/anvil', { refreshInterval: 5000 }); |
||||
|
||||
const anvils = useMemo<APIAnvilOverviewList | undefined>( |
||||
() => rawAnvils && toAnvilOverviewList(rawAnvils), |
||||
[rawAnvils], |
||||
); |
||||
|
||||
const grid = useMemo<ReactNode>( |
||||
() => |
||||
anvils && ( |
||||
<Grid |
||||
columns={{ xs: 1, sm: 2, md: 3, xl: 4 }} |
||||
layout={Object.values(anvils).reduce<GridLayout>( |
||||
(previous, current) => { |
||||
const { description, name, uuid } = current; |
||||
|
||||
const key = `anvil-${uuid}`; |
||||
|
||||
previous[key] = { |
||||
children: ( |
||||
<InnerPanel height="100%" mv={0}> |
||||
<InnerPanelHeader> |
||||
<BodyText |
||||
overflow="hidden" |
||||
textOverflow="ellipsis" |
||||
whiteSpace="nowrap" |
||||
> |
||||
{name}: {description} |
||||
</BodyText> |
||||
</InnerPanelHeader> |
||||
<InnerPanelBody> |
||||
<AnvilSummary anvilUuid={uuid} /> |
||||
</InnerPanelBody> |
||||
</InnerPanel> |
||||
), |
||||
}; |
||||
|
||||
return previous; |
||||
}, |
||||
{}, |
||||
)} |
||||
spacing="1em" |
||||
sx={{ |
||||
alignContent: 'stretch', |
||||
|
||||
[`& > .${gridClasses.item}`]: { |
||||
minWidth: '20em', |
||||
}, |
||||
}} |
||||
/> |
||||
), |
||||
[anvils], |
||||
); |
||||
|
||||
return ( |
||||
<Panel> |
||||
<PanelHeader> |
||||
<HeaderText>Nodes</HeaderText> |
||||
</PanelHeader> |
||||
{loadingAnvils ? <Spinner /> : grid} |
||||
</Panel> |
||||
); |
||||
}; |
||||
|
||||
export default AnvilSummaryList; |
@ -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; |
||||
|
@ -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<StackBarProps> = (props) => { |
||||
const { barProps = {}, thin, underlineProps, value } = props; |
||||
|
||||
const { sx: barSx, ...restBarProps } = barProps; |
||||
|
||||
const values = useMemo<Record<string, StackBarValue>>( |
||||
() => ('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<ReactElement[]>( |
||||
() => |
||||
entries.map<ReactElement>( |
||||
([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 ( |
||||
<Box position="relative"> |
||||
{bars} |
||||
{createElement(creatableUnderline, underlineProps)} |
||||
</Box> |
||||
); |
||||
}; |
||||
|
||||
export default StackBar; |
@ -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; |
@ -1,4 +1,5 @@ |
||||
import AllocationBar from './AllocationBar'; |
||||
import ProgressBar from './ProgressBar'; |
||||
import StackBar from './StackBar'; |
||||
|
||||
export { AllocationBar, ProgressBar }; |
||||
export { AllocationBar, ProgressBar, StackBar }; |
||||
|
@ -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, |
||||
}; |
@ -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<APIAnvilDetail['hosts']>((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; |
@ -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; |
@ -0,0 +1,13 @@ |
||||
const toAnvilOverviewHostList = ( |
||||
data: APIAnvilOverviewArray[number]['hosts'], |
||||
): APIAnvilOverview['hosts'] => |
||||
data.reduce<APIAnvilOverview['hosts']>( |
||||
(previous, { hostName: name, hostType: type, hostUUID: uuid }) => { |
||||
previous[uuid] = { name, type, uuid }; |
||||
|
||||
return previous; |
||||
}, |
||||
{}, |
||||
); |
||||
|
||||
export default toAnvilOverviewHostList; |
@ -0,0 +1,28 @@ |
||||
import toAnvilOverviewHostList from './toAnvilOverviewHostList'; |
||||
|
||||
const toAnvilOverviewList = ( |
||||
data: APIAnvilOverviewArray, |
||||
): APIAnvilOverviewList => |
||||
data.reduce<APIAnvilOverviewList>( |
||||
( |
||||
previous, |
||||
{ |
||||
anvilDescription: description, |
||||
anvilName: name, |
||||
anvilUUID: uuid, |
||||
hosts, |
||||
}, |
||||
) => { |
||||
previous[uuid] = { |
||||
description, |
||||
hosts: toAnvilOverviewHostList(hosts), |
||||
name, |
||||
uuid, |
||||
}; |
||||
|
||||
return previous; |
||||
}, |
||||
{}, |
||||
); |
||||
|
||||
export default toAnvilOverviewList; |
@ -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<APIAnvilSharedStorageOverview>( |
||||
(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; |
@ -0,0 +1,3 @@ |
||||
type AnvilSummaryProps = { |
||||
anvilUuid: string; |
||||
}; |
@ -0,0 +1,14 @@ |
||||
type StackBarValue = { |
||||
colour?: string | Record<number, string>; |
||||
value: number; |
||||
}; |
||||
|
||||
type StackBarOptionalProps = { |
||||
barProps?: import('@mui/material').LinearProgressProps; |
||||
thin?: boolean; |
||||
underlineProps?: import('@mui/material').BoxProps; |
||||
}; |
||||
|
||||
type StackBarProps = StackBarOptionalProps & { |
||||
value: StackBarValue | Record<string, StackBarValue>; |
||||
}; |
Loading…
Reference in new issue