Merge branch 'dev' into feature/gitlab-widget

# Conflicts:
#	public/locales/en/common.json
This commit is contained in:
Urs Kröll 2024-11-22 00:55:51 +01:00
commit 6dddb1c031
No known key found for this signature in database
GPG Key ID: D93E3DE7D924C315
16 changed files with 233 additions and 30 deletions

View File

@ -26,8 +26,6 @@ on:
merge_group: merge_group:
env: env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo> # github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }} IMAGE_NAME: ${{ github.repository }}
@ -66,14 +64,6 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
# Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@main
with:
cosign-release: 'v1.13.1' # optional
# Setup QEMU # Setup QEMU
# https://github.com/marketplace/actions/docker-setup-buildx#with-qemu # https://github.com/marketplace/actions/docker-setup-buildx#with-qemu
- name: Setup QEMU - name: Setup QEMU
@ -99,9 +89,15 @@ jobs:
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker # Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action # https://github.com/docker/metadata-action
@ -109,7 +105,9 @@ jobs:
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: |
${{ env.IMAGE_NAME }}
ghcr.io/${{ env.IMAGE_NAME }}
flavor: | flavor: |
latest=auto latest=auto
@ -133,19 +131,6 @@ jobs:
cache-from: type=local,src=/tmp/.buildx-cache cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
# Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker
# repository is public to avoid leaking data. If you would like to publish
# transparency data even for private images, pass --force to cosign below.
# https://github.com/sigstore/cosign
# - name: Sign the published Docker image
# if: ${{ github.event_name != 'pull_request' }}
# env:
# COSIGN_EXPERIMENTAL: "true"
# # This step uses the identity token to provision an ephemeral certificate
# # against the sigstore community Fulcio instance.
# run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }}
# Temp fix # Temp fix
# https://github.com/docker/build-push-action/issues/252 # https://github.com/docker/build-push-action/issues/252
# https://github.com/moby/buildkit/issues/1896 # https://github.com/moby/buildkit/issues/1896

View File

@ -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.

View File

@ -8,6 +8,7 @@ search:
You can also find a list of all available service widgets in the sidebar navigation. You can also find a list of all available service widgets in the sidebar navigation.
- [Adguard Home](adguard-home.md) - [Adguard Home](adguard-home.md)
- [ArgoCD](argocd.md)
- [Atsumeru](atsumeru.md) - [Atsumeru](atsumeru.md)
- [Audiobookshelf](audiobookshelf.md) - [Audiobookshelf](audiobookshelf.md)
- [Authentik](authentik.md) - [Authentik](authentik.md)

View File

