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/components/widgets/truenas/pool.jsx b/src/components/widgets/truenas/pool.jsx new file mode 100644 index 000000000..c9954b300 --- /dev/null +++ b/src/components/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/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index fb6757b6f..bb6afdbcd 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -441,6 +441,9 @@ export function cleanServiceGroups(groups) { // sonarr, radarr enableQueue, + // truenas + enablePools, + // unifi site, } = cleanedService.widget; @@ -510,6 +513,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..005846cf5 100644 --- a/src/utils/proxy/handlers/credentialed.js +++ b/src/utils/proxy/handlers/credentialed.js @@ -70,6 +70,7 @@ export default async function credentialedProxyHandler(req, res, map) { withCredentials: true, credentials: "include", headers, + body: req.body, }); let resultData = data; diff --git a/src/widgets/truenas/component.jsx b/src/widgets/truenas/component.jsx index c1fc5c53a..0bd9ddfc8 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 "components/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/proxy.js b/src/widgets/truenas/proxy.js new file mode 100644 index 000000000..6982f9727 --- /dev/null +++ b/src/widgets/truenas/proxy.js @@ -0,0 +1,29 @@ +import getServiceWidget from "utils/config/service-helpers"; +import createLogger from "utils/logger"; +import genericProxyHandler from "utils/proxy/handlers/generic"; +import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; + +const logger = createLogger("truenasProxyHandler"); + +export default async function truenasProxyHandler(req, res, map) { + 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); + } + + logger.error("Error getting data from Truenas: Username / password or API key required"); + return res.status(500).json({ error: "Username / password or API key required" }); + } + + return res.status(500).json({ error: "Error parsing widget request" }); +} diff --git a/src/widgets/truenas/widget.js b/src/widgets/truenas/widget.js index 6c0f3622c..1331d98be 100644 --- a/src/widgets/truenas/widget.js +++ b/src/widgets/truenas/widget.js @@ -1,32 +1,10 @@ -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 truenasProxyHandler from "./proxy"; + +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: truenasProxyHandler, mappings: { alerts: { @@ -39,6 +17,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, + })), + }, }, };