diff --git a/src/components/widgets/longhorn/longhorn.jsx b/src/components/widgets/longhorn/longhorn.jsx new file mode 100644 index 00000000..3ba421d0 --- /dev/null +++ b/src/components/widgets/longhorn/longhorn.jsx @@ -0,0 +1,57 @@ +import useSWR from "swr"; +import { BiError } from "react-icons/bi"; +import { i18n, useTranslation } from "next-i18next"; + +import Node from "./node"; + +export default function Longhorn({ options }) { + const { expanded, total, labels, include, nodes } = options; + const { t } = useTranslation(); + const { data, error } = useSWR(`/api/widgets/longhorn`, { + refreshInterval: 1500 + }); + + if (error || data?.error) { + return ( +
+ +
+ {t("widget.api_error")} +
+
+ ); + } + + if (!data) { + return ( +
+
+
+ ); + } + + return ( +
+
+ {data.nodes + .filter((node) => { + if (node.id === 'total' && total) { + return true; + } + if (!nodes) { + return false; + } + if (include && !include.includes(node.id)) { + return false; + } + return true; + }) + .map((node) => +
+ +
+ )} +
+
+ ); +} diff --git a/src/components/widgets/longhorn/node.jsx b/src/components/widgets/longhorn/node.jsx new file mode 100644 index 00000000..44b96b1c --- /dev/null +++ b/src/components/widgets/longhorn/node.jsx @@ -0,0 +1,32 @@ +import { FiHardDrive } from "react-icons/fi"; +import { useTranslation } from "next-i18next"; + +import UsageBar from "../resources/usage-bar"; + +export default function Node({ data, expanded, labels }) { + const { t } = useTranslation(); + + return ( + <> +
+ +
+ +
{t("common.bytes", { value: data.node.available })}
+
{t("resources.free")}
+
+ {expanded && ( + +
{t("common.bytes", { value: data.node.maximum })}
+
{t("resources.total")}
+
+ )} + +
+
+ {labels && ( +
{data.node.id}
+ )} + + ); +} diff --git a/src/components/widgets/widget.jsx b/src/components/widgets/widget.jsx index 86f79dfe..29e7a180 100644 --- a/src/components/widgets/widget.jsx +++ b/src/components/widgets/widget.jsx @@ -13,6 +13,7 @@ const widgetMappings = { unifi_console: dynamic(() => import("components/widgets/unifi_console/unifi_console")), glances: dynamic(() => import("components/widgets/glances/glances")), openmeteo: dynamic(() => import("components/widgets/openmeteo/openmeteo")), + longhorn: dynamic(() => import("components/widgets/longhorn/longhorn")), }; export default function Widget({ widget }) { diff --git a/src/pages/api/widgets/kubernetes.js b/src/pages/api/widgets/kubernetes.js index a740df90..350c93a2 100644 --- a/src/pages/api/widgets/kubernetes.js +++ b/src/pages/api/widgets/kubernetes.js @@ -39,19 +39,7 @@ export default async function handler(req, res) { } }); } - // Maybe Storage CSI can provide this information - // if (type === "disk") { - // if (!existsSync(target)) { - // return res.status(404).json({ - // error: "Target not found", - // }); - // } - // - // return res.status(200).json({ - // drive: await drive.info(target || "/"), - // }); - // } - // + if (type === "memory") { const SCALE_MB = 1024 * 1024; const usedMemMb = memUsage / SCALE_MB; diff --git a/src/pages/api/widgets/longhorn.js b/src/pages/api/widgets/longhorn.js new file mode 100644 index 00000000..cb9ed24b --- /dev/null +++ b/src/pages/api/widgets/longhorn.js @@ -0,0 +1,82 @@ +import { httpProxy } from "../../../utils/proxy/http"; +import createLogger from "../../../utils/logger"; +import { getSettings } from "../../../utils/config/config"; + +const logger = createLogger("longhorn"); + +function parseLonghornData(data) { + const json = JSON.parse(data); + + if (!json) { + return null; + } + + const nodes = json.data.map((node) => { + let available = 0; + let maximum = 0; + let reserved = 0; + let scheduled = 0; + Object.keys(node.disks).forEach((diskKey) => { + const disk = node.disks[diskKey]; + available += disk.storageAvailable; + maximum += disk.storageMaximum; + reserved += disk.storageReserved; + scheduled += disk.storageScheduled; + }); + return { + id: node.id, + available, + maximum, + reserved, + scheduled, + }; + }); + const total = nodes.reduce((summary, node) => ({ + available: summary.available + node.available, + maximum: summary.maximum + node.maximum, + reserved: summary.reserved + node.reserved, + scheduled: summary.scheduled + node.scheduled, + })); + total.id = "total"; + nodes.push(total); + return nodes; +} + +export default async function handler(req, res) { + const settings = getSettings(); + const longhornSettings = settings?.providers?.longhorn; + const {url, username, password} = longhornSettings; + + if (!url) { + const errorMessage = "Missing Longhorn URL"; + logger.error(errorMessage); + return res.status(400).json({ error: errorMessage }); + } + + const apiUrl = `${url}/v1/nodes`; + const headers = { + "Accept-Encoding": "application/json" + }; + if (username && password) { + headers.Authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}` + } + const params = { method: "GET", headers }; + + const [status, contentType, data] = await httpProxy(apiUrl, params); + + if (status === 401) { + logger.error("Authorization failure getting data from Longhorn API. Data: %s", data); + } + + if (status !== 200) { + logger.error("HTTP %d getting data from Longhorn API. Data: %s", status, data); + } + + if (contentType) res.setHeader("Content-Type", contentType); + + const nodes = parseLonghornData(data); + + return res.status(200).json({ + nodes, + }); +}