@ -0,0 +1,15 @@
---
title: Spoolman
description: Spoolman Widget Configuration
---
Learn more about [Spoolman](https://github.com/Donkie/Spoolman).
4 spools are displayed by default. If more than 4 spools are configured in spoolman you can use the spoolIds configuration option to control which are displayed.
```yaml
widget:
type: spoolman
url: http://spoolman.host.or.ip
spoolIds: [1, 2, 3, 4] # optional
```

View File

@ -31,6 +31,7 @@ nav:
- "Service Widgets": - "Service Widgets":
- widgets/services/index.md - widgets/services/index.md
- widgets/services/adguard-home.md - widgets/services/adguard-home.md
- widgets/services/argocd.md
- widgets/services/atsumeru.md - widgets/services/atsumeru.md
- widgets/services/audiobookshelf.md - widgets/services/audiobookshelf.md
- widgets/services/authentik.md - widgets/services/authentik.md
@ -138,6 +139,7 @@ nav:
- widgets/services/scrutiny.md - widgets/services/scrutiny.md
- widgets/services/sonarr.md - widgets/services/sonarr.md
- widgets/services/speedtest-tracker.md - widgets/services/speedtest-tracker.md
- widgets/services/spoolman.md
- widgets/services/stash.md - widgets/services/stash.md
- widgets/services/stocks.md - widgets/services/stocks.md
- widgets/services/swagdashboard.md - widgets/services/swagdashboard.md

View File

@ -989,6 +989,19 @@
"disk": "Disk", "disk": "Disk",
"network": "NET" "network": "NET"
}, },
"argocd": {
"apps": "Apps",
"synced": "Synced",
"outOfSync": "Out Of Sync",
"healthy": "Healthy",
"degraded": "Degraded",
"progressing": "Progressing",
"missing": "Missing",
"suspended": "Suspended"
},
"spoolman": {
"loading": "Loading"
},
"gitlab": { "gitlab": {
"events": "Events", "events": "Events",
"issues": "Issues", "issues": "Issues",

View File

@ -492,6 +492,9 @@ export function cleanServiceGroups(groups) {
// technitium // technitium
range, range,
// spoolman
spoolIds,
} = cleanedService.widget; } = cleanedService.widget;
let fieldsList = fields; let fieldsList = fields;
@ -653,6 +656,9 @@ export function cleanServiceGroups(groups) {
if (metrics) cleanedService.widget.metrics = metrics; if (metrics) cleanedService.widget.metrics = metrics;
if (refreshInterval) cleanedService.widget.refreshInterval = refreshInterval; if (refreshInterval) cleanedService.widget.refreshInterval = refreshInterval;
} }
if (type === "spoolman") {
if (spoolIds !== undefined) cleanedService.widget.spoolIds = spoolIds;
}
} }
return cleanedService; return cleanedService;

View File

