mirror of
https://github.com/gethomepage/homepage.git
synced 2026-09-12 07:05:56 -07:00
Feature: Maintainerr service widget (#6907)
Docker CI / Docker Build & Push (push) Has been cancelled
Lint / Linting Checks (push) Has been cancelled
Release Drafter / Update Release Draft (push) Has been cancelled
Release Drafter / Auto Label PR (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Tests / vitest (2) (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled
Docker CI / Docker Build & Push (push) Has been cancelled
Lint / Linting Checks (push) Has been cancelled
Release Drafter / Update Release Draft (push) Has been cancelled
Release Drafter / Auto Label PR (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Tests / vitest (2) (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
title: Maintainerr
|
||||||
|
description: Maintainerr Widget Configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
Learn more about [Maintainerr](https://github.com/maintainerr/maintainerr).
|
||||||
|
|
||||||
|
Allowed fields: `["itemsHandled", "episodesHandled", "moviesHandled", "showsHandled", "seasonsHandled", "reclaimable", "movieReclaimable", "showReclaimable", "totalCapacity"]` (maximum of 4).
|
||||||
|
|
||||||
|
Default fields: `["itemsHandled", "episodesHandled", "moviesHandled", "reclaimable"]`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
widget:
|
||||||
|
type: maintainerr
|
||||||
|
url: http://maintainerr.host.or.ip:port
|
||||||
|
fields: ["itemsHandled", "episodesHandled", "moviesHandled", "reclaimable"] # optional
|
||||||
|
```
|
||||||
@@ -103,6 +103,7 @@ nav:
|
|||||||
- widgets/services/lubelogger.md
|
- widgets/services/lubelogger.md
|
||||||
- widgets/services/mastodon.md
|
- widgets/services/mastodon.md
|
||||||
- widgets/services/mailcow.md
|
- widgets/services/mailcow.md
|
||||||
|
- widgets/services/maintainerr.md
|
||||||
- widgets/services/mealie.md
|
- widgets/services/mealie.md
|
||||||
- widgets/services/medusa.md
|
- widgets/services/medusa.md
|
||||||
- widgets/services/mikrotik.md
|
- widgets/services/mikrotik.md
|
||||||
|
|||||||
@@ -946,6 +946,17 @@
|
|||||||
"mails": "Mails",
|
"mails": "Mails",
|
||||||
"storage": "Storage"
|
"storage": "Storage"
|
||||||
},
|
},
|
||||||
|
"maintainerr": {
|
||||||
|
"itemsHandled": "Items Handled",
|
||||||
|
"episodesHandled": "Episodes Handled",
|
||||||
|
"moviesHandled": "Movies Handled",
|
||||||
|
"showsHandled": "Shows Handled",
|
||||||
|
"seasonsHandled": "Seasons Handled",
|
||||||
|
"reclaimable": "Reclaimable",
|
||||||
|
"movieReclaimable": "Movie Reclaimable",
|
||||||
|
"showReclaimable": "Show Reclaimable",
|
||||||
|
"totalCapacity": "Total Capacity"
|
||||||
|
},
|
||||||
"netdata": {
|
"netdata": {
|
||||||
"warnings": "Warnings",
|
"warnings": "Warnings",
|
||||||
"criticals": "Criticals"
|
"criticals": "Criticals"
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ const components = {
|
|||||||
linkwarden: dynamic(() => import("./linkwarden/component")),
|
linkwarden: dynamic(() => import("./linkwarden/component")),
|
||||||
lubelogger: dynamic(() => import("./lubelogger/component")),
|
lubelogger: dynamic(() => import("./lubelogger/component")),
|
||||||
mailcow: dynamic(() => import("./mailcow/component")),
|
mailcow: dynamic(() => import("./mailcow/component")),
|
||||||
|
maintainerr: dynamic(() => import("./maintainerr/component")),
|
||||||
mastodon: dynamic(() => import("./mastodon/component")),
|
mastodon: dynamic(() => import("./mastodon/component")),
|
||||||
mealie: dynamic(() => import("./mealie/component")),
|
mealie: dynamic(() => import("./mealie/component")),
|
||||||
medusa: dynamic(() => import("./medusa/component")),
|
medusa: dynamic(() => import("./medusa/component")),
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import Block from "components/services/widget/block";
|
||||||
|
import Container from "components/services/widget/container";
|
||||||
|
import { useTranslation } from "next-i18next/pages";
|
||||||
|
|
||||||
|
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||||
|
|
||||||
|
const DEFAULT_FIELDS = ["itemsHandled", "episodesHandled", "moviesHandled", "reclaimable"];
|
||||||
|
const MAX_FIELDS = 4;
|
||||||
|
|
||||||
|
export default function Component({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { widget } = service;
|
||||||
|
|
||||||
|
if (!widget.fields?.length) {
|
||||||
|
widget.fields = DEFAULT_FIELDS;
|
||||||
|
} else if (widget.fields.length > MAX_FIELDS) {
|
||||||
|
widget.fields = widget.fields.slice(0, MAX_FIELDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = useWidgetAPI(widget);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <Container service={service} error={error} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="maintainerr.itemsHandled" />
|
||||||
|
<Block label="maintainerr.episodesHandled" />
|
||||||
|
<Block label="maintainerr.moviesHandled" />
|
||||||
|
<Block label="maintainerr.showsHandled" />
|
||||||
|
<Block label="maintainerr.seasonsHandled" />
|
||||||
|
<Block label="maintainerr.reclaimable" />
|
||||||
|
<Block label="maintainerr.movieReclaimable" />
|
||||||
|
<Block label="maintainerr.showReclaimable" />
|
||||||
|
<Block label="maintainerr.totalCapacity" />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block
|
||||||
|
label="maintainerr.itemsHandled"
|
||||||
|
value={t("common.number", { value: data.cleanupTotals?.itemsHandled ?? 0 })}
|
||||||
|
/>
|
||||||
|
<Block
|
||||||
|
label="maintainerr.episodesHandled"
|
||||||
|
value={t("common.number", { value: data.cleanupTotals?.episodesHandled ?? 0 })}
|
||||||
|
/>
|
||||||
|
<Block
|
||||||
|
label="maintainerr.moviesHandled"
|
||||||
|
value={t("common.number", { value: data.cleanupTotals?.moviesHandled ?? 0 })}
|
||||||
|
/>
|
||||||
|
<Block
|
||||||
|
label="maintainerr.showsHandled"
|
||||||
|
value={t("common.number", { value: data.cleanupTotals?.showsHandled ?? 0 })}
|
||||||
|
/>
|
||||||
|
<Block
|
||||||
|
label="maintainerr.seasonsHandled"
|
||||||
|
value={t("common.number", { value: data.cleanupTotals?.seasonsHandled ?? 0 })}
|
||||||
|
/>
|
||||||
|
<Block
|
||||||
|
label="maintainerr.reclaimable"
|
||||||
|
value={t("common.bytes", { value: data.collectionSummary?.activeSizeBytes ?? 0 })}
|
||||||
|
/>
|
||||||
|
<Block
|
||||||
|
label="maintainerr.movieReclaimable"
|
||||||
|
value={t("common.bytes", { value: data.collectionSummary?.movieSizeBytes ?? 0 })}
|
||||||
|
/>
|
||||||
|
<Block
|
||||||
|
label="maintainerr.showReclaimable"
|
||||||
|
value={t("common.bytes", { value: data.collectionSummary?.showSizeBytes ?? 0 })}
|
||||||
|
/>
|
||||||
|
<Block label="maintainerr.totalCapacity" value={t("common.bytes", { value: data.totals?.totalSpace ?? 0 })} />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
|
import { screen } from "@testing-library/react";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||||
|
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||||
|
|
||||||
|
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||||
|
|
||||||
|
vi.mock("utils/proxy/use-widget-api", () => ({
|
||||||
|
default: useWidgetAPI,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import Component from "./component";
|
||||||
|
|
||||||
|
describe("widgets/maintainerr/component", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders placeholders for the default fields while loading", () => {
|
||||||
|
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
|
||||||
|
|
||||||
|
const { container } = renderWithProviders(<Component service={{ widget: { type: "maintainerr" } }} />, {
|
||||||
|
settings: { hideErrors: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
|
||||||
|
expect(screen.getByText("maintainerr.itemsHandled")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("maintainerr.episodesHandled")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("maintainerr.moviesHandled")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("maintainerr.reclaimable")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the default storage metrics", () => {
|
||||||
|
useWidgetAPI.mockReturnValue({
|
||||||
|
data: {
|
||||||
|
cleanupTotals: {
|
||||||
|
itemsHandled: 12,
|
||||||
|
episodesHandled: 7,
|
||||||
|
moviesHandled: 5,
|
||||||
|
},
|
||||||
|
collectionSummary: {
|
||||||
|
activeSizeBytes: 1_500_000_000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { container } = renderWithProviders(<Component service={{ widget: { type: "maintainerr" } }} />, {
|
||||||
|
settings: { hideErrors: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
expectBlockValue(container, "maintainerr.itemsHandled", 12);
|
||||||
|
expectBlockValue(container, "maintainerr.episodesHandled", 7);
|
||||||
|
expectBlockValue(container, "maintainerr.moviesHandled", 5);
|
||||||
|
expectBlockValue(container, "maintainerr.reclaimable", 1_500_000_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders configured optional fields", () => {
|
||||||
|
useWidgetAPI.mockReturnValue({
|
||||||
|
data: {
|
||||||
|
cleanupTotals: {
|
||||||
|
showsHandled: 3,
|
||||||
|
seasonsHandled: 8,
|
||||||
|
},
|
||||||
|
collectionSummary: {
|
||||||
|
movieSizeBytes: 1_000_000_000,
|
||||||
|
},
|
||||||
|
totals: {
|
||||||
|
totalSpace: 2_000_000_000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const service = {
|
||||||
|
widget: {
|
||||||
|
type: "maintainerr",
|
||||||
|
fields: ["showsHandled", "seasonsHandled", "movieReclaimable", "totalCapacity"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const { container } = renderWithProviders(<Component service={service} />, {
|
||||||
|
settings: { hideErrors: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
|
||||||
|
expectBlockValue(container, "maintainerr.showsHandled", 3);
|
||||||
|
expectBlockValue(container, "maintainerr.seasonsHandled", 8);
|
||||||
|
expectBlockValue(container, "maintainerr.movieReclaimable", 1_000_000_000);
|
||||||
|
expectBlockValue(container, "maintainerr.totalCapacity", 2_000_000_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("limits configured fields to the first four", () => {
|
||||||
|
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
|
||||||
|
|
||||||
|
const service = {
|
||||||
|
widget: {
|
||||||
|
type: "maintainerr",
|
||||||
|
fields: ["itemsHandled", "episodesHandled", "moviesHandled", "showsHandled", "seasonsHandled"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const { container } = renderWithProviders(<Component service={service} />, {
|
||||||
|
settings: { hideErrors: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
|
||||||
|
expect(screen.getByText("maintainerr.showsHandled")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("maintainerr.seasonsHandled")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders zero for missing metrics", () => {
|
||||||
|
useWidgetAPI.mockReturnValue({ data: {}, error: undefined });
|
||||||
|
|
||||||
|
const { container } = renderWithProviders(<Component service={{ widget: { type: "maintainerr" } }} />, {
|
||||||
|
settings: { hideErrors: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
expectBlockValue(container, "maintainerr.itemsHandled", 0);
|
||||||
|
expectBlockValue(container, "maintainerr.episodesHandled", 0);
|
||||||
|
expectBlockValue(container, "maintainerr.moviesHandled", 0);
|
||||||
|
expectBlockValue(container, "maintainerr.reclaimable", 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the error state", () => {
|
||||||
|
useWidgetAPI.mockReturnValue({ data: undefined, error: { message: "boom" } });
|
||||||
|
|
||||||
|
renderWithProviders(<Component service={{ widget: { type: "maintainerr" } }} />, {
|
||||||
|
settings: { hideErrors: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText("boom")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
api: "{url}/api/storage-metrics",
|
||||||
|
proxyHandler: genericProxyHandler,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default widget;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { describe, it } from "vitest";
|
||||||
|
|
||||||
|
import { expectWidgetConfigShape } from "test-utils/widget-config";
|
||||||
|
|
||||||
|
import widget from "./widget";
|
||||||
|
|
||||||
|
describe("maintainerr widget config", () => {
|
||||||
|
it("exports a valid widget config", () => {
|
||||||
|
expectWidgetConfigShape(widget);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -67,6 +67,7 @@ import lidarr from "./lidarr/widget";
|
|||||||
import linkwarden from "./linkwarden/widget";
|
import linkwarden from "./linkwarden/widget";
|
||||||
import lubelogger from "./lubelogger/widget";
|
import lubelogger from "./lubelogger/widget";
|
||||||
import mailcow from "./mailcow/widget";
|
import mailcow from "./mailcow/widget";
|
||||||
|
import maintainerr from "./maintainerr/widget";
|
||||||
import mastodon from "./mastodon/widget";
|
import mastodon from "./mastodon/widget";
|
||||||
import mealie from "./mealie/widget";
|
import mealie from "./mealie/widget";
|
||||||
import medusa from "./medusa/widget";
|
import medusa from "./medusa/widget";
|
||||||
@@ -229,6 +230,7 @@ const widgets = {
|
|||||||
linkwarden,
|
linkwarden,
|
||||||
lubelogger,
|
lubelogger,
|
||||||
mailcow,
|
mailcow,
|
||||||
|
maintainerr,
|
||||||
mastodon,
|
mastodon,
|
||||||
mealie,
|
mealie,
|
||||||
medusa,
|
medusa,
|
||||||
|
|||||||
Reference in New Issue
Block a user