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,