From 8ce9e57ed80691e5ce3b19a8ac496f4f968d959d Mon Sep 17 00:00:00 2001 From: Andrew Ensley Date: Wed, 11 Jun 2025 12:05:53 -0400 Subject: [PATCH] Enhancement: Komodo widget (#5407) Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- docs/widgets/services/komodo.md | 22 ++++++++ public/locales/en/common.json | 11 ++++ src/utils/config/service-helpers.js | 8 +++ src/widgets/components.js | 1 + src/widgets/komodo/component.jsx | 84 +++++++++++++++++++++++++++++ src/widgets/komodo/proxy.js | 55 +++++++++++++++++++ src/widgets/komodo/widget.js | 32 +++++++++++ src/widgets/widgets.js | 2 + 8 files changed, 215 insertions(+) create mode 100644 docs/widgets/services/komodo.md create mode 100644 src/widgets/komodo/component.jsx create mode 100644 src/widgets/komodo/proxy.js create mode 100644 src/widgets/komodo/widget.js diff --git a/docs/widgets/services/komodo.md b/docs/widgets/services/komodo.md new file mode 100644 index 000000000..9eb93de86 --- /dev/null +++ b/docs/widgets/services/komodo.md @@ -0,0 +1,22 @@ +--- +title: Komodo +description: Komodo Widget Configuration +--- + +This widget shows either details about all containers or stacks (if `showStacks` is true) managed by [Komodo](https://komo.do/) or the number of running servers, containers and stacks when `showSummary` is enabled. + +The api key and secret can be found in the Komodo settings. + +Allowed fields (max 4): `["total", "running", "stopped", "unhealthy", "unknown"]`. +Allowed fields with `showStacks` (max 4): `["total", "running", "down", "unhealthy", "unknown"]`. +Allowed fields with `showSummary`: `["servers", "stacks", "containers"]`. + +```yaml +widget: + type: komodo + url: http://komodo.hostname.or.ip:port + key: K-xxxxxx... + secret: S-xxxxxx... + showSummary: true # optional, default: false + showStacks: true # optional, default: false +``` diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 43378ec1f..07c12d5a8 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -1060,5 +1060,16 @@ "checkmk": { "serviceErrors": "Service issues", "hostErrors": "Host issues" + }, + "komodo": { + "total": "Total", + "running": "Running", + "stopped": "Stopped", + "down": "Down", + "unhealthy": "Unhealthy", + "unknown": "Unknown", + "servers": "Servers", + "stacks": "Stacks", + "containers": "Containers" } } diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index 79e3a489b..d46c86057 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -338,6 +338,10 @@ export function cleanServiceGroups(groups) { // jellystat days, + // komodo + showSummary, + showStacks, + // kopia snapshotHost, snapshotPath, @@ -450,6 +454,10 @@ export function cleanServiceGroups(groups) { if (type === "proxmoxbackupserver") { if (datastore) widget.datastore = datastore; } + if (type === "komodo") { + if (showSummary !== undefined) widget.showSummary = !!JSON.parse(showSummary); + if (showStacks !== undefined) widget.showStacks = !!JSON.parse(showStacks); + } if (type === "kubernetes") { if (namespace) widget.namespace = namespace; if (app) widget.app = app; diff --git a/src/widgets/components.js b/src/widgets/components.js index 92e057dc9..b652c2d86 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -63,6 +63,7 @@ const components = { jellystat: dynamic(() => import("./jellystat/component")), kavita: dynamic(() => import("./kavita/component")), komga: dynamic(() => import("./komga/component")), + komodo: dynamic(() => import("./komodo/component")), kopia: dynamic(() => import("./kopia/component")), lidarr: dynamic(() => import("./lidarr/component")), linkwarden: dynamic(() => import("./linkwarden/component")), diff --git a/src/widgets/komodo/component.jsx b/src/widgets/komodo/component.jsx new file mode 100644 index 000000000..4d22758e5 --- /dev/null +++ b/src/widgets/komodo/component.jsx @@ -0,0 +1,84 @@ +import Block from "components/services/widget/block"; +import Container from "components/services/widget/container"; +import { useTranslation } from "next-i18next"; + +import useWidgetAPI from "utils/proxy/use-widget-api"; + +const MAX_ALLOWED_FIELDS = 4; + +export default function Component({ service }) { + const { t } = useTranslation(); + const { widget } = service; + const containersEndpoint = !(!widget.showSummary && widget.showStacks) ? "containers" : ""; + const { data: containersData, error: containersError } = useWidgetAPI(widget, containersEndpoint); + const stacksEndpoint = widget.showSummary || widget.showStacks ? "stacks" : ""; + const { data: stacksData, error: stacksError } = useWidgetAPI(widget, stacksEndpoint); + const serversEndpoint = widget.showSummary ? "servers" : ""; + const { data: serversData, error: serversError } = useWidgetAPI(widget, serversEndpoint); + + if (containersError || stacksError || serversError) { + return ; + } + + if (!widget.fields || widget.fields.length === 0) { + widget.fields = widget.showSummary + ? ["servers", "stacks", "containers"] + : widget.showStacks + ? ["total", "running", "down", "unhealthy"] + : ["total", "running", "stopped", "unhealthy"]; + } else if (widget.fields?.length > MAX_ALLOWED_FIELDS) { + widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS); + } + + if ( + (!widget.showStacks && !containersData) || + (widget.showSummary && (!stacksData || !serversData)) || + (widget.showStacks && !stacksData) + ) { + return widget.showSummary ? ( + + + + + + ) : widget.showStacks ? ( + + + + + + + ) : ( + + + + + + + ); + } + + return widget.showSummary ? ( + + + + + + ) : widget.showStacks ? ( + + + + + + + + ) : ( + + + + + + + + ); +} diff --git a/src/widgets/komodo/proxy.js b/src/widgets/komodo/proxy.js new file mode 100644 index 000000000..f3c86b2e6 --- /dev/null +++ b/src/widgets/komodo/proxy.js @@ -0,0 +1,55 @@ +import getServiceWidget from "utils/config/service-helpers"; +import createLogger from "utils/logger"; +import { formatApiCall, sanitizeErrorURL } from "utils/proxy/api-helpers"; +import { httpProxy } from "utils/proxy/http"; +import validateWidgetData from "utils/proxy/validate-widget-data"; +import widgets from "widgets/widgets"; + +const logger = createLogger("komodoProxyHandler"); + +export default async function komodoProxyHandler(req, res) { + const { group, service, endpoint, index } = req.query; + + if (group && service) { + const widget = await getServiceWidget(group, service, index); + if (!widgets?.[widget.type]?.api) { + return res.status(403).json({ error: "Service does not support API calls" }); + } + + if (widget) { + // api uses unified read endpoint + const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint: "read", ...widget })).toString(); + + const headers = { + "Content-Type": "application/json", + "X-API-Key": `${widget.key}`, + "X-API-Secret": `${widget.secret}`, + }; + const [status, contentType, data] = await httpProxy(url, { + method: "POST", + body: JSON.stringify(widgets[widget.type].mappings?.[endpoint]?.body || {}), + headers, + }); + + let resultData = data; + + if (status >= 400) { + logger.error("HTTP Error %d calling %s", status, sanitizeErrorURL(url)); + } + + if (status === 200) { + if (!validateWidgetData(widget, endpoint, resultData)) { + return res + .status(500) + .json({ error: { message: "Invalid data", url: sanitizeErrorURL(url), data: resultData } }); + } + } + + if (contentType) res.setHeader("Content-Type", contentType); + return res.status(status).send(resultData); + } + } + + logger.debug("Invalid or missing proxy service type '%s' in group '%s'", service, group); + return res.status(400).json({ error: "Invalid proxy service type" }); +} diff --git a/src/widgets/komodo/widget.js b/src/widgets/komodo/widget.js new file mode 100644 index 000000000..55454f97c --- /dev/null +++ b/src/widgets/komodo/widget.js @@ -0,0 +1,32 @@ +import komodoProxyHandler from "./proxy"; + +const widget = { + api: "{url}/{endpoint}", + proxyHandler: komodoProxyHandler, + + mappings: { + containers: { + endpoint: "containers", // api actually uses unified read endpoint + body: { + type: "GetDockerContainersSummary", + params: {}, + }, + }, + stacks: { + endpoint: "stacks", // api actually uses unified read endpoint + body: { + type: "GetStacksSummary", + params: {}, + }, + }, + servers: { + endpoint: "servers", // api actually uses unified read endpoint + body: { + type: "GetServersSummary", + params: {}, + }, + }, + }, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 2a2df8c23..8178f26ba 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -54,6 +54,7 @@ import jellystat from "./jellystat/widget"; import karakeep from "./karakeep/widget"; import kavita from "./kavita/widget"; import komga from "./komga/widget"; +import komodo from "./komodo/widget"; import kopia from "./kopia/widget"; import lidarr from "./lidarr/widget"; import linkwarden from "./linkwarden/widget"; @@ -197,6 +198,7 @@ const widgets = { jellystat, kavita, komga, + komodo, kopia, lidarr, linkwarden,