mirror of
https://github.com/gethomepage/homepage.git
synced 2026-09-12 07:05:56 -07:00
Feature: sportarr service widget (#6906)
Co-authored-by: Sportarr <sportarr@users.noreply.github.com> Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
@@ -139,7 +139,7 @@ Each widget can optionally provide a list of which fields should be visible via
|
|||||||
|
|
||||||
### Block Highlighting
|
### Block Highlighting
|
||||||
|
|
||||||
Widgets can tint their metric block text automatically based on rules defined alongside the service. Attach a `highlight` section to the widget configuration and map each block to one or more numeric or string rules using the field key (for example, `queued`, `lan_users`).
|
Widgets can tint their metric block text automatically based on rules defined alongside the service. Attach a `highlight` section to the widget configuration and map each block to one or more numeric or string rules using the field key (for example, `queued`, `lan_users`). The custom api widget does not support highlighting.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- Sonarr:
|
- Sonarr:
|
||||||
|
|||||||
@@ -146,6 +146,10 @@ The widget supports different display modes that can be set using the `display`
|
|||||||
|
|
||||||
The default display mode is `block`, which shows fields in a block format.
|
The default display mode is `block`, which shows fields in a block format.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
|
||||||
|
The Custom API widget does not currently support [block highlighting](../../configs/services.md#block-highlighting) in any display mode.
|
||||||
|
|
||||||
### List View
|
### List View
|
||||||
|
|
||||||
You can change the default block view to a list view by setting the `display` option to `list`.
|
You can change the default block view to a list view by setting the `display` option to `list`.
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
title: Sportarr
|
||||||
|
description: Sportarr Widget Configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
Learn more about [Sportarr](https://github.com/Sportarr/Sportarr).
|
||||||
|
|
||||||
|
Find your API key under `Settings > General`.
|
||||||
|
|
||||||
|
Allowed fields: `["wanted", "queued", "leagues"]`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
widget:
|
||||||
|
type: sportarr
|
||||||
|
url: http://sportarr.host.or.ip:1867
|
||||||
|
key: sportarrapikeyapikeyapikeyapikeyapikey
|
||||||
|
```
|
||||||
@@ -159,6 +159,7 @@ nav:
|
|||||||
- widgets/services/sparkyfitness.md
|
- widgets/services/sparkyfitness.md
|
||||||
- widgets/services/speedtest-tracker.md
|
- widgets/services/speedtest-tracker.md
|
||||||
- widgets/services/spoolman.md
|
- widgets/services/spoolman.md
|
||||||
|
- widgets/services/sportarr.md
|
||||||
- widgets/services/stash.md
|
- widgets/services/stash.md
|
||||||
- widgets/services/stocks.md
|
- widgets/services/stocks.md
|
||||||
- widgets/services/suwayomi.md
|
- widgets/services/suwayomi.md
|
||||||
|
|||||||
@@ -1230,5 +1230,10 @@
|
|||||||
"synced": "Synced",
|
"synced": "Synced",
|
||||||
"errors": "Errors",
|
"errors": "Errors",
|
||||||
"storage": "Storage"
|
"storage": "Storage"
|
||||||
|
},
|
||||||
|
"sportarr": {
|
||||||
|
"wanted": "Wanted",
|
||||||
|
"queued": "Queued",
|
||||||
|
"leagues": "Leagues"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ const components = {
|
|||||||
sparkyfitness: dynamic(() => import("./sparkyfitness/component")),
|
sparkyfitness: dynamic(() => import("./sparkyfitness/component")),
|
||||||
speedtest: dynamic(() => import("./speedtest/component")),
|
speedtest: dynamic(() => import("./speedtest/component")),
|
||||||
spoolman: dynamic(() => import("./spoolman/component")),
|
spoolman: dynamic(() => import("./spoolman/component")),
|
||||||
|
sportarr: dynamic(() => import("./sportarr/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")),
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
export default function Component({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { widget } = service;
|
||||||
|
|
||||||
|
const { data: wantedData, error: wantedError } = useWidgetAPI(widget, "wanted/missing", { page: 1, pageSize: 1 });
|
||||||
|
const { data: queueData, error: queueError } = useWidgetAPI(widget, "queue");
|
||||||
|
const { data: leaguesData, error: leaguesError } = useWidgetAPI(widget, "leagues");
|
||||||
|
|
||||||
|
if (wantedError || queueError || leaguesError) {
|
||||||
|
const finalError = wantedError ?? queueError ?? leaguesError;
|
||||||
|
return <Container service={service} error={finalError} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wantedData || !queueData || !leaguesData) {
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="sportarr.wanted" />
|
||||||
|
<Block label="sportarr.queued" />
|
||||||
|
<Block label="sportarr.leagues" />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="sportarr.wanted" value={t("common.number", { value: wantedData.totalRecords })} />
|
||||||
|
<Block label="sportarr.queued" value={t("common.number", { value: queueData.total })} />
|
||||||
|
<Block label="sportarr.leagues" value={t("common.number", { value: leaguesData.total })} />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// @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/sportarr/component", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders placeholders while loading", () => {
|
||||||
|
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
|
||||||
|
|
||||||
|
const { container } = renderWithProviders(<Component service={{ widget: { type: "sportarr" } }} />, {
|
||||||
|
settings: { hideErrors: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
|
||||||
|
expect(screen.getByText("sportarr.wanted")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("sportarr.queued")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("sportarr.leagues")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders counts when all endpoints resolve", () => {
|
||||||
|
useWidgetAPI.mockImplementation((_widget, endpoint) => {
|
||||||
|
if (endpoint === "wanted/missing") return { data: { totalRecords: 4 }, error: undefined };
|
||||||
|
if (endpoint === "queue") return { data: { total: 2 }, error: undefined };
|
||||||
|
if (endpoint === "leagues") return { data: { total: 7 }, error: undefined };
|
||||||
|
return { data: undefined, error: undefined };
|
||||||
|
});
|
||||||
|
|
||||||
|
const { container } = renderWithProviders(<Component service={{ widget: { type: "sportarr" } }} />, {
|
||||||
|
settings: { hideErrors: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
expectBlockValue(container, "sportarr.wanted", 4);
|
||||||
|
expectBlockValue(container, "sportarr.queued", 2);
|
||||||
|
expectBlockValue(container, "sportarr.leagues", 7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the error state when an endpoint fails", () => {
|
||||||
|
useWidgetAPI.mockImplementation((_widget, endpoint) => {
|
||||||
|
if (endpoint === "queue") return { data: undefined, error: { message: "unreachable" } };
|
||||||
|
return { data: undefined, error: undefined };
|
||||||
|
});
|
||||||
|
|
||||||
|
renderWithProviders(<Component service={{ widget: { type: "sportarr" } }} />, {
|
||||||
|
settings: { hideErrors: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { asJson } from "utils/proxy/api-helpers";
|
||||||
|
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
api: "{url}/api/{endpoint}?apikey={key}",
|
||||||
|
proxyHandler: genericProxyHandler,
|
||||||
|
|
||||||
|
mappings: {
|
||||||
|
"wanted/missing": {
|
||||||
|
endpoint: "wanted/missing",
|
||||||
|
params: ["page", "pageSize"],
|
||||||
|
validate: ["totalRecords"],
|
||||||
|
},
|
||||||
|
queue: {
|
||||||
|
endpoint: "queue",
|
||||||
|
map: (data) => ({ total: asJson(data).length }),
|
||||||
|
},
|
||||||
|
leagues: {
|
||||||
|
endpoint: "leagues",
|
||||||
|
map: (data) => ({ total: asJson(data).length }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default widget;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { expectWidgetConfigShape } from "test-utils/widget-config";
|
||||||
|
|
||||||
|
import widget from "./widget";
|
||||||
|
|
||||||
|
describe("sportarr widget config", () => {
|
||||||
|
it("exports a valid widget config", () => {
|
||||||
|
expectWidgetConfigShape(widget);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps the queue response to a total count", () => {
|
||||||
|
const result = widget.mappings.queue.map(Buffer.from(JSON.stringify([{ id: 1 }, { id: 2 }, { id: 3 }])));
|
||||||
|
|
||||||
|
expect(result).toEqual({ total: 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps the leagues response to a total count", () => {
|
||||||
|
const result = widget.mappings.leagues.map(Buffer.from(JSON.stringify([{ id: 1, name: "Formula 1" }])));
|
||||||
|
|
||||||
|
expect(result).toEqual({ total: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates totalRecords on the wanted endpoint", () => {
|
||||||
|
expect(widget.mappings["wanted/missing"].validate).toEqual(["totalRecords"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -123,6 +123,7 @@ import sonarr from "./sonarr/widget";
|
|||||||
import sparkyfitness from "./sparkyfitness/widget";
|
import sparkyfitness from "./sparkyfitness/widget";
|
||||||
import speedtest from "./speedtest/widget";
|
import speedtest from "./speedtest/widget";
|
||||||
import spoolman from "./spoolman/widget";
|
import spoolman from "./spoolman/widget";
|
||||||
|
import sportarr from "./sportarr/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";
|
||||||
@@ -286,6 +287,7 @@ const widgets = {
|
|||||||
sparkyfitness,
|
sparkyfitness,
|
||||||
speedtest,
|
speedtest,
|
||||||
spoolman,
|
spoolman,
|
||||||
|
sportarr,
|
||||||
stash,
|
stash,
|
||||||
stocks,
|
stocks,
|
||||||
strelaysrv,
|
strelaysrv,
|
||||||
|
|||||||
Reference in New Issue
Block a user