diff --git a/docs/widgets/services/argocd.md b/docs/widgets/services/argocd.md
new file mode 100644
index 000000000..6a81b8db9
--- /dev/null
+++ b/docs/widgets/services/argocd.md
@@ -0,0 +1,33 @@
+---
+title: ArgoCD
+description: ArgoCD Widget Configuration
+---
+
+Learn more about [ArgoCD](https://argo-cd.readthedocs.io/en/stable/).
+
+Allowed fields (limited to a max of 4): `["apps", "synced", "outOfSync", "healthy", "progressing", "degraded", "suspended", "missing"]`
+
+```yaml
+widget:
+ type: argocd
+ url: http://argocd.host.or.ip:port
+ key: argocdapikey
+```
+
+You can generate an API key either by creating a bearer token for an existing account, see [Authorization](https://argo-cd.readthedocs.io/en/latest/developer-guide/api-docs/#authorization) (not recommended) or create a new local user account with limited privileges and generate an authentication token for this account. To do this the steps are:
+
+- [Create a new local user](https://argo-cd.readthedocs.io/en/stable/operator-manual/user-management/#create-new-user) and give it the `apiKey` capability
+- Setup [RBAC configuration](https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/#rbac-configuration) for your the user and give it readonly access to your ArgoCD resources, e.g. by giving it the `role:readonly` role.
+- In your ArgoCD project under _Settings / Accounts_ open the newly created account and in the _Tokens_ section click on _Generate New_ to generate an access token, optionally specifying an expiry date.
+
+If you installed ArgoCD via the official Helm chart, the account creation and rbac config can be achived by overriding these helm values:
+
+```yaml
+configs:
+ cm:
+ accounts.readonly: apiKey
+ rbac:
+ policy.csv: "g, readonly, role:readonly"
+```
+
+This creates a new account called `readonly` and attaches the `role:readonly` role to it.
diff --git a/docs/widgets/services/index.md b/docs/widgets/services/index.md
index 8ea2e9331..ae506f086 100644
--- a/docs/widgets/services/index.md
+++ b/docs/widgets/services/index.md
@@ -8,6 +8,7 @@ search:
You can also find a list of all available service widgets in the sidebar navigation.
- [Adguard Home](adguard-home.md)
+- [ArgoCD](argocd.md)
- [Atsumeru](atsumeru.md)
- [Audiobookshelf](audiobookshelf.md)
- [Authentik](authentik.md)
diff --git a/mkdocs.yml b/mkdocs.yml
index 42abce301..1e9d59cc8 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -31,6 +31,7 @@ nav:
- "Service Widgets":
- widgets/services/index.md
- widgets/services/adguard-home.md
+ - widgets/services/argocd.md
- widgets/services/atsumeru.md
- widgets/services/audiobookshelf.md
- widgets/services/authentik.md
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 81576984e..ab7dcfc92 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -988,5 +988,15 @@
"memory": "MEM",
"disk": "Disk",
"network": "NET"
+ },
+ "argocd": {
+ "apps": "Apps",
+ "synced": "Synced",
+ "outOfSync": "Out Of Sync",
+ "healthy": "Healthy",
+ "degraded": "Degraded",
+ "progressing": "Progressing",
+ "missing": "Missing",
+ "suspended": "Suspended"
}
}
diff --git a/src/utils/proxy/handlers/credentialed.js b/src/utils/proxy/handlers/credentialed.js
index eb2aab69a..8d4340c2a 100644
--- a/src/utils/proxy/handlers/credentialed.js
+++ b/src/utils/proxy/handlers/credentialed.js
@@ -36,6 +36,7 @@ export default async function credentialedProxyHandler(req, res, map) {
headers["X-gotify-Key"] = `${widget.key}`;
} else if (
[
+ "argocd",
"authentik",
"cloudflared",
"ghostfolio",
diff --git a/src/widgets/argocd/component.jsx b/src/widgets/argocd/component.jsx
new file mode 100644
index 000000000..d3b519363
--- /dev/null
+++ b/src/widgets/argocd/component.jsx
@@ -0,0 +1,52 @@
+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 { widget } = service;
+
+ if (!widget.fields) {
+ widget.fields = ["apps", "synced", "outOfSync", "healthy"];
+ }
+
+ const MAX_ALLOWED_FIELDS = 4;
+ if (widget.fields.length > MAX_ALLOWED_FIELDS) {
+ widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
+ }
+
+ const { data: appsData, error: appsError } = useWidgetAPI(widget, "applications");
+
+ const appCounts = widget.fields.map((status) => {
+ if (status === "apps") {
+ return { status, count: appsData?.items?.length };
+ }
+ const count = appsData?.items?.filter(
+ (item) =>
+ item.status?.sync?.status.toLowerCase() === status.toLowerCase() ||
+ item.status?.health?.status.toLowerCase() === status.toLowerCase(),
+ ).length;
+ return { status, count };
+ });
+
+ if (appsError) {
+ return ;
+ }
+
+ if (!appsData) {
+ return (
+
+ {appCounts.map((a) => (
+
+ ))}
+
+ );
+ }
+
+ return (
+
+ {appCounts.map((a) => (
+
+ ))}
+
+ );
+}
diff --git a/src/widgets/argocd/widget.js b/src/widgets/argocd/widget.js
new file mode 100644
index 000000000..5030adaa1
--- /dev/null
+++ b/src/widgets/argocd/widget.js
@@ -0,0 +1,14 @@
+import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
+
+const widget = {
+ api: "{url}/api/v1/{endpoint}",
+ proxyHandler: credentialedProxyHandler,
+
+ mappings: {
+ applications: {
+ endpoint: "applications",
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/components.js b/src/widgets/components.js
index 3cba84d2d..aa476c464 100644
--- a/src/widgets/components.js
+++ b/src/widgets/components.js
@@ -2,6 +2,7 @@ import dynamic from "next/dynamic";
const components = {
adguard: dynamic(() => import("./adguard/component")),
+ argocd: dynamic(() => import("./argocd/component")),
atsumeru: dynamic(() => import("./atsumeru/component")),
audiobookshelf: dynamic(() => import("./audiobookshelf/component")),
authentik: dynamic(() => import("./authentik/component")),
diff --git a/src/widgets/prometheusmetric/component.jsx b/src/widgets/prometheusmetric/component.jsx
index 347aaa0c2..350a6b7dd 100644
--- a/src/widgets/prometheusmetric/component.jsx
+++ b/src/widgets/prometheusmetric/component.jsx
@@ -5,6 +5,7 @@ import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api";
function formatValue(t, metric, rawValue) {
+ if (!metric?.format) return rawValue;
if (!rawValue) return "-";
let value = rawValue;
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index 791103789..0cad5346d 100644
--- a/src/widgets/widgets.js
+++ b/src/widgets/widgets.js
@@ -1,4 +1,5 @@
import adguard from "./adguard/widget";
+import argocd from "./argocd/widget";
import atsumeru from "./atsumeru/widget";
import audiobookshelf from "./audiobookshelf/widget";
import authentik from "./authentik/widget";
@@ -130,6 +131,7 @@ import zabbix from "./zabbix/widget";
const widgets = {
adguard,
+ argocd,
atsumeru,
audiobookshelf,
authentik,