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,