From 7d019185a3abc16011df052505aea172d70b1e52 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Wed, 4 Feb 2026 20:07:07 -0600 Subject: [PATCH] Feature: arcane service widget (#6274) Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- docs/widgets/services/arcane.md | 18 +++++++ docs/widgets/services/index.md | 1 + mkdocs.yml | 1 + public/locales/en/common.json | 7 +++ src/utils/config/service-helpers.js | 7 +++ src/widgets/arcane/component.jsx | 73 +++++++++++++++++++++++++++++ src/widgets/arcane/widget.js | 24 ++++++++++ src/widgets/components.js | 1 + src/widgets/widgets.js | 2 + 9 files changed, 134 insertions(+) create mode 100644 docs/widgets/services/arcane.md create mode 100644 src/widgets/arcane/component.jsx create mode 100644 src/widgets/arcane/widget.js diff --git a/docs/widgets/services/arcane.md b/docs/widgets/services/arcane.md new file mode 100644 index 000000000..c8d88207f --- /dev/null +++ b/docs/widgets/services/arcane.md @@ -0,0 +1,18 @@ +--- +title: Arcane +description: Arcane Widget Configuration +--- + +Learn more about [Arcane](https://github.com/getarcaneapp/arcane). + +**Allowed fields** (max 4): `running`, `stopped`, `total`, `images`, `images_used`, `images_unused`, `image_updates`. +**Default fields**: `running`, `stopped`, `total`, `image_updates`. + +```yaml +widget: + type: arcane + url: http://localhost:3552 + env: 0 # required, 0 is Arcane default local environment + key: your-api-key + fields: ["running", "stopped", "total", "image_updates"] # optional +``` diff --git a/docs/widgets/services/index.md b/docs/widgets/services/index.md index 8b43802e9..4aa67bdd6 100644 --- a/docs/widgets/services/index.md +++ b/docs/widgets/services/index.md @@ -9,6 +9,7 @@ You can also find a list of all available service widgets in the sidebar navigat - [Adguard Home](adguard-home.md) - [APC UPS](apcups.md) +- [Arcane](arcane.md) - [ArgoCD](argocd.md) - [Atsumeru](atsumeru.md) - [Audiobookshelf](audiobookshelf.md) diff --git a/mkdocs.yml b/mkdocs.yml index 30dad803f..6b240aee4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,6 +33,7 @@ nav: - widgets/services/index.md - widgets/services/adguard-home.md - widgets/services/apcups.md + - widgets/services/arcane.md - widgets/services/argocd.md - widgets/services/atsumeru.md - widgets/services/audiobookshelf.md diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 2f655bf32..08fed5656 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -1151,6 +1151,13 @@ "time": "Time", "artists": "Artists" }, + "arcane": { + "containers": "Containers", + "images": "Images", + "image_updates": "Image Updates", + "images_unused": "Unused", + "environment_required": "Environment ID Required" + }, "dockhand": { "running": "Running", "stopped": "Stopped", diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index 75740c3ef..7e913c981 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -258,6 +258,9 @@ export function cleanServiceGroups(groups) { highlight, type, + // arcane + env, + // azuredevops repositoryId, userEmail, @@ -472,6 +475,10 @@ export function cleanServiceGroups(groups) { if (repositoryId) widget.repositoryId = repositoryId; } + if (type === "arcane") { + if (env !== undefined) widget.env = env; + } + if (type === "beszel") { if (systemId) widget.systemId = systemId; } diff --git a/src/widgets/arcane/component.jsx b/src/widgets/arcane/component.jsx new file mode 100644 index 000000000..d8a085156 --- /dev/null +++ b/src/widgets/arcane/component.jsx @@ -0,0 +1,73 @@ +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_FIELDS = 4; + +export default function Component({ service }) { + const { t } = useTranslation(); + const { widget } = service; + + if (!widget.fields) { + widget.fields = ["running", "stopped", "total", "image_updates"]; + } else if (widget.fields.length > MAX_FIELDS) { + widget.fields = widget.fields.slice(0, MAX_FIELDS); + } + + if (widget?.env == null || widget.env === "") { + return ; + } + + const { data: containers, error: containersError } = useWidgetAPI(widget, "containers"); + const { data: images, error: imagesError } = useWidgetAPI(widget, "images"); + const { data: updates, error: updatesError } = useWidgetAPI(widget, "updates"); + + const error = containersError ?? imagesError ?? updatesError; + if (error) { + return ; + } + + if (!containers || !images || !updates) { + return ( + + + + + + + + + + ); + } + + const runningContainers = containers?.runningContainers ?? 0; + const totalContainers = containers?.totalContainers ?? 0; + const stoppedContainers = containers?.stoppedContainers ?? 0; + const totalImages = images?.totalImages ?? 0; + const imagesInuse = images?.imagesInuse ?? 0; + const imagesUnused = images?.imagesUnused ?? 0; + const imagesWithUpdates = updates?.imagesWithUpdates ?? 0; + + return ( + + + + + + + + + + ); +} diff --git a/src/widgets/arcane/widget.js b/src/widgets/arcane/widget.js new file mode 100644 index 000000000..f71802140 --- /dev/null +++ b/src/widgets/arcane/widget.js @@ -0,0 +1,24 @@ +import { asJson } from "utils/proxy/api-helpers"; +import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; + +const widget = { + api: "{url}/api/{endpoint}", + proxyHandler: credentialedProxyHandler, + + mappings: { + containers: { + endpoint: "environments/{env}/containers/counts", + map: (data) => asJson(data).data, + }, + images: { + endpoint: "environments/{env}/images/counts", + map: (data) => asJson(data).data, + }, + updates: { + endpoint: "environments/{env}/image-updates/summary", + map: (data) => asJson(data).data, + }, + }, +}; + +export default widget; diff --git a/src/widgets/components.js b/src/widgets/components.js index c114a82a5..61585f5f6 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -3,6 +3,7 @@ import dynamic from "next/dynamic"; const components = { adguard: dynamic(() => import("./adguard/component")), apcups: dynamic(() => import("./apcups/component")), + arcane: dynamic(() => import("./arcane/component")), argocd: dynamic(() => import("./argocd/component")), atsumeru: dynamic(() => import("./atsumeru/component")), audiobookshelf: dynamic(() => import("./audiobookshelf/component")), diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 26235729b..5142ee23c 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -1,5 +1,6 @@ import adguard from "./adguard/widget"; import apcups from "./apcups/widget"; +import arcane from "./arcane/widget"; import argocd from "./argocd/widget"; import atsumeru from "./atsumeru/widget"; import audiobookshelf from "./audiobookshelf/widget"; @@ -152,6 +153,7 @@ import zabbix from "./zabbix/widget"; const widgets = { adguard, apcups, + arcane, argocd, atsumeru, audiobookshelf,