Files
Termix/plugins/homepage/src/frontend/widgets/SystemOverviewWidget.tsx
T

143 lines
4.4 KiB
TypeScript

import { useEffect, useState } from "react";
import { Server } from "lucide-react";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import { registerWidget } from "./WidgetRegistry";
import type { SystemOverviewConfig, WidgetComponentProps } from "../types.js";
import { GRID_SIZE } from "../types.js";
import {
WidgetTitle,
getDatabaseHealth,
getUptime,
getVersionInfo,
runVisibleInterval,
} from "@termix/plugin-sdk/ui";
interface InfoRowProps {
label: string;
value: string | null | undefined;
valueColor?: string;
}
function InfoRow({ label, value, valueColor }: InfoRowProps) {
if (!value) return null;
return (
<div className="flex justify-between text-[10px] gap-2">
<span className="text-muted-foreground shrink-0">{label}</span>
<span
className="text-foreground truncate text-right font-medium"
style={valueColor ? { color: valueColor } : undefined}
>
{value}
</span>
</div>
);
}
function SystemOverviewWidget({
widget,
config,
}: WidgetComponentProps<SystemOverviewConfig>) {
const { t } = useTranslation();
const { showVersion, showDbHealth, showUptime } = config;
const [version, setVersion] = useState<string | null>(null);
const [updateAvailable, setUpdateAvailable] = useState(false);
const [dbStatus, setDbStatus] = useState<string | null>(null);
const [uptimeFormatted, setUptimeFormatted] = useState<string | null>(null);
const fetchData = async () => {
try {
if (showVersion) {
// `checkRemote=false` makes /version return early with just
// {localVersion, status: "update_check_disabled"}, and the response has
// no `updateAvailable` field in either mode -- so the previous read was
// false twice over. `status` is what the endpoint actually answers with,
// and what the dashboard and profile badges already read.
const info = await getVersionInfo();
setVersion((info?.localVersion as string) ?? null);
setUpdateAvailable(info?.status === "requires_update");
}
if (showDbHealth) {
const health = await getDatabaseHealth();
setDbStatus((health?.status as string) ?? "unknown");
}
if (showUptime) {
const up = await getUptime();
setUptimeFormatted(up.formatted);
}
} catch {
/* ignore */
}
};
useEffect(() => {
fetchData();
return runVisibleInterval(() => {
void fetchData();
}, 300_000);
}, [showVersion, showDbHealth, showUptime]);
const accent =
getComputedStyle(document.documentElement)
.getPropertyValue("--accent-brand")
.trim() || "#f59145";
return (
<div className="flex flex-col w-full h-full overflow-hidden">
<WidgetTitle
title={widget.title}
icon={<Server size={11} />}
fallback="Termix"
/>
<div className="flex flex-col gap-1.5 p-3 flex-1 overflow-auto">
{showVersion && (
<InfoRow
label={t("homepage.overviewVersion")}
value={version ?? t("common.loading")}
valueColor={updateAvailable ? "#f97316" : undefined}
/>
)}
{showVersion && updateAvailable && (
<InfoRow
label={t("homepage.overviewUpdateLabel")}
value={t("homepage.overviewUpdateAvailable")}
valueColor="#f97316"
/>
)}
{showDbHealth && (
<InfoRow
label={t("homepage.overviewDatabase")}
value={dbStatus ? dbStatus.toUpperCase() : t("common.loading")}
valueColor={
dbStatus === "ok" || dbStatus === "healthy"
? accent
: dbStatus
? "#ef4444"
: undefined
}
/>
)}
{showUptime && (
<InfoRow
label={t("homepage.overviewUptime")}
value={uptimeFormatted ?? t("common.loading")}
/>
)}
</div>
</div>
);
}
registerWidget<SystemOverviewConfig>({
id: "system_overview",
name: "System Overview",
description: "Termix version, database health, and uptime at a glance",
category: "system",
icon: <Server size={14} />,
defaultConfig: { showVersion: true, showDbHealth: true, showUptime: true },
defaultSize: { w: GRID_SIZE * 10, h: GRID_SIZE * 6 },
minSize: { w: GRID_SIZE * 4, h: GRID_SIZE * 3 },
component: SystemOverviewWidget,
});
export { SystemOverviewWidget };