feat(cluster-health): add visibility options (#3210)
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user