Update glances, requires changing the useWidgetAPI hook

This commit is contained in:
shamoon
2026-08-21 12:33:35 -07:00
parent dd6a7efee3
commit c9c5204de5
10 changed files with 190 additions and 130 deletions
+6 -6
View File
@@ -2,13 +2,13 @@ import useSWR from "swr";
import { formatProxyUrl } from "./api-helpers";
export default function useWidgetAPI(widget, ...options) {
const config = {};
if (options && options[1]?.refreshInterval) {
config.refreshInterval = options[1].refreshInterval;
export default function useWidgetAPI(widget, endpoint, queryParams, swrConfig = {}) {
const config = { ...swrConfig };
if (queryParams?.refreshInterval) {
config.refreshInterval = queryParams.refreshInterval;
}
let url = formatProxyUrl(widget, ...options);
if (options[0] === "") {
let url = formatProxyUrl(widget, endpoint, queryParams);
if (endpoint === "") {
url = null;
}
const { data, error, mutate } = useSWR(url, config);
+12
View File
@@ -28,6 +28,18 @@ describe("utils/proxy/use-widget-api", () => {
expect(result.mutate).toBe("m");
});
it("passes additional SWR configuration separately from the proxy query", () => {
useSWR.mockReturnValue({ data: undefined, error: undefined, mutate: vi.fn() });
const widget = { service_group: "g", service_name: "s", index: 0 };
const onSuccess = vi.fn();
useWidgetAPI(widget, "status", { refreshInterval: 123 }, { onSuccess });
const [url, config] = useSWR.mock.calls[0];
expect(url).not.toContain("onSuccess");
expect(config).toEqual({ refreshInterval: 123, onSuccess });
});
it("returns data.error as the top-level error", () => {
const dataError = { message: "nope" };
useSWR.mockReturnValue({ data: { error: dataError }, error: undefined, mutate: vi.fn() });
+19 -17
View File
@@ -1,10 +1,12 @@
import { useTranslation } from "next-i18next/pages";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
import { useCallback } from "react";
import Block from "../components/block";
import Container from "../components/container";
import useDataPoints from "./use-data-points";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
@@ -19,26 +21,26 @@ export default function Component({ service }) {
const { chart, refreshInterval = defaultInterval, pointsLimit = defaultPointsLimit, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
const [dataPoints, addDataPoint] = useDataPoints(pointsLimit, { value: 0 });
const { data, error } = useWidgetAPI(service.widget, `${apiVersion}/cpu`, {
refreshInterval: Math.max(defaultInterval, refreshInterval),
});
const handleData = useCallback(
(newData) => {
if (newData) addDataPoint({ value: newData.total });
},
[addDataPoint],
);
const { data, error } = useWidgetAPI(
service.widget,
`${apiVersion}/cpu`,
{
refreshInterval: Math.max(defaultInterval, refreshInterval),
},
{ onSuccess: handleData },
);
const { data: quicklookData, error: quicklookError } = useWidgetAPI(service.widget, `${apiVersion}/quicklook`);
useEffect(() => {
if (data) {
setDataPoints((prevDataPoints) => {
const newDataPoints = [...prevDataPoints, { value: data.total }];
if (newDataPoints.length > pointsLimit) {
newDataPoints.shift();
}
return newDataPoints;
});
}
}, [data, pointsLimit]);
if (error) {
return <Container error={error} widget={widget} />;
}
+27 -27
View File
@@ -1,10 +1,12 @@
import { useTranslation } from "next-i18next/pages";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
import { useCallback } from "react";
import Block from "../components/block";
import Container from "../components/container";
import useDataPoints from "./use-data-points";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
@@ -20,39 +22,37 @@ export default function Component({ service }) {
const apiVersion = parseVersionForUrl(version, 3);
const [, diskName] = widget.metric.split(":");
const [dataPoints, setDataPoints] = useState(
new Array(pointsLimit).fill({ read_bytes: 0, write_bytes: 0, time_since_update: 0 }, 0, pointsLimit),
);
const [ratePoints, setRatePoints] = useState(new Array(pointsLimit).fill({ a: 0, b: 0 }, 0, pointsLimit));
const { data, error } = useWidgetAPI(service.widget, `${apiVersion}/diskio`, {
refreshInterval: Math.max(defaultInterval, refreshInterval),
const [dataPoints, addDataPoint] = useDataPoints(pointsLimit, {
read_bytes: 0,
write_bytes: 0,
time_since_update: 0,
});
const handleData = useCallback(
(newData) => {
if (!newData?.error) {
const diskData = newData.find((item) => item.disk_name === diskName);
if (diskData) addDataPoint(diskData);
}
},
[addDataPoint, diskName],
);
const { data, error } = useWidgetAPI(
service.widget,
`${apiVersion}/diskio`,
{
refreshInterval: Math.max(defaultInterval, refreshInterval),
},
{ onSuccess: handleData },
);
const calculateRates = (d) =>
d.map((item) => ({
a: item.read_bytes / item.time_since_update,
b: item.write_bytes / item.time_since_update,
}));
useEffect(() => {
if (data && !data.error) {
const diskData = data.find((item) => item.disk_name === diskName);
setDataPoints((prevDataPoints) => {
const newDataPoints = [...prevDataPoints, diskData];
if (newDataPoints.length > pointsLimit) {
newDataPoints.shift();
}
return newDataPoints;
});
}
}, [data, diskName, pointsLimit]);
useEffect(() => {
setRatePoints(calculateRates(dataPoints));
}, [dataPoints]);
if (error || (data && data.error)) {
const finalError = error || data.error;
return <Container error={finalError} widget={widget} />;
@@ -83,7 +83,7 @@ export default function Component({ service }) {
<Container chart={chart}>
{chart && (
<ChartDual
dataPoints={ratePoints}
dataPoints={diskRates}
label={[t("glances.read"), t("glances.write")]}
max={diskData.critical}
formatter={(value) =>
+21 -20
View File
@@ -1,10 +1,12 @@
import { useTranslation } from "next-i18next/pages";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
import { useCallback } from "react";
import Block from "../components/block";
import Container from "../components/container";
import useDataPoints from "./use-data-points";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
@@ -20,27 +22,26 @@ export default function Component({ service }) {
const apiVersion = parseVersionForUrl(version, 3);
const [, gpuName] = widget.metric.split(":");
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ a: 0, b: 0 }, 0, pointsLimit));
const [dataPoints, addDataPoint] = useDataPoints(pointsLimit, { a: 0, b: 0 });
const { data, error } = useWidgetAPI(widget, `${apiVersion}/gpu`, {
refreshInterval: Math.max(defaultInterval, refreshInterval),
});
useEffect(() => {
if (data && !data.error) {
const gpuData = data.find((item) => item[item.key] == gpuName);
if (gpuData) {
setDataPoints((prevDataPoints) => {
const newDataPoints = [...prevDataPoints, { a: gpuData.mem, b: gpuData.proc }];
if (newDataPoints.length > pointsLimit) {
newDataPoints.shift();
}
return newDataPoints;
});
const handleData = useCallback(
(newData) => {
if (!newData?.error) {
const gpuData = newData.find((item) => item[item.key] == gpuName);
if (gpuData) addDataPoint({ a: gpuData.mem, b: gpuData.proc });
}
}
}, [data, gpuName, pointsLimit]);
},
[addDataPoint, gpuName],
);
const { data, error } = useWidgetAPI(
widget,
`${apiVersion}/gpu`,
{
refreshInterval: Math.max(defaultInterval, refreshInterval),
},
{ onSuccess: handleData },
);
if (error || (data && data.error)) {
const finalError = error || data.error;
+18 -16
View File
@@ -1,10 +1,12 @@
import { useTranslation } from "next-i18next/pages";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
import { useCallback } from "react";
import Block from "../components/block";
import Container from "../components/container";
import useDataPoints from "./use-data-points";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
@@ -20,23 +22,23 @@ export default function Component({ service }) {
const { refreshInterval = defaultInterval(chart), pointsLimit = defaultPointsLimit, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
const [dataPoints, addDataPoint] = useDataPoints(pointsLimit, { a: 0, b: 0 });
const { data, error } = useWidgetAPI(service.widget, `${apiVersion}/mem`, {
refreshInterval: Math.max(defaultInterval(chart), refreshInterval),
});
const handleData = useCallback(
(newData) => {
if (newData) addDataPoint({ a: newData.used, b: newData.available });
},
[addDataPoint],
);
useEffect(() => {
if (data) {
setDataPoints((prevDataPoints) => {
const newDataPoints = [...prevDataPoints, { a: data.used, b: data.available }];
if (newDataPoints.length > pointsLimit) {
newDataPoints.shift();
}
return newDataPoints;
});
}
}, [data, pointsLimit]);
const { data, error } = useWidgetAPI(
service.widget,
`${apiVersion}/mem`,
{
refreshInterval: Math.max(defaultInterval(chart), refreshInterval),
},
{ onSuccess: handleData },
);
if (error) {
return <Container error={error} widget={widget} />;
+26 -25
View File
@@ -1,10 +1,12 @@
import { useTranslation } from "next-i18next/pages";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
import { useCallback } from "react";
import Block from "../components/block";
import Container from "../components/container";
import useDataPoints from "./use-data-points";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
@@ -25,33 +27,32 @@ export default function Component({ service }) {
const [, interfaceName] = metric.split(":");
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
const [dataPoints, addDataPoint] = useDataPoints(pointsLimit, { a: 0, b: 0 });
const { data, error } = useWidgetAPI(widget, `${apiVersion}/network`, {
refreshInterval: Math.max(defaultInterval(chart), refreshInterval),
});
const handleData = useCallback(
(newData) => {
if (!newData?.error) {
const interfaceData = newData.find((item) => item[item.key] === interfaceName);
useEffect(() => {
if (data && !data.error) {
const interfaceData = data.find((item) => item[item.key] === interfaceName);
if (interfaceData) {
setDataPoints((prevDataPoints) => {
const newDataPoints = [
...prevDataPoints,
{
a: (interfaceData[rxKey] * 8) / interfaceData.time_since_update,
b: (interfaceData[txKey] * 8) / interfaceData.time_since_update,
},
];
if (newDataPoints.length > pointsLimit) {
newDataPoints.shift();
}
return newDataPoints;
});
if (interfaceData) {
addDataPoint({
a: (interfaceData[rxKey] * 8) / interfaceData.time_since_update,
b: (interfaceData[txKey] * 8) / interfaceData.time_since_update,
});
}
}
}
}, [data, interfaceName, pointsLimit, rxKey, txKey]);
},
[addDataPoint, interfaceName, rxKey, txKey],
);
const { data, error } = useWidgetAPI(
widget,
`${apiVersion}/network`,
{
refreshInterval: Math.max(defaultInterval(chart), refreshInterval),
},
{ onSuccess: handleData },
);
if (error || (data && data.error)) {
const finalError = error || data.error;
+21 -19
View File
@@ -1,10 +1,12 @@
import { useTranslation } from "next-i18next/pages";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
import { useCallback } from "react";
import Block from "../components/block";
import Container from "../components/container";
import useDataPoints from "./use-data-points";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
@@ -20,26 +22,26 @@ export default function Component({ service }) {
const apiVersion = parseVersionForUrl(version, 3);
const [, sensorName] = widget.metric.split(":");
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
const [dataPoints, addDataPoint] = useDataPoints(pointsLimit, { value: 0 });
const { data, error } = useWidgetAPI(service.widget, `${apiVersion}/sensors`, {
refreshInterval: Math.max(defaultInterval, refreshInterval),
});
useEffect(() => {
if (data && !data.error) {
const sensorData = data.find((item) => item.label === sensorName);
if (sensorData) {
setDataPoints((prevDataPoints) => {
const newDataPoints = [...prevDataPoints, { value: sensorData.value }];
if (newDataPoints.length > pointsLimit) {
newDataPoints.shift();
}
return newDataPoints;
});
const handleData = useCallback(
(newData) => {
if (!newData?.error) {
const sensorData = newData.find((item) => item.label === sensorName);
if (sensorData) addDataPoint({ value: sensorData.value });
}
}
}, [data, sensorName, pointsLimit]);
},
[addDataPoint, sensorName],
);
const { data, error } = useWidgetAPI(
service.widget,
`${apiVersion}/sensors`,
{
refreshInterval: Math.max(defaultInterval, refreshInterval),
},
{ onSuccess: handleData },
);
if (error || data?.error) {
return <Container error={error || data.error} widget={widget} />;
@@ -0,0 +1,17 @@
import { useCallback, useState } from "react";
export default function useDataPoints(pointsLimit, initialPoint) {
const [dataPoints, setDataPoints] = useState(() => new Array(pointsLimit).fill(initialPoint));
const addDataPoint = useCallback(
(dataPoint) => {
setDataPoints((currentDataPoints) => {
if (pointsLimit <= 0) return [];
return [...currentDataPoints, dataPoint].slice(-pointsLimit);
});
},
[pointsLimit],
);
return [dataPoints, addDataPoint];
}
@@ -0,0 +1,23 @@
// @vitest-environment jsdom
import { act, renderHook } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import useDataPoints from "./use-data-points";
describe("widgets/glances/metrics/use-data-points", () => {
it("appends data points while keeping the requested history length", () => {
const { result, rerender } = renderHook(({ pointsLimit }) => useDataPoints(pointsLimit, { value: 0 }), {
initialProps: { pointsLimit: 3 },
});
expect(result.current[0]).toEqual([{ value: 0 }, { value: 0 }, { value: 0 }]);
act(() => result.current[1]({ value: 1 }));
expect(result.current[0]).toEqual([{ value: 0 }, { value: 0 }, { value: 1 }]);
rerender({ pointsLimit: 2 });
act(() => result.current[1]({ value: 2 }));
expect(result.current[0]).toEqual([{ value: 1 }, { value: 2 }]);
});
});