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:
@@ -13,15 +13,13 @@ export const healthMonitoringRouter = createTRPCRouter({
|
|||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
ctx.integrations.map(async (integration) => {
|
ctx.integrations.map(async (integration) => {
|
||||||
const channel = createItemAndIntegrationChannel<HealthMonitoring>("healthMonitoring", integration.id);
|
const channel = createItemAndIntegrationChannel<HealthMonitoring>("healthMonitoring", integration.id);
|
||||||
const data = await channel.getAsync();
|
const { data: healthInfo, timestamp } = (await channel.getAsync()) ?? { data: null, timestamp: new Date(0) };
|
||||||
if (!data) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
integrationId: integration.id,
|
integrationId: integration.id,
|
||||||
integrationName: integration.name,
|
integrationName: integration.name,
|
||||||
healthInfo: data.data,
|
healthInfo,
|
||||||
|
timestamp,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -30,7 +28,7 @@ export const healthMonitoringRouter = createTRPCRouter({
|
|||||||
subscribeHealthStatus: publicProcedure
|
subscribeHealthStatus: publicProcedure
|
||||||
.unstable_concat(createManyIntegrationMiddleware("query", "openmediavault"))
|
.unstable_concat(createManyIntegrationMiddleware("query", "openmediavault"))
|
||||||
.subscription(({ ctx }) => {
|
.subscription(({ ctx }) => {
|
||||||
return observable<{ integrationId: string; healthInfo: HealthMonitoring }>((emit) => {
|
return observable<{ integrationId: string; healthInfo: HealthMonitoring; timestamp: Date }>((emit) => {
|
||||||
const unsubscribes: (() => void)[] = [];
|
const unsubscribes: (() => void)[] = [];
|
||||||
for (const integration of ctx.integrations) {
|
for (const integration of ctx.integrations) {
|
||||||
const channel = createItemAndIntegrationChannel<HealthMonitoring>("healthMonitoring", integration.id);
|
const channel = createItemAndIntegrationChannel<HealthMonitoring>("healthMonitoring", integration.id);
|
||||||
@@ -38,6 +36,7 @@ export const healthMonitoringRouter = createTRPCRouter({
|
|||||||
emit.next({
|
emit.next({
|
||||||
integrationId: integration.id,
|
integrationId: integration.id,
|
||||||
healthInfo,
|
healthInfo,
|
||||||
|
timestamp: new Date(0),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
unsubscribes.push(unsubscribe);
|
unsubscribes.push(unsubscribe);
|
||||||
|
|||||||
@@ -584,6 +584,9 @@ export default {
|
|||||||
information: {
|
information: {
|
||||||
min: "Min",
|
min: "Min",
|
||||||
max: "Max",
|
max: "Max",
|
||||||
|
days: "Days",
|
||||||
|
hours: "Hours",
|
||||||
|
minutes: "Minutes",
|
||||||
},
|
},
|
||||||
notification: {
|
notification: {
|
||||||
create: {
|
create: {
|
||||||
@@ -1119,16 +1122,17 @@ export default {
|
|||||||
},
|
},
|
||||||
popover: {
|
popover: {
|
||||||
information: "Information",
|
information: "Information",
|
||||||
processor: "Processor:",
|
processor: "Processor: {cpuModelName}",
|
||||||
memory: "Memory:",
|
memory: "Memory: {memory}GiB",
|
||||||
version: "Version:",
|
memoryAvailable: "Available: {memoryAvailable}GiB ({percent}%)",
|
||||||
uptime: "Uptime: {days} days, {hours} hours",
|
version: "Version: {version}",
|
||||||
|
uptime: "Uptime: {days} Days, {hours} Hours, {minutes} Minutes",
|
||||||
loadAverage: "Load average:",
|
loadAverage: "Load average:",
|
||||||
minute: "1 minute:",
|
minute: "1 minute",
|
||||||
minutes: "{count} minutes:",
|
minutes: "{count} minutes",
|
||||||
used: "Used",
|
used: "Used",
|
||||||
diskAvailable: "Available",
|
available: "Available",
|
||||||
memAvailable: "Available:",
|
lastSeen: "Last status update: {lastSeen}",
|
||||||
},
|
},
|
||||||
memory: {},
|
memory: {},
|
||||||
error: {
|
error: {
|
||||||
|
|||||||
@@ -29,14 +29,19 @@ import {
|
|||||||
IconTemperature,
|
IconTemperature,
|
||||||
IconVersions,
|
IconVersions,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import duration from "dayjs/plugin/duration";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
import type { HealthMonitoring } from "@homarr/integrations";
|
||||||
import type { TranslationFunction } from "@homarr/translation";
|
import type { TranslationFunction } from "@homarr/translation";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import type { WidgetComponentProps } from "../definition";
|
import type { WidgetComponentProps } from "../definition";
|
||||||
import { NoIntegrationSelectedError } from "../errors";
|
import { NoIntegrationSelectedError } from "../errors";
|
||||||
|
|
||||||
|
dayjs.extend(duration);
|
||||||
|
|
||||||
export default function HealthMonitoringWidget({ options, integrationIds }: WidgetComponentProps<"healthMonitoring">) {
|
export default function HealthMonitoringWidget({ options, integrationIds }: WidgetComponentProps<"healthMonitoring">) {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const [healthData] = clientApi.widget.healthMonitoring.getHealthStatus.useSuspenseQuery(
|
const [healthData] = clientApi.widget.healthMonitoring.getHealthStatus.useSuspenseQuery(
|
||||||
@@ -48,17 +53,56 @@ export default function HealthMonitoringWidget({ options, integrationIds }: Widg
|
|||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
retry: 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 [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) {
|
if (integrationIds.length === 0) {
|
||||||
throw new NoIntegrationSelectedError();
|
throw new NoIntegrationSelectedError();
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Stack h="100%" gap="2.5cqmin" className="health-monitoring">
|
<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 disksData = matchFileSystemAndSmart(healthInfo.fileSystem, healthInfo.smart);
|
||||||
const memoryUsage = formatMemoryUsage(healthInfo.memAvailable, healthInfo.memUsed);
|
const memoryUsage = formatMemoryUsage(healthInfo.memAvailable, healthInfo.memUsed);
|
||||||
return (
|
return (
|
||||||
@@ -107,21 +151,30 @@ export default function HealthMonitoringWidget({ options, integrationIds }: Widg
|
|||||||
className="health-monitoring-information-processor"
|
className="health-monitoring-information-processor"
|
||||||
icon={<IconCpu2 size="1.5cqmin" />}
|
icon={<IconCpu2 size="1.5cqmin" />}
|
||||||
>
|
>
|
||||||
{t("widget.healthMonitoring.popover.processor")} {healthInfo.cpuModelName}
|
{t("widget.healthMonitoring.popover.processor", { cpuModelName: healthInfo.cpuModelName })}
|
||||||
</List.Item>
|
</List.Item>
|
||||||
<List.Item
|
<List.Item
|
||||||
className="health-monitoring-information-memory"
|
className="health-monitoring-information-memory"
|
||||||
icon={<IconBrain size="1.5cqmin" />}
|
icon={<IconBrain size="1.5cqmin" />}
|
||||||
>
|
>
|
||||||
{t("widget.healthMonitoring.popover.memory")} {memoryUsage.memTotal.GB}GiB -{" "}
|
{t("widget.healthMonitoring.popover.memory", { memory: memoryUsage.memTotal.GB })}
|
||||||
{t("widget.healthMonitoring.popover.memAvailable")} {memoryUsage.memFree.GB}GiB (
|
</List.Item>
|
||||||
{memoryUsage.memFree.percent}%)
|
<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>
|
||||||
<List.Item
|
<List.Item
|
||||||
className="health-monitoring-information-version"
|
className="health-monitoring-information-version"
|
||||||
icon={<IconVersions size="1.5cqmin" />}
|
icon={<IconVersions size="1.5cqmin" />}
|
||||||
>
|
>
|
||||||
{t("widget.healthMonitoring.popover.version")} {healthInfo.version}
|
{t("widget.healthMonitoring.popover.version", {
|
||||||
|
version: healthInfo.version,
|
||||||
|
})}
|
||||||
</List.Item>
|
</List.Item>
|
||||||
<List.Item
|
<List.Item
|
||||||
className="health-monitoring-information-uptime"
|
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} />}
|
{options.memory && <MemoryRing available={healthInfo.memAvailable} used={healthInfo.memUsed} />}
|
||||||
</Flex>
|
</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>
|
</Card>
|
||||||
{options.fileSystem &&
|
{options.fileSystem &&
|
||||||
disksData.map((disk) => {
|
disksData.map((disk) => {
|
||||||
return (
|
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">
|
<Flex className="health-monitoring-disk-status" justify="space-between" align="center" m="1.5cqmin">
|
||||||
<Group gap="1cqmin">
|
<Group gap="1cqmin">
|
||||||
<IconServer className="health-monitoring-disk-icon" size="5cqmin" />
|
<IconServer className="health-monitoring-disk-icon" size="5cqmin" />
|
||||||
@@ -211,7 +278,7 @@ export default function HealthMonitoringWidget({ options, integrationIds }: Widg
|
|||||||
color="default"
|
color="default"
|
||||||
>
|
>
|
||||||
<Progress.Label className="health-monitoring-disk-available-value" fz="2.5cqmin">
|
<Progress.Label className="health-monitoring-disk-available-value" fz="2.5cqmin">
|
||||||
{t("widget.healthMonitoring.popover.diskAvailable")}
|
{t("widget.healthMonitoring.popover.available")}
|
||||||
</Progress.Label>
|
</Progress.Label>
|
||||||
</Progress.Section>
|
</Progress.Section>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -227,9 +294,12 @@ export default function HealthMonitoringWidget({ options, integrationIds }: Widg
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const formatUptime = (uptimeInSeconds: number, t: TranslationFunction) => {
|
export const formatUptime = (uptimeInSeconds: number, t: TranslationFunction) => {
|
||||||
const days = Math.floor(uptimeInSeconds / (60 * 60 * 24));
|
const uptimeDuration = dayjs.duration(uptimeInSeconds, "seconds");
|
||||||
const remainingHours = Math.floor((uptimeInSeconds % (60 * 60 * 24)) / 3600);
|
const days = uptimeDuration.days();
|
||||||
return t("widget.healthMonitoring.popover.uptime", { days, hours: remainingHours });
|
const hours = uptimeDuration.hours();
|
||||||
|
const minutes = uptimeDuration.minutes();
|
||||||
|
|
||||||
|
return t("widget.healthMonitoring.popover.uptime", { days, hours, minutes });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const progressColor = (percentage: number) => {
|
export const progressColor = (percentage: number) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user