@ -36,6 +36,7 @@ export default async function credentialedProxyHandler(req, res, map) {
headers["X-gotify-Key"] = `${widget.key}`; headers["X-gotify-Key"] = `${widget.key}`;
} else if ( } else if (
[ [
"argocd",
"authentik", "authentik",
"cloudflared", "cloudflared",
"ghostfolio", "ghostfolio",

View File

@ -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 <Container service={service} error={appsError} />;
}
if (!appsData) {
return (
<Container service={service}>
{appCounts.map((a) => (
<Block label={`argocd.${a.status}`} key={a.status} />
))}
</Container>
);
}
return (
<Container service={service}>
{appCounts.map((a) => (
<Block label={`argocd.${a.status}`} key={a.status} value={a.count} />
))}
</Container>
);
}

View File

@ -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;

View File

@ -46,11 +46,8 @@ export default function Component({ service }) {
<Block label="audiobookshelf.books" value={t("common.number", { value: totalBooks })} /> <Block label="audiobookshelf.books" value={t("common.number", { value: totalBooks })} />
<Block <Block
label="audiobookshelf.booksDuration" label="audiobookshelf.booksDuration"
value={t("common.number", { value={t("common.duration", {
value: totalBooksDuration / 60, value: totalBooksDuration,
maximumFractionDigits: 0,
style: "unit",
unit: "minute",
})} })}
/> />
</Container> </Container>

View File

@ -2,6 +2,7 @@ import dynamic from "next/dynamic";
const components = { const components = {
adguard: dynamic(() => import("./adguard/component")), adguard: dynamic(() => import("./adguard/component")),
argocd: dynamic(() => import("./argocd/component")),
atsumeru: dynamic(() => import("./atsumeru/component")), atsumeru: dynamic(() => import("./atsumeru/component")),
audiobookshelf: dynamic(() => import("./audiobookshelf/component")), audiobookshelf: dynamic(() => import("./audiobookshelf/component")),
authentik: dynamic(() => import("./authentik/component")), authentik: dynamic(() => import("./authentik/component")),
@ -111,6 +112,7 @@ const components = {
scrutiny: dynamic(() => import("./scrutiny/component")), scrutiny: dynamic(() => import("./scrutiny/component")),
sonarr: dynamic(() => import("./sonarr/component")), sonarr: dynamic(() => import("./sonarr/component")),
speedtest: dynamic(() => import("./speedtest/component")), speedtest: dynamic(() => import("./speedtest/component")),
spoolman: dynamic(() => import("./spoolman/component")),
stash: dynamic(() => import("./stash/component")), stash: dynamic(() => import("./stash/component")),
stocks: dynamic(() => import("./stocks/component")), stocks: dynamic(() => import("./stocks/component")),
strelaysrv: dynamic(() => import("./strelaysrv/component")), strelaysrv: dynamic(() => import("./strelaysrv/component")),

View File

@ -5,6 +5,7 @@ import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
function formatValue(t, metric, rawValue) { function formatValue(t, metric, rawValue) {
if (!metric?.format) return rawValue;
if (!rawValue) return "-"; if (!rawValue) return "-";
let value = rawValue; let value = rawValue;

View File

@ -0,0 +1,63 @@
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;
// eslint-disable-next-line prefer-const
let { data: spoolData, error: spoolError } = useWidgetAPI(widget, "spools");
if (spoolError) {
return <Container service={service} error={spoolError} />;
}
if (!spoolData) {
const nBlocksGuess = widget.spoolIds?.length ?? 4;
return (
<Container service={service}>
{[...Array(nBlocksGuess)].map((_, i) => (
// eslint-disable-next-line react/no-array-index-key
<Block key={i} label="spoolman.loading" />
))}
</Container>
);
}
if (spoolData.error || spoolData.message) {
return <Container service={service} error={spoolData?.error ?? spoolData} />;
}
if (spoolData.length === 0) {
return (
<Container service={service}>
<Block label="spoolman.noSpools" />
</Container>
);
}
if (widget.spoolIds?.length) {
spoolData = spoolData.filter((spool) => widget.spoolIds.includes(spool.id));
}
if (spoolData.length > 4) {
spoolData = spoolData.slice(0, 4);
}
return (
<Container service={service}>
{spoolData.map((spool) => (
<Block
key={spool.id}
label={spool.filament.name}
value={t("common.percent", {
value: (spool.remaining_weight / spool.initial_weight) * 100,
})}
/>
))}
</Container>
);
}

View File

@ -0,0 +1,14 @@
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = {
api: "{url}/api/v1/{endpoint}",
proxyHandler: credentialedProxyHandler,
mappings: {
spools: {
endpoint: "spool",
},
},
};
export default widget;

View File

@ -1,4 +1,5 @@
import adguard from "./adguard/widget"; import adguard from "./adguard/widget";
import argocd from "./argocd/widget";
import atsumeru from "./atsumeru/widget"; import atsumeru from "./atsumeru/widget";
import audiobookshelf from "./audiobookshelf/widget"; import audiobookshelf from "./audiobookshelf/widget";
import authentik from "./authentik/widget"; import authentik from "./authentik/widget";
@ -102,6 +103,7 @@ import sabnzbd from "./sabnzbd/widget";
import scrutiny from "./scrutiny/widget"; import scrutiny from "./scrutiny/widget";
import sonarr from "./sonarr/widget"; import sonarr from "./sonarr/widget";
import speedtest from "./speedtest/widget"; import speedtest from "./speedtest/widget";
import spoolman from "./spoolman/widget";
import stash from "./stash/widget"; import stash from "./stash/widget";
import stocks from "./stocks/widget"; import stocks from "./stocks/widget";
import strelaysrv from "./strelaysrv/widget"; import strelaysrv from "./strelaysrv/widget";
@ -131,6 +133,7 @@ import zabbix from "./zabbix/widget";
const widgets = { const widgets = {
adguard, adguard,
argocd,
atsumeru, atsumeru,
audiobookshelf, audiobookshelf,
authentik, authentik,
@ -237,6 +240,7 @@ const widgets = {
scrutiny, scrutiny,
sonarr, sonarr,
speedtest, speedtest,
spoolman,
stash, stash,
stocks, stocks,
strelaysrv, strelaysrv,