From 912ae0adfc4fd5e78a05e91b8321aae70161ad6d Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 5 Nov 2024 12:39:41 -0800 Subject: [PATCH] Feature: Beszel service widget (#4251) --- docs/widgets/services/beszel.md | 20 ++++++ docs/widgets/services/index.md | 1 + mkdocs.yml | 1 + public/locales/en/common.json | 11 ++++ src/utils/config/service-helpers.js | 7 ++ src/widgets/beszel/component.jsx | 60 +++++++++++++++++ src/widgets/beszel/proxy.js | 99 +++++++++++++++++++++++++++++ src/widgets/beszel/widget.js | 14 ++++ src/widgets/components.js | 1 + src/widgets/widgets.js | 2 + 10 files changed, 216 insertions(+) create mode 100644 docs/widgets/services/beszel.md create mode 100644 src/widgets/beszel/component.jsx create mode 100644 src/widgets/beszel/proxy.js create mode 100644 src/widgets/beszel/widget.js diff --git a/docs/widgets/services/beszel.md b/docs/widgets/services/beszel.md new file mode 100644 index 000000000..3cc828b9d --- /dev/null +++ b/docs/widgets/services/beszel.md @@ -0,0 +1,20 @@ +--- +title: Beszel +description: Beszel Widget Configuration +--- + +Learn more about [Beszel]() + +The widget has two modes, a single system with detailed info if `systemId` is provided, or an overview of all systems if `systemId` is not provided. + +Allowed fields for 'overview' mode: `["systems", "up"]` +Allowed fields for a single system: `["name", "status", "updated", "cpu", "memory", "disk", "network"]` + +```yaml +widget: + type: beszel + url: http://beszel.host.or.ip + username: username # email + password: password + systemId: systemId # optional +``` diff --git a/docs/widgets/services/index.md b/docs/widgets/services/index.md index 3dee2a05d..83ad0fed0 100644 --- a/docs/widgets/services/index.md +++ b/docs/widgets/services/index.md @@ -14,6 +14,7 @@ You can also find a list of all available service widgets in the sidebar navigat - [Autobrr](autobrr.md) - [Azure DevOps](azuredevops.md) - [Bazarr](bazarr.md) +- [Beszel](beszel.md) - [Caddy](caddy.md) - [Calendar](calendar.md) - [Calibre-Web](calibre-web.md) diff --git a/mkdocs.yml b/mkdocs.yml index 94af034d7..cd4668920 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -37,6 +37,7 @@ nav: - widgets/services/autobrr.md - widgets/services/azuredevops.md - widgets/services/bazarr.md + - widgets/services/beszel.md - widgets/services/caddy.md - widgets/services/calendar.md - widgets/services/calibre-web.md diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 38ee3e857..bdde0a34b 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -967,5 +967,16 @@ "status": "Status", "online": "Online", "offline": "Offline" + }, + "beszel": { + "name": "Name", + "systems": "Systems", + "up": "Up", + "status": "Status", + "updated": "Updated", + "cpu": "CPU", + "memory": "MEM", + "disk": "Disk", + "network": "NET" } } diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index 9d55ce43a..63dfb6082 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -368,6 +368,9 @@ export function cleanServiceGroups(groups) { repositoryId, userEmail, + // beszel + systemId, + // calendar firstDayInWeek, integrations, @@ -511,6 +514,10 @@ export function cleanServiceGroups(groups) { if (repositoryId) cleanedService.widget.repositoryId = repositoryId; } + if (type === "beszel") { + if (systemId) cleanedService.widget.systemId = systemId; + } + if (type === "coinmarketcap") { if (currency) cleanedService.widget.currency = currency; if (symbols) cleanedService.widget.symbols = symbols; diff --git a/src/widgets/beszel/component.jsx b/src/widgets/beszel/component.jsx new file mode 100644 index 000000000..1d35b6e94 --- /dev/null +++ b/src/widgets/beszel/component.jsx @@ -0,0 +1,60 @@ +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"; + +export default function Component({ service }) { + const { t } = useTranslation(); + + const { widget } = service; + const { systemId } = widget; + + const { data: systems, error: systemsError } = useWidgetAPI(widget, "systems"); + + const MAX_ALLOWED_FIELDS = 4; + if (!widget.fields?.length > 0) { + widget.fields = systemId ? ["name", "status", "cpu", "memory"] : ["systems", "up"]; + } + if (widget.fields?.length > MAX_ALLOWED_FIELDS) { + widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS); + } + + if (systemsError) { + return ; + } + + if (!systems) { + return ( + + + + + ); + } + + if (systemId) { + const system = systems.items.find((item) => item.id === systemId); + + return ( + + + + + + + + + + ); + } + + const upTotal = systems.items.filter((item) => item.status === "up").length; + + return ( + + + + + ); +} diff --git a/src/widgets/beszel/proxy.js b/src/widgets/beszel/proxy.js new file mode 100644 index 000000000..5f70a10d2 --- /dev/null +++ b/src/widgets/beszel/proxy.js @@ -0,0 +1,99 @@ +import cache from "memory-cache"; + +import getServiceWidget from "utils/config/service-helpers"; +import { formatApiCall } from "utils/proxy/api-helpers"; +import { httpProxy } from "utils/proxy/http"; +import widgets from "widgets/widgets"; +import createLogger from "utils/logger"; + +const proxyName = "beszelProxyHandler"; +const tokenCacheKey = `${proxyName}__token`; +const logger = createLogger(proxyName); + +async function login(loginUrl, username, password, service) { + const authResponse = await httpProxy(loginUrl, { + method: "POST", + body: JSON.stringify({ identity: username, password }), + headers: { + "Content-Type": "application/json", + }, + }); + + const status = authResponse[0]; + let data = authResponse[2]; + try { + data = JSON.parse(Buffer.from(authResponse[2]).toString()); + + if (status === 200) { + cache.put(`${tokenCacheKey}.${service}`, data.token); + } + } catch (e) { + logger.error(`Error ${status} logging into beszel`, JSON.stringify(authResponse[2])); + } + return [status, data.token ?? data]; +} + +export default async function beszelProxyHandler(req, res) { + const { group, service, endpoint } = req.query; + + if (group && service) { + const widget = await getServiceWidget(group, service); + + if (!widgets?.[widget.type]?.api) { + return res.status(403).json({ error: "Service does not support API calls" }); + } + + if (widget) { + const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); + const loginUrl = formatApiCall(widgets[widget.type].api, { endpoint: "admins/auth-with-password", ...widget }); + + let status; + let data; + + let token = cache.get(`${tokenCacheKey}.${service}`); + if (!token) { + [status, token] = await login(loginUrl, widget.username, widget.password, service); + if (status !== 200) { + logger.debug(`HTTTP ${status} logging into npm api: ${token}`); + return res.status(status).send(token); + } + } + + [status, , data] = await httpProxy(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); + + if (status === 403) { + logger.debug(`HTTTP ${status} retrieving data from npm api, logging in and trying again.`); + cache.del(`${tokenCacheKey}.${service}`); + [status, token] = await login(loginUrl, widget.username, widget.password, service); + + if (status !== 200) { + logger.debug(`HTTTP ${status} logging into npm api: ${data}`); + return res.status(status).send(data); + } + + // eslint-disable-next-line no-unused-vars + [status, , data] = await httpProxy(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); + } + + if (status !== 200) { + return res.status(status).send(data); + } + + return res.send(data); + } + } + + return res.status(400).json({ error: "Invalid proxy service type" }); +} diff --git a/src/widgets/beszel/widget.js b/src/widgets/beszel/widget.js new file mode 100644 index 000000000..508c1debb --- /dev/null +++ b/src/widgets/beszel/widget.js @@ -0,0 +1,14 @@ +import beszelProxyHandler from "./proxy"; + +const widget = { + api: "{url}/api/{endpoint}", + proxyHandler: beszelProxyHandler, + + mappings: { + systems: { + endpoint: "collections/systems/records?page=1&perPage=500&sort=%2Bcreated", + }, + }, +}; + +export default widget; diff --git a/src/widgets/components.js b/src/widgets/components.js index 8453e5275..50564e493 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -8,6 +8,7 @@ const components = { autobrr: dynamic(() => import("./autobrr/component")), azuredevops: dynamic(() => import("./azuredevops/component")), bazarr: dynamic(() => import("./bazarr/component")), + beszel: dynamic(() => import("./beszel/component")), caddy: dynamic(() => import("./caddy/component")), calendar: dynamic(() => import("./calendar/component")), calibreweb: dynamic(() => import("./calibreweb/component")), diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index a1af3661b..40b121aef 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -5,6 +5,7 @@ import authentik from "./authentik/widget"; import autobrr from "./autobrr/widget"; import azuredevops from "./azuredevops/widget"; import bazarr from "./bazarr/widget"; +import beszel from "./beszel/widget"; import caddy from "./caddy/widget"; import calendar from "./calendar/widget"; import calibreweb from "./calibreweb/widget"; @@ -133,6 +134,7 @@ const widgets = { autobrr, azuredevops, bazarr, + beszel, caddy, calibreweb, changedetectionio,