feat(cluster-health): add visibility options (#3210)

This commit is contained in:
Meier Lukas
2025-05-24 17:49:49 +02:00
committed by GitHub
parent 2dc871e531
commit 939df8f6d1
4 changed files with 236 additions and 127 deletions

View File

@@ -157,6 +157,21 @@ const optionMapping: OptionMapping = {
defaultTab: (oldOptions) => ("defaultTabState" in oldOptions ? oldOptions.defaultTabState : undefined), defaultTab: (oldOptions) => ("defaultTabState" in oldOptions ? oldOptions.defaultTabState : undefined),
sectionIndicatorRequirement: (oldOptions) => sectionIndicatorRequirement: (oldOptions) =>
"sectionIndicatorColor" in oldOptions ? oldOptions.sectionIndicatorColor : undefined, "sectionIndicatorColor" in oldOptions ? oldOptions.sectionIndicatorColor : undefined,
showUptime: () => undefined,
visibleClusterSections: (oldOptions) => {
if (!("showNode" in oldOptions)) return undefined;
const oldKeys = {
showNode: "node" as const,
showLXCs: "lxc" as const,
showVM: "qemu" as const,
showStorage: "storage" as const,
} satisfies Partial<Record<keyof typeof oldOptions, string>>;
return objectEntries(oldKeys)
.filter(([key]) => oldOptions[key])
.map(([_, section]) => section);
},
}, },
mediaTranscoding: { mediaTranscoding: {
defaultView: (oldOptions) => oldOptions.defaultView, defaultView: (oldOptions) => oldOptions.defaultView,

View File

@@ -1757,12 +1757,18 @@
"memory": { "memory": {
"label": "Show Memory Info" "label": "Show Memory Info"
}, },
"showUptime": {
"label": "Show Uptime"
},
"fileSystem": { "fileSystem": {
"label": "Show Filesystem Info" "label": "Show Filesystem Info"
}, },
"defaultTab": { "defaultTab": {
"label": "Default tab" "label": "Default tab"
}, },
"visibleClusterSections": {
"label": "Visible cluster sections"
},
"sectionIndicatorRequirement": { "sectionIndicatorRequirement": {
"label": "Section indicator requirement" "label": "Section indicator requirement"
} }

View File

@@ -72,126 +72,156 @@ export const ClusterHealthMonitoring = ({
const cpuPercent = maxCpu ? (usedCpu / maxCpu) * 100 : 0; const cpuPercent = maxCpu ? (usedCpu / maxCpu) * 100 : 0;
const memPercent = maxMem ? (usedMem / maxMem) * 100 : 0; const memPercent = maxMem ? (usedMem / maxMem) * 100 : 0;
const defaultValue = [options.visibleClusterSections.at(0) ?? "node"];
const isTiny = width < 256; const isTiny = width < 256;
return ( return (
<Stack h="100%" p="xs" gap={isTiny ? "xs" : "md"}> <Stack h="100%" p="xs" gap={isTiny ? "xs" : "md"}>
<Group justify="center" wrap="nowrap"> {options.showUptime && (
<Text fz={isTiny ? 8 : "xs"} tt="uppercase" fw={700} c="dimmed" ta="center"> <Group justify="center" wrap="nowrap">
{formatUptime(uptime, t)} <Text fz={isTiny ? 8 : "xs"} tt="uppercase" fw={700} c="dimmed" ta="center">
</Text> {formatUptime(uptime, t)}
</Group> </Text>
<SummaryHeader cpu={cpuPercent} memory={memPercent} isTiny={isTiny} /> </Group>
<Accordion variant="contained" chevronPosition="right" multiple defaultValue={["node"]}> )}
<ResourceAccordionItem <SummaryHeader
value="node" cpu={{
title={t("widget.healthMonitoring.cluster.resource.node.name")} value: cpuPercent,
icon={IconServer} hidden: !options.cpu,
badge={addBadgeColor({ }}
activeCount: activeNodes, memory={{
totalCount: healthData.nodes.length, value: memPercent,
sectionIndicatorRequirement: options.sectionIndicatorRequirement, hidden: !options.memory,
})} }}
isTiny={isTiny} isTiny={isTiny}
> />
<ResourceTable type="node" data={healthData.nodes} isTiny={isTiny} /> {options.visibleClusterSections.length >= 1 && (
</ResourceAccordionItem> <Accordion variant="contained" chevronPosition="right" multiple defaultValue={defaultValue}>
{options.visibleClusterSections.includes("node") && (
<ResourceAccordionItem
value="node"
title={t("widget.healthMonitoring.cluster.resource.node.name")}
icon={IconServer}
badge={addBadgeColor({
activeCount: activeNodes,
totalCount: healthData.nodes.length,
sectionIndicatorRequirement: options.sectionIndicatorRequirement,
})}
isTiny={isTiny}
>
<ResourceTable type="node" data={healthData.nodes} isTiny={isTiny} />
</ResourceAccordionItem>
)}
<ResourceAccordionItem {options.visibleClusterSections.includes("qemu") && (
value="qemu" <ResourceAccordionItem
title={t("widget.healthMonitoring.cluster.resource.qemu.name")} value="qemu"
icon={IconDeviceLaptop} title={t("widget.healthMonitoring.cluster.resource.qemu.name")}
badge={addBadgeColor({ icon={IconDeviceLaptop}
activeCount: activeVMs, badge={addBadgeColor({
totalCount: healthData.vms.length, activeCount: activeVMs,
sectionIndicatorRequirement: options.sectionIndicatorRequirement, totalCount: healthData.vms.length,
})} sectionIndicatorRequirement: options.sectionIndicatorRequirement,
isTiny={isTiny} })}
> isTiny={isTiny}
<ResourceTable type="qemu" data={healthData.vms} isTiny={isTiny} /> >
</ResourceAccordionItem> <ResourceTable type="qemu" data={healthData.vms} isTiny={isTiny} />
</ResourceAccordionItem>
)}
<ResourceAccordionItem {options.visibleClusterSections.includes("lxc") && (
value="lxc" <ResourceAccordionItem
title={t("widget.healthMonitoring.cluster.resource.lxc.name")} value="lxc"
icon={IconCube} title={t("widget.healthMonitoring.cluster.resource.lxc.name")}
badge={addBadgeColor({ icon={IconCube}
activeCount: activeLXCs, badge={addBadgeColor({
totalCount: healthData.lxcs.length, activeCount: activeLXCs,
sectionIndicatorRequirement: options.sectionIndicatorRequirement, totalCount: healthData.lxcs.length,
})} sectionIndicatorRequirement: options.sectionIndicatorRequirement,
isTiny={isTiny} })}
> isTiny={isTiny}
<ResourceTable type="lxc" data={healthData.lxcs} isTiny={isTiny} /> >
</ResourceAccordionItem> <ResourceTable type="lxc" data={healthData.lxcs} isTiny={isTiny} />
</ResourceAccordionItem>
)}
<ResourceAccordionItem {options.visibleClusterSections.includes("storage") && (
value="storage" <ResourceAccordionItem
title={t("widget.healthMonitoring.cluster.resource.storage.name")} value="storage"
icon={IconDatabase} title={t("widget.healthMonitoring.cluster.resource.storage.name")}
badge={addBadgeColor({ icon={IconDatabase}
activeCount: activeStorage, badge={addBadgeColor({
totalCount: healthData.storages.length, activeCount: activeStorage,
sectionIndicatorRequirement: options.sectionIndicatorRequirement, totalCount: healthData.storages.length,
})} sectionIndicatorRequirement: options.sectionIndicatorRequirement,
isTiny={isTiny} })}
> isTiny={isTiny}
<ResourceTable type="storage" data={healthData.storages} isTiny={isTiny} /> >
</ResourceAccordionItem> <ResourceTable type="storage" data={healthData.storages} isTiny={isTiny} />
</Accordion> </ResourceAccordionItem>
)}
</Accordion>
)}
</Stack> </Stack>
); );
}; };
interface SummaryHeaderProps { interface SummaryHeaderProps {
cpu: number; cpu: { value: number; hidden: boolean };
memory: number; memory: { value: number; hidden: boolean };
isTiny: boolean; isTiny: boolean;
} }
const SummaryHeader = ({ cpu, memory, isTiny }: SummaryHeaderProps) => { const SummaryHeader = ({ cpu, memory, isTiny }: SummaryHeaderProps) => {
const t = useI18n(); const t = useI18n();
if (cpu.hidden && memory.hidden) return null;
return ( return (
<Center> <Center>
<Group wrap="wrap" justify="center" gap="xs"> <Group wrap="wrap" justify="center" gap="xs">
<Flex direction="row"> {!cpu.hidden && (
<RingProgress <Flex direction="row">
roundCaps <RingProgress
size={isTiny ? 32 : 48} roundCaps
thickness={isTiny ? 2 : 4} size={isTiny ? 32 : 48}
label={ thickness={isTiny ? 2 : 4}
<Center> label={
<IconCpu size={isTiny ? 12 : 20} /> <Center>
</Center> <IconCpu size={isTiny ? 12 : 20} />
} </Center>
sections={[{ value: cpu, color: cpu > 75 ? "orange" : "green" }]} }
/> sections={[{ value: cpu.value, color: cpu.value > 75 ? "orange" : "green" }]}
<Stack align="center" justify="center" gap={0}> />
<Text fw={500} size={isTiny ? "xs" : "sm"}> <Stack align="center" justify="center" gap={0}>
{t("widget.healthMonitoring.cluster.summary.cpu")} <Text fw={500} size={isTiny ? "xs" : "sm"}>
</Text> {t("widget.healthMonitoring.cluster.summary.cpu")}
<Text size={isTiny ? "8px" : "xs"}>{cpu.toFixed(1)}%</Text> </Text>
</Stack> <Text size={isTiny ? "8px" : "xs"}>{cpu.value.toFixed(1)}%</Text>
</Flex> </Stack>
<Flex> </Flex>
<RingProgress )}
roundCaps {!memory.hidden && (
size={isTiny ? 32 : 48} <Flex>
thickness={isTiny ? 2 : 4} <RingProgress
label={ roundCaps
<Center> size={isTiny ? 32 : 48}
<IconBrain size={isTiny ? 12 : 20} /> thickness={isTiny ? 2 : 4}
</Center> label={
} <Center>
sections={[{ value: memory, color: memory > 75 ? "orange" : "green" }]} <IconBrain size={isTiny ? 12 : 20} />
/> </Center>
<Stack align="center" justify="center" gap={0}> }
<Text size={isTiny ? "xs" : "sm"} fw={500}> sections={[{ value: memory.value, color: memory.value > 75 ? "orange" : "green" }]}
{t("widget.healthMonitoring.cluster.summary.memory")} />
</Text> <Stack align="center" justify="center" gap={0}>
<Text size={isTiny ? "8px" : "xs"}>{memory.toFixed(1)}%</Text> <Text size={isTiny ? "xs" : "sm"} fw={500}>
</Stack> {t("widget.healthMonitoring.cluster.summary.memory")}
</Flex> </Text>
<Text size={isTiny ? "8px" : "xs"}>{memory.value.toFixed(1)}%</Text>
</Stack>
</Flex>
)}
</Group> </Group>
</Center> </Center>
); );

View File

@@ -8,34 +8,92 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("healthMonitoring", { export const { definition, componentLoader } = createWidgetDefinition("healthMonitoring", {
icon: IconHeartRateMonitor, icon: IconHeartRateMonitor,
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from(
fahrenheit: factory.switch({ (factory) => ({
defaultValue: false, fahrenheit: factory.switch({
defaultValue: false,
}),
cpu: factory.switch({
defaultValue: true,
}),
memory: factory.switch({
defaultValue: true,
}),
showUptime: factory.switch({
defaultValue: true,
}),
fileSystem: factory.switch({
defaultValue: true,
}),
visibleClusterSections: factory.multiSelect({
options: [
{
value: "node",
label: (t) => t("widget.healthMonitoring.cluster.resource.node.name"),
},
{
value: "qemu",
label: (t) => t("widget.healthMonitoring.cluster.resource.qemu.name"),
},
{
value: "lxc",
label: (t) => t("widget.healthMonitoring.cluster.resource.lxc.name"),
},
{
value: "storage",
label: (t) => t("widget.healthMonitoring.cluster.resource.storage.name"),
},
] as const,
defaultValue: ["node", "qemu", "lxc", "storage"] as const,
}),
defaultTab: factory.select({
defaultValue: "system",
options: [
{ value: "system", label: "System" },
{ value: "cluster", label: "Cluster" },
] as const,
}),
sectionIndicatorRequirement: factory.select({
defaultValue: "all",
options: [
{ value: "all", label: "All active" },
{ value: "any", label: "Any active" },
] as const,
}),
}), }),
cpu: factory.switch({ {
defaultValue: true, fahrenheit: {
}), shouldHide(_, integrationKinds) {
memory: factory.switch({ // File system is only shown on system health tab
defaultValue: true, return integrationKinds.every((kind) => kind === "proxmox") || integrationKinds.length === 0;
}), },
fileSystem: factory.switch({ },
defaultValue: true, fileSystem: {
}), shouldHide(_, integrationKinds) {
defaultTab: factory.select({ // File system is only shown on system health tab
defaultValue: "system", return integrationKinds.every((kind) => kind === "proxmox") || integrationKinds.length === 0;
options: [ },
{ value: "system", label: "System" }, },
{ value: "cluster", label: "Cluster" }, showUptime: {
] as const, shouldHide(_, integrationKinds) {
}), // Uptime is only shown on cluster health tab
sectionIndicatorRequirement: factory.select({ return !integrationKinds.includes("proxmox");
defaultValue: "all", },
options: [ },
{ value: "all", label: "All active" }, sectionIndicatorRequirement: {
{ value: "any", label: "Any active" }, shouldHide(_, integrationKinds) {
] as const, // Section indicator requirement is only shown on cluster health tab
}), return !integrationKinds.includes("proxmox");
})); },
},
visibleClusterSections: {
shouldHide(_, integrationKinds) {
// Cluster sections are only shown on cluster health tab
return !integrationKinds.includes("proxmox");
},
},
},
);
}, },
supportedIntegrations: getIntegrationKindsByCategory("healthMonitoring"), supportedIntegrations: getIntegrationKindsByCategory("healthMonitoring"),
errors: { errors: {