From 0d47dcaac7697409c38b9e6e4d9d3931b278ac6c Mon Sep 17 00:00:00 2001 From: Derek Stotz Date: Thu, 15 Feb 2024 23:38:55 -0600 Subject: [PATCH] Enhancement: Add enablePools option to TrueNAS service widget (#2908) --------- Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- docs/widgets/services/truenas.md | 3 ++ src/utils/config/service-helpers.js | 6 ++++ src/utils/proxy/handlers/credentialed.js | 10 +++++-- src/widgets/truenas/component.jsx | 24 ++++++++++----- src/widgets/truenas/pool.jsx | 31 +++++++++++++++++++ src/widgets/truenas/widget.js | 38 ++++++++---------------- 6 files changed, 77 insertions(+), 35 deletions(-) create mode 100644 src/widgets/truenas/pool.jsx diff --git a/docs/widgets/services/truenas.md b/docs/widgets/services/truenas.md index 6d747ef17..243504905 100644 --- a/docs/widgets/services/truenas.md +++ b/docs/widgets/services/truenas.md @@ -9,6 +9,8 @@ Allowed fields: `["load", "uptime", "alerts"]`. To create an API Key, follow [the official TrueNAS documentation](https://www.truenas.com/docs/scale/scaletutorials/toptoolbar/managingapikeys/). +A detailed pool listing is disabled by default, but can be enabled with the `enablePools` option. + ```yaml widget: type: truenas @@ -16,4 +18,5 @@ widget: username: user # not required if using api key password: pass # not required if using api key key: yourtruenasapikey # not required if using username / password + enablePools: true # optional, defaults to false ``` diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index e203e6d7f..67502a7a7 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -442,6 +442,9 @@ export function cleanServiceGroups(groups) { // sonarr, radarr enableQueue, + // truenas + enablePools, + // unifi site, } = cleanedService.widget; @@ -511,6 +514,9 @@ export function cleanServiceGroups(groups) { if (["sonarr", "radarr"].includes(type)) { if (enableQueue !== undefined) cleanedService.widget.enableQueue = JSON.parse(enableQueue); } + if (type === "truenas") { + if (enablePools !== undefined) cleanedService.widget.enablePools = JSON.parse(enablePools); + } if (["diskstation", "qnap"].includes(type)) { if (volume) cleanedService.widget.volume = volume; } diff --git a/src/utils/proxy/handlers/credentialed.js b/src/utils/proxy/handlers/credentialed.js index 0795efd52..bc0875932 100644 --- a/src/utils/proxy/handlers/credentialed.js +++ b/src/utils/proxy/handlers/credentialed.js @@ -29,11 +29,15 @@ export default async function credentialedProxyHandler(req, res, map) { } else if (widget.type === "gotify") { headers["X-gotify-Key"] = `${widget.key}`; } else if ( - ["authentik", "cloudflared", "ghostfolio", "mealie", "tailscale", "truenas", "pterodactyl"].includes( - widget.type, - ) + ["authentik", "cloudflared", "ghostfolio", "mealie", "tailscale", "pterodactyl"].includes(widget.type) ) { headers.Authorization = `Bearer ${widget.key}`; + } else if (widget.type === "truenas") { + if (widget.key) { + headers.Authorization = `Bearer ${widget.key}`; + } else { + headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`; + } } else if (widget.type === "proxmox") { headers.Authorization = `PVEAPIToken=${widget.username}=${widget.password}`; } else if (widget.type === "proxmoxbackupserver") { diff --git a/src/widgets/truenas/component.jsx b/src/widgets/truenas/component.jsx index c1fc5c53a..872d8c647 100644 --- a/src/widgets/truenas/component.jsx +++ b/src/widgets/truenas/component.jsx @@ -3,6 +3,7 @@ import { useTranslation } from "next-i18next"; import Container from "components/services/widget/container"; import Block from "components/services/widget/block"; import useWidgetAPI from "utils/proxy/use-widget-api"; +import Pool from "widgets/truenas/pool"; export default function Component({ service }) { const { t } = useTranslation(); @@ -11,9 +12,10 @@ export default function Component({ service }) { const { data: alertData, error: alertError } = useWidgetAPI(widget, "alerts"); const { data: statusData, error: statusError } = useWidgetAPI(widget, "status"); + const { data: poolsData, error: poolsError } = useWidgetAPI(widget, "pools"); - if (alertError || statusError) { - const finalError = alertError ?? statusError; + if (alertError || statusError || poolsError) { + const finalError = alertError ?? statusError ?? poolsError; return ; } @@ -27,11 +29,19 @@ export default function Component({ service }) { ); } + const enablePools = widget?.enablePools && Array.isArray(poolsData) && poolsData.length > 0; + return ( - - - - - + <> + + + + + + {enablePools && + poolsData.map((pool) => ( + + ))} + ); } diff --git a/src/widgets/truenas/pool.jsx b/src/widgets/truenas/pool.jsx new file mode 100644 index 000000000..8e9d04656 --- /dev/null +++ b/src/widgets/truenas/pool.jsx @@ -0,0 +1,31 @@ +import classNames from "classnames"; +import prettyBytes from "pretty-bytes"; + +export default function Pool({ name, free, allocated, healthy }) { + const total = free + allocated; + const usedPercent = Math.round((allocated / total) * 100); + const statusColor = healthy ? "bg-green-500" : "bg-yellow-500"; + + return ( +
+
+ + + +
+
{name}
+
+
+ + {prettyBytes(allocated)} / {prettyBytes(total)} + + ({usedPercent}%) +
+
+ ); +} diff --git a/src/widgets/truenas/widget.js b/src/widgets/truenas/widget.js index 6c0f3622c..7435b6e19 100644 --- a/src/widgets/truenas/widget.js +++ b/src/widgets/truenas/widget.js @@ -1,32 +1,9 @@ -import { jsonArrayFilter } from "utils/proxy/api-helpers"; import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; -import genericProxyHandler from "utils/proxy/handlers/generic"; -import getServiceWidget from "utils/config/service-helpers"; +import { asJson, jsonArrayFilter } from "utils/proxy/api-helpers"; const widget = { api: "{url}/api/v2.0/{endpoint}", - proxyHandler: async (req, res, map) => { - // choose proxy handler based on widget settings - const { group, service } = req.query; - - if (group && service) { - const widgetOpts = await getServiceWidget(group, service); - let handler; - if (widgetOpts.username && widgetOpts.password) { - handler = genericProxyHandler; - } else if (widgetOpts.key) { - handler = credentialedProxyHandler; - } - - if (handler) { - return handler(req, res, map); - } - - return res.status(500).json({ error: "Username / password or API key required" }); - } - - return res.status(500).json({ error: "Error parsing widget request" }); - }, + proxyHandler: credentialedProxyHandler, mappings: { alerts: { @@ -39,6 +16,17 @@ const widget = { endpoint: "system/info", validate: ["loadavg", "uptime_seconds"], }, + pools: { + endpoint: "pool", + map: (data) => + asJson(data).map((entry) => ({ + id: entry.name, + name: entry.name, + healthy: entry.healthy, + allocated: entry.allocated, + free: entry.free, + })), + }, }, };