mirror of
https://github.com/gethomepage/homepage.git
synced 2026-08-31 01:15:53 -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/mastodon.md
|
||||
- widgets/services/mailcow.md
|
||||
- widgets/services/maintainerr.md
|
||||
- widgets/services/mealie.md
|
||||
- widgets/services/medusa.md
|
||||
- widgets/services/mikrotik.md
|
||||
|
||||
@@ -946,6 +946,17 @@
|
||||
"mails": "Mails",
|
||||
"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": {
|
||||
"warnings": "Warnings",
|
||||
"criticals": "Criticals"
|
||||
|
||||
@@ -76,6 +76,7 @@ const components = {
|
||||
linkwarden: dynamic(() => import("./linkwarden/component")),
|
||||
lubelogger: dynamic(() => import("./lubelogger/component")),
|
||||
mailcow: dynamic(() => import("./mailcow/component")),
|
||||
maintainerr: dynamic(() => import("./maintainerr/component")),
|
||||
mastodon: dynamic(() => import("./mastodon/component")),
|
||||
mealie: dynamic(() => import("./mealie/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 lubelogger from "./lubelogger/widget";
|
||||
import mailcow from "./mailcow/widget";
|
||||
import maintainerr from "./maintainerr/widget";
|
||||
import mastodon from "./mastodon/widget";
|
||||
import mealie from "./mealie/widget";
|
||||
import medusa from "./medusa/widget";
|
||||
@@ -229,6 +230,7 @@ const widgets = {
|
||||
linkwarden,
|
||||
lubelogger,
|
||||
mailcow,
|
||||
maintainerr,
|
||||
mastodon,
|
||||
mealie,
|
||||
medusa,
|
||||
|
||||
Reference in New Issue
Block a user