fix: add subscription to health monitoring widget (#1210)

* fix: add subscription

* fix: add time stamped

* fix: rtl, timestamp, scrollArea

* fix: common.rtl

* fix: rtl

* fix: reviewed changes

* fix: translation

* fix: reviewed changes

* fix: deepScource

* fix: reviewed changes

* fix: add last seen
This commit is contained in:
Yossi Hillali
2024-10-18 21:42:33 +03:00
committed by GitHub
parent c52fd972b7
commit ce67fcd57c
3 changed files with 99 additions and 26 deletions

View File

@@ -13,15 +13,13 @@ export const healthMonitoringRouter = createTRPCRouter({
return await Promise.all(
ctx.integrations.map(async (integration) => {
const channel = createItemAndIntegrationChannel<HealthMonitoring>("healthMonitoring", integration.id);
const data = await channel.getAsync();
if (!data) {
return null;
}
const { data: healthInfo, timestamp } = (await channel.getAsync()) ?? { data: null, timestamp: new Date(0) };
return {
integrationId: integration.id,
integrationName: integration.name,
healthInfo: data.data,
healthInfo,
timestamp,
};
}),
);
@@ -30,7 +28,7 @@ export const healthMonitoringRouter = createTRPCRouter({
subscribeHealthStatus: publicProcedure
.unstable_concat(createManyIntegrationMiddleware("query", "openmediavault"))
.subscription(({ ctx }) => {
return observable<{ integrationId: string; healthInfo: HealthMonitoring }>((emit) => {
return observable<{ integrationId: string; healthInfo: HealthMonitoring; timestamp: Date }>((emit) => {
const unsubscribes: (() => void)[] = [];
for (const integration of ctx.integrations) {
const channel = createItemAndIntegrationChannel<HealthMonitoring>("healthMonitoring", integration.id);
@@ -38,6 +36,7 @@ export const healthMonitoringRouter = createTRPCRouter({
emit.next({
integrationId: integration.id,
healthInfo,
timestamp: new Date(0),
});
});
unsubscribes.push(unsubscribe);

View File

@@ -584,6 +584,9 @@ export default {
information: {
min: "Min",
max: "Max",
days: "Days",
hours: "Hours",
minutes: "Minutes",
},
notification: {
create: {
@@ -1119,16 +1122,17 @@ export default {
},
popover: {
information: "Information",
processor: "Processor:",
memory: "Memory:",
version: "Version:",
uptime: "Uptime: {days} days, {hours} hours",
processor: "Processor: {cpuModelName}",
memory: "Memory: {memory}GiB",
memoryAvailable: "Available: {memoryAvailable}GiB ({percent}%)",
version: "Version: {version}",
uptime: "Uptime: {days} Days, {hours} Hours, {minutes} Minutes",
loadAverage: "Load average:",
minute: "1 minute:",
minutes: "{count} minutes:",
minute: "1 minute",
minutes: "{count} minutes",
used: "Used",
diskAvailable: "Available",
memAvailable: "Available:",
available: "Available",
lastSeen: "Last status update: {lastSeen}",
},
memory: {},
error: {

View File

@@ -29,14 +29,19 @@ import {
IconTemperature,
IconVersions,
} from "@tabler/icons-react";
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
import { clientApi } from "@homarr/api/client";
import type { HealthMonitoring } from "@homarr/integrations";
import type { TranslationFunction } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../definition";
import { NoIntegrationSelectedError } from "../errors";
dayjs.extend(duration);
export default function HealthMonitoringWidget({ options, integrationIds }: WidgetComponentProps<"healthMonitoring">) {
const t = useI18n();
const [healthData] = clientApi.widget.healthMonitoring.getHealthStatus.useSuspenseQuery(
@@ -48,17 +53,56 @@ export default function HealthMonitoringWidget({ options, integrationIds }: Widg
refetchOnWindowFocus: false,
refetchOnReconnect: false,
retry: false,
select: (data) => data.filter((health) => health !== null),
select: (data) =>
data.filter(
(
health,
): health is {
integrationId: string;
integrationName: string;
healthInfo: HealthMonitoring;
timestamp: Date;
} => health.healthInfo !== null,
),
},
);
const [opened, { open, close }] = useDisclosure(false);
const utils = clientApi.useUtils();
clientApi.widget.healthMonitoring.subscribeHealthStatus.useSubscription(
{ integrationIds },
{
onData(data) {
utils.widget.healthMonitoring.getHealthStatus.setData({ integrationIds }, (prevData) => {
if (!prevData) {
return undefined;
}
const newData = prevData.map((item) =>
item.integrationId === data.integrationId
? { ...item, healthInfo: data.healthInfo, timestamp: new Date(0) }
: item,
);
return newData.filter(
(
health,
): health is {
integrationId: string;
integrationName: string;
healthInfo: HealthMonitoring;
timestamp: Date;
} => health.healthInfo !== null,
);
});
},
},
);
if (integrationIds.length === 0) {
throw new NoIntegrationSelectedError();
}
return (
<Stack h="100%" gap="2.5cqmin" className="health-monitoring">
{healthData.map(({ integrationId, integrationName, healthInfo }) => {
{healthData.map(({ integrationId, integrationName, healthInfo, timestamp }) => {
const disksData = matchFileSystemAndSmart(healthInfo.fileSystem, healthInfo.smart);
const memoryUsage = formatMemoryUsage(healthInfo.memAvailable, healthInfo.memUsed);
return (
@@ -107,21 +151,30 @@ export default function HealthMonitoringWidget({ options, integrationIds }: Widg
className="health-monitoring-information-processor"
icon={<IconCpu2 size="1.5cqmin" />}
>
{t("widget.healthMonitoring.popover.processor")} {healthInfo.cpuModelName}
{t("widget.healthMonitoring.popover.processor", { cpuModelName: healthInfo.cpuModelName })}
</List.Item>
<List.Item
className="health-monitoring-information-memory"
icon={<IconBrain size="1.5cqmin" />}
>
{t("widget.healthMonitoring.popover.memory")} {memoryUsage.memTotal.GB}GiB -{" "}
{t("widget.healthMonitoring.popover.memAvailable")} {memoryUsage.memFree.GB}GiB (
{memoryUsage.memFree.percent}%)
{t("widget.healthMonitoring.popover.memory", { memory: memoryUsage.memTotal.GB })}
</List.Item>
<List.Item
className="health-monitoring-information-memory"
icon={<IconBrain size="1.5cqmin" />}
>
{t("widget.healthMonitoring.popover.memoryAvailable", {
memoryAvailable: memoryUsage.memFree.GB,
percent: memoryUsage.memFree.percent,
})}
</List.Item>
<List.Item
className="health-monitoring-information-version"
icon={<IconVersions size="1.5cqmin" />}
>
{t("widget.healthMonitoring.popover.version")} {healthInfo.version}
{t("widget.healthMonitoring.popover.version", {
version: healthInfo.version,
})}
</List.Item>
<List.Item
className="health-monitoring-information-uptime"
@@ -158,11 +211,25 @@ export default function HealthMonitoringWidget({ options, integrationIds }: Widg
)}
{options.memory && <MemoryRing available={healthInfo.memAvailable} used={healthInfo.memUsed} />}
</Flex>
<Text
className="health-monitoring-status-update-time"
c="dimmed"
size="3.5cqmin"
ta="center"
mb="2.5cqmin"
>
{t("widget.healthMonitoring.popover.lastSeen", { lastSeen: dayjs(timestamp).fromNow() })}
</Text>
</Card>
{options.fileSystem &&
disksData.map((disk) => {
return (
<Card className="health-monitoring-disk-card" key={disk.deviceName} p="2.5cqmin" withBorder>
<Card
className={`health-monitoring-disk-card health-monitoring-disk-card-${integrationName}`}
key={disk.deviceName}
p="2.5cqmin"
withBorder
>
<Flex className="health-monitoring-disk-status" justify="space-between" align="center" m="1.5cqmin">
<Group gap="1cqmin">
<IconServer className="health-monitoring-disk-icon" size="5cqmin" />
@@ -211,7 +278,7 @@ export default function HealthMonitoringWidget({ options, integrationIds }: Widg
color="default"
>
<Progress.Label className="health-monitoring-disk-available-value" fz="2.5cqmin">
{t("widget.healthMonitoring.popover.diskAvailable")}
{t("widget.healthMonitoring.popover.available")}
</Progress.Label>
</Progress.Section>
</Tooltip>
@@ -227,9 +294,12 @@ export default function HealthMonitoringWidget({ options, integrationIds }: Widg
}
export const formatUptime = (uptimeInSeconds: number, t: TranslationFunction) => {
const days = Math.floor(uptimeInSeconds / (60 * 60 * 24));
const remainingHours = Math.floor((uptimeInSeconds % (60 * 60 * 24)) / 3600);
return t("widget.healthMonitoring.popover.uptime", { days, hours: remainingHours });
const uptimeDuration = dayjs.duration(uptimeInSeconds, "seconds");
const days = uptimeDuration.days();
const hours = uptimeDuration.hours();
const minutes = uptimeDuration.minutes();
return t("widget.healthMonitoring.popover.uptime", { days, hours, minutes });
};
export const progressColor = (percentage: number) => {