style: auto-fix lint and format
This commit is contained in:
1 parent
5eebfb7719
commit
515b285ef7
14 files changed
+370
-223
No files matched your search
@@ -613,7 +613,7 @@ export const boardRouter = createTRPCRouter({
|
||||
boardId: board.id,
|
||||
});
|
||||
|
||||
const sortedLayouts = board.layouts.sort((layoutA, layoutB) => layoutA.columnCount - layoutB.columnCount);
|
||||
const sortedLayouts = board.layouts.toSorted((layoutA, layoutB) => layoutA.columnCount - layoutB.columnCount);
|
||||
// Fallback to biggest if none exists with columnCount bigger than addedLayout.columnCount
|
||||
const layoutToClone =
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
@@ -1215,7 +1215,7 @@ export const boardRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
return {
|
||||
inherited: dbGroupPermissions.sort((permissionA, permissionB) => {
|
||||
inherited: dbGroupPermissions.toSorted((permissionA, permissionB) => {
|
||||
return permissionA.group.name.localeCompare(permissionB.group.name);
|
||||
}),
|
||||
users: userPermissions
|
||||
@@ -1223,7 +1223,7 @@ export const boardRouter = createTRPCRouter({
|
||||
user,
|
||||
permission,
|
||||
}))
|
||||
.sort((permissionA, permissionB) => {
|
||||
.toSorted((permissionA, permissionB) => {
|
||||
return (permissionA.user.name ?? "").localeCompare(permissionB.user.name ?? "");
|
||||
}),
|
||||
groups: dbGroupBoardPermission
|
||||
@@ -1234,7 +1234,7 @@ export const boardRouter = createTRPCRouter({
|
||||
},
|
||||
permission,
|
||||
}))
|
||||
.sort((permissionA, permissionB) => {
|
||||
.toSorted((permissionA, permissionB) => {
|
||||
return permissionA.group.name.localeCompare(permissionB.group.name);
|
||||
}),
|
||||
};
|
||||
@@ -1586,7 +1586,7 @@ const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, use
|
||||
...otherBoardProperties,
|
||||
layouts: layouts
|
||||
.map(({ boardId: _, ...layout }) => layout)
|
||||
.sort((layoutA, layoutB) => layoutA.breakpoint - layoutB.breakpoint),
|
||||
.toSorted((layoutA, layoutB) => layoutA.breakpoint - layoutB.breakpoint),
|
||||
sections: sections.map(({ collapseStates, ...section }) =>
|
||||
parseSection({
|
||||
...section,
|
||||
|
||||
@@ -5,33 +5,29 @@ import {
|
||||
beszelAlertsRequestHandler,
|
||||
beszelStatsRequestHandler,
|
||||
beszelSystemsRequestHandler,
|
||||
type BeszelAlertsData,
|
||||
type BeszelStatsData,
|
||||
type BeszelSystemRow,
|
||||
} from "@homarr/request-handler/beszel";
|
||||
import type { BeszelAlertsData, BeszelStatsData, BeszelSystemRow } from "@homarr/request-handler/beszel";
|
||||
|
||||
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const beszelRouter = createTRPCRouter({
|
||||
getSystems: publicProcedure
|
||||
.concat(createManyIntegrationMiddleware("query", "beszel"))
|
||||
.query(async ({ ctx }) => {
|
||||
const results = await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
const innerHandler = beszelSystemsRequestHandler.handler(integration, {});
|
||||
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
systems: data,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
}),
|
||||
);
|
||||
return results;
|
||||
}),
|
||||
getSystems: publicProcedure.concat(createManyIntegrationMiddleware("query", "beszel")).query(async ({ ctx }) => {
|
||||
const results = await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
const innerHandler = beszelSystemsRequestHandler.handler(integration, {});
|
||||
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
systems: data,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
}),
|
||||
);
|
||||
return results;
|
||||
}),
|
||||
|
||||
subscribeSystems: publicProcedure
|
||||
.concat(createManyIntegrationMiddleware("query", "beszel"))
|
||||
|
||||
@@ -44,17 +44,14 @@ export class BeszelIntegration extends Integration {
|
||||
return existingSession;
|
||||
}
|
||||
|
||||
const response = await fetchWithTrustedCertificatesAsync(
|
||||
this.url("/api/collections/users/auth-with-password"),
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
identity: this.getSecretValue("username"),
|
||||
password: this.getSecretValue("password"),
|
||||
}),
|
||||
},
|
||||
);
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/collections/users/auth-with-password"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
identity: this.getSecretValue("username"),
|
||||
password: this.getSecretValue("password"),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ResponseError(response);
|
||||
@@ -66,10 +63,7 @@ export class BeszelIntegration extends Integration {
|
||||
return session;
|
||||
}
|
||||
|
||||
private async fetchWithAuthAsync(
|
||||
url: URL,
|
||||
options: { method?: string; body?: string } = {},
|
||||
) {
|
||||
private async fetchWithAuthAsync(url: URL, options: { method?: string; body?: string } = {}) {
|
||||
let session = await this.authenticateAsync();
|
||||
|
||||
const doFetch = (token: string) =>
|
||||
@@ -97,8 +91,6 @@ export class BeszelIntegration extends Integration {
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async getSystemsAsync(): Promise<BeszelSystem[]> {
|
||||
const response = await this.fetchWithAuthAsync(
|
||||
this.url("/api/collections/systems/records", { perPage: "500", sort: "-updated" }),
|
||||
@@ -114,11 +106,7 @@ export class BeszelIntegration extends Integration {
|
||||
return (await response.json()) as BeszelSystemDetails;
|
||||
}
|
||||
|
||||
public async getSystemStatsAsync(
|
||||
systemId: string,
|
||||
type = "1m",
|
||||
perPage = 60,
|
||||
): Promise<BeszelSystemStatsRecord[]> {
|
||||
public async getSystemStatsAsync(systemId: string, type = "1m", perPage = 60): Promise<BeszelSystemStatsRecord[]> {
|
||||
const filter = `system='${escapeFilterValue(systemId)}' && type='${escapeFilterValue(type)}'`;
|
||||
const response = await this.fetchWithAuthAsync(
|
||||
this.url("/api/collections/system_stats/records", {
|
||||
@@ -186,9 +174,7 @@ export class BeszelIntegration extends Integration {
|
||||
if (systemId) {
|
||||
params.filter = `system='${escapeFilterValue(systemId)}'`;
|
||||
}
|
||||
const response = await this.fetchWithAuthAsync(
|
||||
this.url("/api/collections/alerts/records", params),
|
||||
);
|
||||
const response = await this.fetchWithAuthAsync(this.url("/api/collections/alerts/records", params));
|
||||
const data = (await response.json()) as PocketBaseListResponse<BeszelAlert>;
|
||||
return data.items;
|
||||
}
|
||||
@@ -201,29 +187,24 @@ export class BeszelIntegration extends Integration {
|
||||
if (systemId) {
|
||||
params.filter = `system='${escapeFilterValue(systemId)}'`;
|
||||
}
|
||||
const response = await this.fetchWithAuthAsync(
|
||||
this.url("/api/collections/alerts_history/records", params),
|
||||
);
|
||||
const response = await this.fetchWithAuthAsync(this.url("/api/collections/alerts_history/records", params));
|
||||
const data = (await response.json()) as PocketBaseListResponse<BeszelAlertHistory>;
|
||||
return data.items;
|
||||
}
|
||||
|
||||
public async createAlertAsync(systemId: string, input: CreateAlertInput): Promise<BeszelAlert> {
|
||||
const session = await this.authenticateAsync();
|
||||
const response = await this.fetchWithAuthAsync(
|
||||
this.url("/api/collections/alerts/records"),
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
system: systemId,
|
||||
user: session.userId,
|
||||
name: input.name,
|
||||
value: input.value,
|
||||
min: input.min ?? 0,
|
||||
triggered: false,
|
||||
}),
|
||||
},
|
||||
);
|
||||
const response = await this.fetchWithAuthAsync(this.url("/api/collections/alerts/records"), {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
system: systemId,
|
||||
user: session.userId,
|
||||
name: input.name,
|
||||
value: input.value,
|
||||
min: input.min ?? 0,
|
||||
triggered: false,
|
||||
}),
|
||||
});
|
||||
return (await response.json()) as BeszelAlert;
|
||||
}
|
||||
|
||||
@@ -239,37 +220,29 @@ export class BeszelIntegration extends Integration {
|
||||
}
|
||||
|
||||
public async deleteAlertAsync(alertId: string): Promise<void> {
|
||||
await this.fetchWithAuthAsync(
|
||||
this.url(`/api/collections/alerts/records/${alertId}` as `/${string}`),
|
||||
{ method: "DELETE" },
|
||||
);
|
||||
await this.fetchWithAuthAsync(this.url(`/api/collections/alerts/records/${alertId}` as `/${string}`), {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
public async pauseSystemAsync(systemId: string): Promise<void> {
|
||||
await this.fetchWithAuthAsync(
|
||||
this.url(`/api/collections/systems/records/${systemId}` as `/${string}`),
|
||||
{
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status: "paused" }),
|
||||
},
|
||||
);
|
||||
await this.fetchWithAuthAsync(this.url(`/api/collections/systems/records/${systemId}` as `/${string}`), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status: "paused" }),
|
||||
});
|
||||
}
|
||||
|
||||
public async resumeSystemAsync(systemId: string): Promise<void> {
|
||||
await this.fetchWithAuthAsync(
|
||||
this.url(`/api/collections/systems/records/${systemId}` as `/${string}`),
|
||||
{
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status: "pending" }),
|
||||
},
|
||||
);
|
||||
await this.fetchWithAuthAsync(this.url(`/api/collections/systems/records/${systemId}` as `/${string}`), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status: "pending" }),
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteSystemAsync(systemId: string): Promise<void> {
|
||||
await this.fetchWithAuthAsync(
|
||||
this.url(`/api/collections/systems/records/${systemId}` as `/${string}`),
|
||||
{ method: "DELETE" },
|
||||
);
|
||||
await this.fetchWithAuthAsync(this.url(`/api/collections/systems/records/${systemId}` as `/${string}`), {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
|
||||
@@ -144,7 +144,6 @@ describe.skipIf(!shouldRun)("BeszelIntegration (real instance)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("alert CRUD", () => {
|
||||
let alertId: string;
|
||||
|
||||
|
||||
@@ -60,7 +60,6 @@ export const beszelSystemsRequestHandler = createCachedIntegrationRequestHandler
|
||||
queryKey: "beszelSystems",
|
||||
});
|
||||
|
||||
|
||||
export interface BeszelAlertsData {
|
||||
alerts: BeszelAlert[];
|
||||
history: BeszelAlertHistory[];
|
||||
@@ -74,9 +73,7 @@ export const beszelAlertsRequestHandler = createCachedIntegrationRequestHandler<
|
||||
async requestAsync(integration, input) {
|
||||
const instance = await createIntegrationAsync(integration);
|
||||
const alerts = await instance.getAlertsAsync();
|
||||
const history = input.includeHistory
|
||||
? await instance.getAlertHistoryAsync(undefined, input.maxHistoryItems)
|
||||
: [];
|
||||
const history = input.includeHistory ? await instance.getAlertHistoryAsync(undefined, input.maxHistoryItems) : [];
|
||||
return { alerts, history };
|
||||
},
|
||||
cacheDuration: dayjs.duration(15, "seconds"),
|
||||
|
||||
@@ -6,17 +6,7 @@ import { IconBellOff, IconCircleCheck, IconFlame, IconHistory } from "@tabler/ic
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
Activity,
|
||||
Battery,
|
||||
Cpu,
|
||||
HardDrive,
|
||||
MemoryStick,
|
||||
Monitor,
|
||||
Network,
|
||||
Server,
|
||||
Thermometer,
|
||||
} from "lucide-react";
|
||||
import { Activity, Battery, Cpu, HardDrive, MemoryStick, Monitor, Network, Server, Thermometer } from "lucide-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
@@ -90,7 +80,9 @@ export default function BeszelAlertsWidget({
|
||||
|
||||
const history = useMemo(() => {
|
||||
const all = results.flatMap((r) => r.history.map((h) => ({ ...h, _key: `${r.integrationId}:${h.id}` })));
|
||||
return all.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime()).slice(0, options.maxHistoryItems);
|
||||
return all
|
||||
.toSorted((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime())
|
||||
.slice(0, options.maxHistoryItems);
|
||||
}, [results, options.maxHistoryItems]);
|
||||
|
||||
const triggeredAlerts = alerts.filter((a) => a.triggered);
|
||||
@@ -104,7 +96,9 @@ export default function BeszelAlertsWidget({
|
||||
<ThemeIcon variant="light" color="gray" size="lg" radius="xl">
|
||||
<IconBellOff size={18} />
|
||||
</ThemeIcon>
|
||||
<Text size="sm" c="dimmed">{t("empty")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("empty")}
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
@@ -245,9 +239,7 @@ function AlertRow({ name, value, min, systemName, triggered }: AlertRowProps) {
|
||||
style={{
|
||||
borderRadius: "var(--mantine-radius-sm)",
|
||||
borderLeft: `3px solid var(--mantine-color-${triggered ? "red" : "green"}-6)`,
|
||||
backgroundColor: triggered
|
||||
? "var(--mantine-color-red-light)"
|
||||
: "var(--mantine-color-default-hover)",
|
||||
backgroundColor: triggered ? "var(--mantine-color-red-light)" : "var(--mantine-color-default-hover)",
|
||||
}}
|
||||
>
|
||||
<Icon size={14} opacity={0.7} style={{ flexShrink: 0 }} />
|
||||
@@ -256,7 +248,9 @@ function AlertRow({ name, value, min, systemName, triggered }: AlertRowProps) {
|
||||
<Text size="xs" fw={600} truncate>
|
||||
{systemName}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">·</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
·
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" style={{ whiteSpace: "nowrap" }}>
|
||||
{name}
|
||||
</Text>
|
||||
|
||||
@@ -38,9 +38,51 @@ interface SizeConfig {
|
||||
}
|
||||
|
||||
const sizeThresholds: [number, SizeConfig][] = [
|
||||
[0, { iconSize: 10, fontSize: "10px", progressSize: "xs", labelMiw: 40, valueMiw: 30, rowHeight: 18, cardPadding: 4, badgeHeight: 20, badgeSize: "xs", gap: 2 }],
|
||||
[160, { iconSize: 12, fontSize: "xs", progressSize: "xs", labelMiw: 50, valueMiw: 34, rowHeight: 22, cardPadding: 6, badgeHeight: 22, badgeSize: "xs", gap: 6 }],
|
||||
[280, { iconSize: 14, fontSize: "xs", progressSize: "sm", labelMiw: 55, valueMiw: 38, rowHeight: 24, cardPadding: 8, badgeHeight: 26, badgeSize: "sm", gap: 8 }],
|
||||
[
|
||||
0,
|
||||
{
|
||||
iconSize: 10,
|
||||
fontSize: "10px",
|
||||
progressSize: "xs",
|
||||
labelMiw: 40,
|
||||
valueMiw: 30,
|
||||
rowHeight: 18,
|
||||
cardPadding: 4,
|
||||
badgeHeight: 20,
|
||||
badgeSize: "xs",
|
||||
gap: 2,
|
||||
},
|
||||
],
|
||||
[
|
||||
160,
|
||||
{
|
||||
iconSize: 12,
|
||||
fontSize: "xs",
|
||||
progressSize: "xs",
|
||||
labelMiw: 50,
|
||||
valueMiw: 34,
|
||||
rowHeight: 22,
|
||||
cardPadding: 6,
|
||||
badgeHeight: 22,
|
||||
badgeSize: "xs",
|
||||
gap: 6,
|
||||
},
|
||||
],
|
||||
[
|
||||
280,
|
||||
{
|
||||
iconSize: 14,
|
||||
fontSize: "xs",
|
||||
progressSize: "sm",
|
||||
labelMiw: 55,
|
||||
valueMiw: 38,
|
||||
rowHeight: 24,
|
||||
cardPadding: 8,
|
||||
badgeHeight: 26,
|
||||
badgeSize: "sm",
|
||||
gap: 8,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
const getSizeConfig = (cellWidth: number, cellHeight: number): SizeConfig => {
|
||||
@@ -85,15 +127,21 @@ interface MetricRowProps {
|
||||
|
||||
const MetricValue = ({ value, progress, size }: Pick<MetricRowProps, "value" | "progress" | "size">) => (
|
||||
<Group gap={6} wrap="nowrap" style={{ flex: 1 }}>
|
||||
<Text size={size.fontSize} fw={500} miw={size.valueMiw} ta="right">{value}</Text>
|
||||
{progress && <Progress value={progress.value} color={progress.color} size={size.progressSize} style={{ flex: 1 }} />}
|
||||
<Text size={size.fontSize} fw={500} miw={size.valueMiw} ta="right">
|
||||
{value}
|
||||
</Text>
|
||||
{progress && (
|
||||
<Progress value={progress.value} color={progress.color} size={size.progressSize} style={{ flex: 1 }} />
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
|
||||
const MetricRow = ({ icon, label, value, progress, size }: MetricRowProps) => (
|
||||
<Group gap="xs" wrap="nowrap" style={{ minHeight: size.rowHeight }}>
|
||||
{icon}
|
||||
<Text size={size.fontSize} c="dimmed" miw={size.labelMiw}>{label}</Text>
|
||||
<Text size={size.fontSize} c="dimmed" miw={size.labelMiw}>
|
||||
{label}
|
||||
</Text>
|
||||
<MetricValue value={value} progress={progress} size={size} />
|
||||
</Group>
|
||||
);
|
||||
@@ -107,50 +155,170 @@ interface SystemCardProps {
|
||||
}
|
||||
|
||||
const metricRenderers = [
|
||||
{ key: "showCpu", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
<MetricRow key="cpu" icon={<Cpu size={sz.iconSize} />} label={t("metric.cpu")} value={formatPercent(s.cpu)} progress={{ value: s.cpu, color: thresholdColor(s.cpu) }} size={sz} />
|
||||
), visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showCpu },
|
||||
{ key: "showMemory", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
<MetricRow key="mem" icon={<MemoryStick size={sz.iconSize} />} label={t("metric.memory")} value={formatPercent(s.memory)} progress={{ value: s.memory, color: thresholdColor(s.memory) }} size={sz} />
|
||||
), visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showMemory },
|
||||
{ key: "showDisk", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
<MetricRow key="disk" icon={<HardDrive size={sz.iconSize} />} label={t("metric.disk")} value={formatPercent(s.disk)} progress={{ value: s.disk, color: thresholdColor(s.disk) }} size={sz} />
|
||||
), visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showDisk },
|
||||
{ key: "showGpu", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
<MetricRow key="gpu" icon={<Monitor size={sz.iconSize} />} label={t("metric.gpu")} value={formatPercent(s.gpu)} progress={{ value: s.gpu, color: thresholdColor(s.gpu) }} size={sz} />
|
||||
), visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showGpu && s.gpu > 0 },
|
||||
{ key: "showLoadAvg", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
<MetricRow key="la" icon={<Activity size={sz.iconSize} />} label={t("metric.loadAvg")} value={formatLoadAvg(s.loadAvg)} size={sz} />
|
||||
), visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showLoadAvg && s.loadAvg !== null },
|
||||
{ key: "showNet", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
<MetricRow key="net" icon={<Network size={sz.iconSize} />} label={t("metric.net")} value={formatByteRate(s.netBytes)} size={sz} />
|
||||
), visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showNet },
|
||||
{ key: "showTemp", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
<MetricRow key="temp" icon={<Thermometer size={sz.iconSize} />} label={t("metric.temp")} value={formatTemp(s.temp, false)} size={sz} />
|
||||
), visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showTemp && s.temp !== null },
|
||||
{ key: "showBattery", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => {
|
||||
const batteryIcons = [Battery, BatteryCharging] as const;
|
||||
const Icon = batteryIcons[Math.min(1, s.battery?.[1] ?? 0)]!;
|
||||
return <MetricRow key="bat" icon={<Icon size={sz.iconSize} />} label={t("metric.battery")} value={`${s.battery?.[0] ?? 0}%`} size={sz} />;
|
||||
}, visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showBattery && s.battery !== null },
|
||||
{ key: "showServices", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
<MetricRow key="svc" icon={<Server size={sz.iconSize} />} label={t("metric.services")} value={String(s.services)} size={sz} />
|
||||
), visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showServices },
|
||||
{ key: "showUptime", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
<MetricRow key="up" icon={<Activity size={sz.iconSize} />} label={t("metric.uptime")} value={formatUptime(s.uptime)} size={sz} />
|
||||
), visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showUptime },
|
||||
{ key: "showAgent", render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
<MetricRow key="agent" icon={<Wifi size={sz.iconSize} />} label={t("metric.agent")} value={s.agentVersion} size={sz} />
|
||||
), visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showAgent },
|
||||
{
|
||||
key: "showCpu",
|
||||
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
<MetricRow
|
||||
key="cpu"
|
||||
icon={<Cpu size={sz.iconSize} />}
|
||||
label={t("metric.cpu")}
|
||||
value={formatPercent(s.cpu)}
|
||||
progress={{ value: s.cpu, color: thresholdColor(s.cpu) }}
|
||||
size={sz}
|
||||
/>
|
||||
),
|
||||
visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showCpu,
|
||||
},
|
||||
{
|
||||
key: "showMemory",
|
||||
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
<MetricRow
|
||||
key="mem"
|
||||
icon={<MemoryStick size={sz.iconSize} />}
|
||||
label={t("metric.memory")}
|
||||
value={formatPercent(s.memory)}
|
||||
progress={{ value: s.memory, color: thresholdColor(s.memory) }}
|
||||
size={sz}
|
||||
/>
|
||||
),
|
||||
visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showMemory,
|
||||
},
|
||||
{
|
||||
key: "showDisk",
|
||||
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
<MetricRow
|
||||
key="disk"
|
||||
icon={<HardDrive size={sz.iconSize} />}
|
||||
label={t("metric.disk")}
|
||||
value={formatPercent(s.disk)}
|
||||
progress={{ value: s.disk, color: thresholdColor(s.disk) }}
|
||||
size={sz}
|
||||
/>
|
||||
),
|
||||
visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showDisk,
|
||||
},
|
||||
{
|
||||
key: "showGpu",
|
||||
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
<MetricRow
|
||||
key="gpu"
|
||||
icon={<Monitor size={sz.iconSize} />}
|
||||
label={t("metric.gpu")}
|
||||
value={formatPercent(s.gpu)}
|
||||
progress={{ value: s.gpu, color: thresholdColor(s.gpu) }}
|
||||
size={sz}
|
||||
/>
|
||||
),
|
||||
visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showGpu && s.gpu > 0,
|
||||
},
|
||||
{
|
||||
key: "showLoadAvg",
|
||||
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
<MetricRow
|
||||
key="la"
|
||||
icon={<Activity size={sz.iconSize} />}
|
||||
label={t("metric.loadAvg")}
|
||||
value={formatLoadAvg(s.loadAvg)}
|
||||
size={sz}
|
||||
/>
|
||||
),
|
||||
visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showLoadAvg && s.loadAvg !== null,
|
||||
},
|
||||
{
|
||||
key: "showNet",
|
||||
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
<MetricRow
|
||||
key="net"
|
||||
icon={<Network size={sz.iconSize} />}
|
||||
label={t("metric.net")}
|
||||
value={formatByteRate(s.netBytes)}
|
||||
size={sz}
|
||||
/>
|
||||
),
|
||||
visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showNet,
|
||||
},
|
||||
{
|
||||
key: "showTemp",
|
||||
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
<MetricRow
|
||||
key="temp"
|
||||
icon={<Thermometer size={sz.iconSize} />}
|
||||
label={t("metric.temp")}
|
||||
value={formatTemp(s.temp, false)}
|
||||
size={sz}
|
||||
/>
|
||||
),
|
||||
visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showTemp && s.temp !== null,
|
||||
},
|
||||
{
|
||||
key: "showBattery",
|
||||
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => {
|
||||
const batteryIcons = [Battery, BatteryCharging] as const;
|
||||
const Icon = batteryIcons[Math.min(1, s.battery?.[1] ?? 0)]!;
|
||||
return (
|
||||
<MetricRow
|
||||
key="bat"
|
||||
icon={<Icon size={sz.iconSize} />}
|
||||
label={t("metric.battery")}
|
||||
value={`${s.battery?.[0] ?? 0}%`}
|
||||
size={sz}
|
||||
/>
|
||||
);
|
||||
},
|
||||
visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showBattery && s.battery !== null,
|
||||
},
|
||||
{
|
||||
key: "showServices",
|
||||
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
<MetricRow
|
||||
key="svc"
|
||||
icon={<Server size={sz.iconSize} />}
|
||||
label={t("metric.services")}
|
||||
value={String(s.services)}
|
||||
size={sz}
|
||||
/>
|
||||
),
|
||||
visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showServices,
|
||||
},
|
||||
{
|
||||
key: "showUptime",
|
||||
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
<MetricRow
|
||||
key="up"
|
||||
icon={<Activity size={sz.iconSize} />}
|
||||
label={t("metric.uptime")}
|
||||
value={formatUptime(s.uptime)}
|
||||
size={sz}
|
||||
/>
|
||||
),
|
||||
visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showUptime,
|
||||
},
|
||||
{
|
||||
key: "showAgent",
|
||||
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
<MetricRow
|
||||
key="agent"
|
||||
icon={<Wifi size={sz.iconSize} />}
|
||||
label={t("metric.agent")}
|
||||
value={s.agentVersion}
|
||||
size={sz}
|
||||
/>
|
||||
),
|
||||
visible: (s: BeszelSystemRow, o: SystemCardProps["options"]) => o.showAgent,
|
||||
},
|
||||
] as const;
|
||||
|
||||
const SystemCard = ({ system, options, t, size, maxMetrics }: SystemCardProps) => {
|
||||
const visibleMetrics = metricRenderers
|
||||
.filter((m) => m.visible(system, options))
|
||||
.slice(0, maxMetrics);
|
||||
const visibleMetrics = metricRenderers.filter((m) => m.visible(system, options)).slice(0, maxMetrics);
|
||||
|
||||
return (
|
||||
<Card padding={size.cardPadding} radius="sm" withBorder h="100%" style={{ overflow: "hidden", display: "flex", flexDirection: "column" as const }}>
|
||||
<Card
|
||||
padding={size.cardPadding}
|
||||
radius="sm"
|
||||
withBorder
|
||||
h="100%"
|
||||
style={{ overflow: "hidden", display: "flex", flexDirection: "column" as const }}
|
||||
>
|
||||
<Group gap="xs" mb={2}>
|
||||
<Badge
|
||||
size={size.badgeSize}
|
||||
|
||||
@@ -9,21 +9,19 @@ export const { definition, componentLoader } = createWidgetDefinition("beszelSys
|
||||
supportedIntegrations: ["beszel"],
|
||||
integrationsRequired: true,
|
||||
createOptions() {
|
||||
return optionsBuilder.from(
|
||||
(factory) => ({
|
||||
statusFilter: factory.select({ defaultValue: "all", options: statusOptions }),
|
||||
showCpu: factory.switch({ defaultValue: true }),
|
||||
showMemory: factory.switch({ defaultValue: true }),
|
||||
showDisk: factory.switch({ defaultValue: true }),
|
||||
showGpu: factory.switch({ defaultValue: true }),
|
||||
showLoadAvg: factory.switch({ defaultValue: true }),
|
||||
showNet: factory.switch({ defaultValue: true }),
|
||||
showTemp: factory.switch({ defaultValue: true }),
|
||||
showBattery: factory.switch({ defaultValue: true }),
|
||||
showServices: factory.switch({ defaultValue: true }),
|
||||
showUptime: factory.switch({ defaultValue: true }),
|
||||
showAgent: factory.switch({ defaultValue: true }),
|
||||
}),
|
||||
);
|
||||
return optionsBuilder.from((factory) => ({
|
||||
statusFilter: factory.select({ defaultValue: "all", options: statusOptions }),
|
||||
showCpu: factory.switch({ defaultValue: true }),
|
||||
showMemory: factory.switch({ defaultValue: true }),
|
||||
showDisk: factory.switch({ defaultValue: true }),
|
||||
showGpu: factory.switch({ defaultValue: true }),
|
||||
showLoadAvg: factory.switch({ defaultValue: true }),
|
||||
showNet: factory.switch({ defaultValue: true }),
|
||||
showTemp: factory.switch({ defaultValue: true }),
|
||||
showBattery: factory.switch({ defaultValue: true }),
|
||||
showServices: factory.switch({ defaultValue: true }),
|
||||
showUptime: factory.switch({ defaultValue: true }),
|
||||
showAgent: factory.switch({ defaultValue: true }),
|
||||
}));
|
||||
},
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
@@ -60,19 +60,28 @@ export default function BeszelSystemStatsWidget({
|
||||
const BYTES_TO_GB = 1024 * 1024 * 1024;
|
||||
|
||||
const cpuMap = useCallback((s: { cpu: number }) => ({ [t("chart.cpu.series")]: s.cpu }), [t]);
|
||||
const memoryMap = useCallback((s: { mu: number; mb: number }) => ({
|
||||
[t("chart.memory.series")]: s.mu / BYTES_TO_GB,
|
||||
[t("chart.memory.cache")]: (s.mb ?? 0) / BYTES_TO_GB,
|
||||
}), [t]);
|
||||
const memoryMap = useCallback(
|
||||
(s: { mu: number; mb: number }) => ({
|
||||
[t("chart.memory.series")]: s.mu / BYTES_TO_GB,
|
||||
[t("chart.memory.cache")]: (s.mb ?? 0) / BYTES_TO_GB,
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
const diskMap = useCallback((s: { du: number }) => ({ [t("chart.disk.series")]: s.du / BYTES_TO_GB }), [t]);
|
||||
const diskIOMap = useCallback((s: { dr?: number; dw?: number }) => ({
|
||||
[t("chart.diskIO.read")]: s.dr ?? 0,
|
||||
[t("chart.diskIO.write")]: s.dw ?? 0,
|
||||
}), [t]);
|
||||
const networkMap = useCallback((s: { ns: number; nr: number }) => ({
|
||||
[t("chart.network.sent")]: s.ns,
|
||||
[t("chart.network.recv")]: s.nr,
|
||||
}), [t]);
|
||||
const diskIOMap = useCallback(
|
||||
(s: { dr?: number; dw?: number }) => ({
|
||||
[t("chart.diskIO.read")]: s.dr ?? 0,
|
||||
[t("chart.diskIO.write")]: s.dw ?? 0,
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
const networkMap = useCallback(
|
||||
(s: { ns: number; nr: number }) => ({
|
||||
[t("chart.network.sent")]: s.ns,
|
||||
[t("chart.network.recv")]: s.nr,
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
const cpuData = useSystemChartData(statsResult?.systemStats, cpuMap);
|
||||
const memoryData = useSystemChartData(statsResult?.systemStats, memoryMap);
|
||||
|
||||
@@ -68,7 +68,7 @@ export default function BeszelSystemTableWidget({
|
||||
const sortedSystems = useMemo(() => {
|
||||
const accessor = sortStatus.columnAccessor as keyof BeszelSystemRow;
|
||||
const dir = directionMultiplier[sortStatus.direction] ?? 1;
|
||||
return [...filteredSystems].sort((a, b) => {
|
||||
return [...filteredSystems].toSorted((a, b) => {
|
||||
const aVal = a[accessor] ?? 0;
|
||||
const bVal = b[accessor] ?? 0;
|
||||
if (typeof aVal === "string" && typeof bVal === "string") return aVal.localeCompare(bVal) * dir;
|
||||
@@ -161,10 +161,7 @@ export default function BeszelSystemTableWidget({
|
||||
sortable: true,
|
||||
render: (record) => (
|
||||
<Group gap={8} wrap="nowrap">
|
||||
<Indicator
|
||||
color={record.loadAvg ? loadAvgColor(record.loadAvg[0], record.cores) : "gray"}
|
||||
size={7}
|
||||
/>
|
||||
<Indicator color={record.loadAvg ? loadAvgColor(record.loadAvg[0], record.cores) : "gray"} size={7} />
|
||||
<Text size={size.fontSize}>{formatLoadAvg(record.loadAvg)}</Text>
|
||||
</Group>
|
||||
),
|
||||
@@ -203,9 +200,7 @@ export default function BeszelSystemTableWidget({
|
||||
<Text inherit>{t("column.battery")}</Text>
|
||||
</Group>
|
||||
),
|
||||
render: (record) => (
|
||||
<Text size={size.fontSize}>{record.battery ? `${record.battery[0]}%` : "—"}</Text>
|
||||
),
|
||||
render: (record) => <Text size={size.fontSize}>{record.battery ? `${record.battery[0]}%` : "—"}</Text>,
|
||||
},
|
||||
options.showServices && {
|
||||
accessor: "services",
|
||||
|
||||
@@ -28,23 +28,21 @@ export const { definition, componentLoader } = createWidgetDefinition("beszelSys
|
||||
supportedIntegrations: ["beszel"],
|
||||
integrationsRequired: true,
|
||||
createOptions() {
|
||||
return optionsBuilder.from(
|
||||
(factory) => ({
|
||||
statusFilter: factory.select({ defaultValue: "all", options: statusOptions }),
|
||||
sortBy: factory.select({ defaultValue: "name", options: sortOptions }),
|
||||
sortDirection: factory.select({ defaultValue: "asc", options: sortDirectionOptions }),
|
||||
showCpu: factory.switch({ defaultValue: true }),
|
||||
showMemory: factory.switch({ defaultValue: true }),
|
||||
showDisk: factory.switch({ defaultValue: true }),
|
||||
showGpu: factory.switch({ defaultValue: true }),
|
||||
showLoadAvg: factory.switch({ defaultValue: true }),
|
||||
showNet: factory.switch({ defaultValue: true }),
|
||||
showTemp: factory.switch({ defaultValue: true }),
|
||||
showBattery: factory.switch({ defaultValue: true }),
|
||||
showServices: factory.switch({ defaultValue: true }),
|
||||
showUptime: factory.switch({ defaultValue: true }),
|
||||
showAgent: factory.switch({ defaultValue: true }),
|
||||
}),
|
||||
);
|
||||
return optionsBuilder.from((factory) => ({
|
||||
statusFilter: factory.select({ defaultValue: "all", options: statusOptions }),
|
||||
sortBy: factory.select({ defaultValue: "name", options: sortOptions }),
|
||||
sortDirection: factory.select({ defaultValue: "asc", options: sortDirectionOptions }),
|
||||
showCpu: factory.switch({ defaultValue: true }),
|
||||
showMemory: factory.switch({ defaultValue: true }),
|
||||
showDisk: factory.switch({ defaultValue: true }),
|
||||
showGpu: factory.switch({ defaultValue: true }),
|
||||
showLoadAvg: factory.switch({ defaultValue: true }),
|
||||
showNet: factory.switch({ defaultValue: true }),
|
||||
showTemp: factory.switch({ defaultValue: true }),
|
||||
showBattery: factory.switch({ defaultValue: true }),
|
||||
showServices: factory.switch({ defaultValue: true }),
|
||||
showUptime: factory.switch({ defaultValue: true }),
|
||||
showAgent: factory.switch({ defaultValue: true }),
|
||||
}));
|
||||
},
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
@@ -8,7 +8,6 @@ import dayjs from "dayjs";
|
||||
|
||||
import type { BeszelContainerStatsRecord, BeszelSystemStatsRecord } from "@homarr/integrations/types";
|
||||
|
||||
|
||||
const formatTime = (timestamp: string) => dayjs(timestamp).format("HH:mm");
|
||||
|
||||
const yAxisBase = { tickMargin: 0, tick: { fontSize: 10 } } as const;
|
||||
@@ -22,8 +21,14 @@ interface ChartPanelProps {
|
||||
export const ChartPanel = ({ title, subtitle, children }: ChartPanelProps) => (
|
||||
<Stack gap={4}>
|
||||
<Group gap="xs">
|
||||
<Text size="sm" fw={600}>{title}</Text>
|
||||
{subtitle && <Text size="xs" c="dimmed">{subtitle}</Text>}
|
||||
<Text size="sm" fw={600}>
|
||||
{title}
|
||||
</Text>
|
||||
{subtitle && (
|
||||
<Text size="xs" c="dimmed">
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
{children}
|
||||
</Stack>
|
||||
@@ -41,7 +46,12 @@ export const BeszelAreaChart = ({ yAxisFormatter, yAxisDomain, yAxisProps, ...pr
|
||||
withDots={false}
|
||||
withXAxis
|
||||
withYAxis
|
||||
yAxisProps={{ ...yAxisBase, ...(yAxisDomain ? { domain: yAxisDomain } : {}), tickFormatter: yAxisFormatter, ...yAxisProps }}
|
||||
yAxisProps={{
|
||||
...yAxisBase,
|
||||
...(yAxisDomain ? { domain: yAxisDomain } : {}),
|
||||
tickFormatter: yAxisFormatter,
|
||||
...yAxisProps,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -52,7 +62,7 @@ export const useSystemChartData = (
|
||||
) =>
|
||||
useMemo(() => {
|
||||
if (!systemStats) return [];
|
||||
return [...systemStats].reverse().map((r) => ({
|
||||
return [...systemStats].toReversed().map((r) => ({
|
||||
time: formatTime(r.created),
|
||||
...mapFn(r.stats),
|
||||
}));
|
||||
@@ -86,7 +96,7 @@ export const useDockerChartData = (
|
||||
useMemo(() => {
|
||||
if (!containerStats?.length) return [];
|
||||
const extract = defaultContainerExtractors[metric]!;
|
||||
return [...containerStats].reverse().map((record) => {
|
||||
return [...containerStats].toReversed().map((record) => {
|
||||
const point: Record<string, unknown> = { time: formatTime(record.created) };
|
||||
for (const name of containerNames) {
|
||||
point[name] = extract(record.stats.find((c) => c.n === name));
|
||||
|
||||
@@ -26,7 +26,19 @@ export const alertColorMap: Record<string, string> = {
|
||||
};
|
||||
|
||||
export const containerColors = [
|
||||
"blue.6", "teal.6", "orange.6", "grape.6", "cyan.6",
|
||||
"pink.6", "lime.6", "violet.6", "indigo.6", "yellow.6",
|
||||
"red.6", "green.6", "blue.4", "teal.4", "orange.4",
|
||||
"blue.6",
|
||||
"teal.6",
|
||||
"orange.6",
|
||||
"grape.6",
|
||||
"cyan.6",
|
||||
"pink.6",
|
||||
"lime.6",
|
||||
"violet.6",
|
||||
"indigo.6",
|
||||
"yellow.6",
|
||||
"red.6",
|
||||
"green.6",
|
||||
"blue.4",
|
||||
"teal.4",
|
||||
"orange.4",
|
||||
];
|
||||
@@ -78,9 +78,7 @@ const PortalTooltipContent = ({ active, label, payload, formatter, showTotal }:
|
||||
|
||||
if (!isActive) return null;
|
||||
|
||||
const sorted = [...(payload ?? [])]
|
||||
.filter((p) => p.value > 0)
|
||||
.sort((a, b) => b.value - a.value);
|
||||
const sorted = [...(payload ?? [])].filter((p) => p.value > 0).toSorted((a, b) => b.value - a.value);
|
||||
|
||||
if (!sorted.length) return null;
|
||||
|
||||
|
||||
Reference in new issue
Block a user