From d11ca0d70e91075c52bb0113418a16e90327bc5b Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Fri, 21 Aug 2026 12:29:01 -0700 Subject: [PATCH] Yay drop all these max fields --- docs/widgets/authoring/getting-started.md | 1 + src/widgets/arcane/component.jsx | 3 +-- src/widgets/backrest/component.jsx | 3 +-- src/widgets/dockhand/component.jsx | 3 +-- src/widgets/fritzbox/component.jsx | 3 +-- src/widgets/karakeep/component.jsx | 3 +-- src/widgets/komodo/component.jsx | 3 +-- src/widgets/maintainerr/component.jsx | 3 +-- src/widgets/pangolin/component.jsx | 3 +-- src/widgets/romm/component.jsx | 3 +-- src/widgets/seerr/component.jsx | 3 +-- src/widgets/slskd/component.jsx | 3 +-- src/widgets/tailscale/component.jsx | 3 +-- src/widgets/technitium/component.jsx | 4 +--- src/widgets/unraid/component.jsx | 11 +++-------- src/widgets/unraid/component.test.jsx | 9 ++++++--- src/widgets/wallos/component.jsx | 3 +-- 17 files changed, 24 insertions(+), 40 deletions(-) diff --git a/docs/widgets/authoring/getting-started.md b/docs/widgets/authoring/getting-started.md index d93b01862..27fbb0570 100644 --- a/docs/widgets/authoring/getting-started.md +++ b/docs/widgets/authoring/getting-started.md @@ -85,6 +85,7 @@ To ensure cohesiveness of various widgets, the following should be used as a gui - Note that we reserve the right to decline widgets for projects that are very young (eg < ~1y) or those with a small reach (eg low GitHub stars). Again, this is in an effort to keep overall widget maintenance under control. - Widgets should be only one row of blocks - Widgets should be no more than 4 blocks wide and generally conform to the styling / design choices of other widgets +- Use `withWidgetFields(service, defaultFields)` from `utils/widget-fields` to apply default fields and cap configured fields without mutating the service. The helper limits widgets to 4 fields. - Minimize the number of API calls - Avoid the use of custom proxy unless absolutely necessary - Widgets should be 'read-only', as in they should not make write changes using the relevant tool's API. Homepage widgets are designed to surface information, not to be a (usually worse) replacement for the tool itself. diff --git a/src/widgets/arcane/component.jsx b/src/widgets/arcane/component.jsx index 9f6d416e2..cdd14e300 100644 --- a/src/widgets/arcane/component.jsx +++ b/src/widgets/arcane/component.jsx @@ -5,12 +5,11 @@ import { useTranslation } from "next-i18next/pages"; import useWidgetAPI from "utils/proxy/use-widget-api"; import withWidgetFields from "utils/widget-fields"; -const MAX_FIELDS = 4; const DEFAULT_FIELDS = ["running", "stopped", "total", "image_updates"]; export default function Component({ service: configuredService }) { const { t } = useTranslation(); - const service = withWidgetFields(configuredService, DEFAULT_FIELDS, MAX_FIELDS); + const service = withWidgetFields(configuredService, DEFAULT_FIELDS); const { widget } = service; const envNotSet = widget.env == null || widget.env === ""; diff --git a/src/widgets/backrest/component.jsx b/src/widgets/backrest/component.jsx index 20e6745fd..0ab2ca483 100644 --- a/src/widgets/backrest/component.jsx +++ b/src/widgets/backrest/component.jsx @@ -6,12 +6,11 @@ import useWidgetAPI from "utils/proxy/use-widget-api"; import withWidgetFields from "utils/widget-fields"; const BACKREST_DEFAULT_FIELDS = ["num_success_latest", "num_failure_latest", "num_failure_30", "bytes_added_30"]; -const MAX_ALLOWED_FIELDS = 4; export default function Component({ service: configuredService }) { const { t } = useTranslation(); - const service = withWidgetFields(configuredService, BACKREST_DEFAULT_FIELDS, MAX_ALLOWED_FIELDS); + const service = withWidgetFields(configuredService, BACKREST_DEFAULT_FIELDS); const { widget } = service; const { data, error } = useWidgetAPI(widget, "summary"); diff --git a/src/widgets/dockhand/component.jsx b/src/widgets/dockhand/component.jsx index 6a5bdcc0e..e72132f5d 100644 --- a/src/widgets/dockhand/component.jsx +++ b/src/widgets/dockhand/component.jsx @@ -5,12 +5,11 @@ import { useTranslation } from "next-i18next/pages"; import useWidgetAPI from "utils/proxy/use-widget-api"; import withWidgetFields from "utils/widget-fields"; -const MAX_FIELDS = 4; const DEFAULT_FIELDS = ["running", "total", "cpu", "memory"]; export default function Component({ service: configuredService }) { const { t } = useTranslation(); - const service = withWidgetFields(configuredService, DEFAULT_FIELDS, MAX_FIELDS); + const service = withWidgetFields(configuredService, DEFAULT_FIELDS); const { widget } = service; const { data: stats, error: statsError } = useWidgetAPI(widget, "dashboard/stats"); diff --git a/src/widgets/fritzbox/component.jsx b/src/widgets/fritzbox/component.jsx index 3eda567e2..1f7f6c576 100644 --- a/src/widgets/fritzbox/component.jsx +++ b/src/widgets/fritzbox/component.jsx @@ -6,11 +6,10 @@ import useWidgetAPI from "utils/proxy/use-widget-api"; import withWidgetFields from "utils/widget-fields"; export const fritzboxDefaultFields = ["connectionStatus", "uptime", "maxDown", "maxUp"]; -const MAX_ALLOWED_FIELDS = 4; export default function Component({ service: configuredService }) { const { t } = useTranslation(); - const service = withWidgetFields(configuredService, fritzboxDefaultFields, MAX_ALLOWED_FIELDS); + const service = withWidgetFields(configuredService, fritzboxDefaultFields); const { widget } = service; const { data: fritzboxData, error: fritzboxError } = useWidgetAPI(widget, "status"); diff --git a/src/widgets/karakeep/component.jsx b/src/widgets/karakeep/component.jsx index 3453e8275..00cc90d04 100644 --- a/src/widgets/karakeep/component.jsx +++ b/src/widgets/karakeep/component.jsx @@ -6,11 +6,10 @@ import useWidgetAPI from "utils/proxy/use-widget-api"; import withWidgetFields from "utils/widget-fields"; export const karakeepDefaultFields = ["bookmarks", "favorites", "archived", "highlights"]; -const MAX_ALLOWED_FIELDS = 4; export default function Component({ service: configuredService }) { const { t } = useTranslation(); - const service = withWidgetFields(configuredService, karakeepDefaultFields, MAX_ALLOWED_FIELDS); + const service = withWidgetFields(configuredService, karakeepDefaultFields); const { widget } = service; const { data: statsData, error: statsError } = useWidgetAPI(widget, "stats"); diff --git a/src/widgets/komodo/component.jsx b/src/widgets/komodo/component.jsx index 509ab6fb1..a1e4a6f07 100644 --- a/src/widgets/komodo/component.jsx +++ b/src/widgets/komodo/component.jsx @@ -5,7 +5,6 @@ import { useTranslation } from "next-i18next/pages"; import useWidgetAPI from "utils/proxy/use-widget-api"; import withWidgetFields from "utils/widget-fields"; -const MAX_ALLOWED_FIELDS = 4; const SUMMARY_FIELDS = ["servers", "stacks", "containers"]; const STACK_FIELDS = ["total", "running", "down", "unhealthy"]; const CONTAINER_FIELDS = ["total", "running", "stopped", "unhealthy"]; @@ -18,7 +17,7 @@ export default function Component({ service: configuredService }) { : configuredWidget.showStacks ? STACK_FIELDS : CONTAINER_FIELDS; - const service = withWidgetFields(configuredService, defaultFields, MAX_ALLOWED_FIELDS); + const service = withWidgetFields(configuredService, defaultFields); const { widget } = service; const containersEndpoint = !(!widget.showSummary && widget.showStacks) ? "containers" : ""; const { data: containersData, error: containersError } = useWidgetAPI(widget, containersEndpoint); diff --git a/src/widgets/maintainerr/component.jsx b/src/widgets/maintainerr/component.jsx index 2e0e175ee..77ee89ec0 100644 --- a/src/widgets/maintainerr/component.jsx +++ b/src/widgets/maintainerr/component.jsx @@ -6,11 +6,10 @@ import useWidgetAPI from "utils/proxy/use-widget-api"; import withWidgetFields from "utils/widget-fields"; const DEFAULT_FIELDS = ["itemsHandled", "episodesHandled", "moviesHandled", "reclaimable"]; -const MAX_FIELDS = 4; export default function Component({ service: configuredService }) { const { t } = useTranslation(); - const service = withWidgetFields(configuredService, DEFAULT_FIELDS, MAX_FIELDS); + const service = withWidgetFields(configuredService, DEFAULT_FIELDS); const { widget } = service; const { data, error } = useWidgetAPI(widget); diff --git a/src/widgets/pangolin/component.jsx b/src/widgets/pangolin/component.jsx index 1473487c6..e7ad38c7e 100644 --- a/src/widgets/pangolin/component.jsx +++ b/src/widgets/pangolin/component.jsx @@ -5,12 +5,11 @@ import { useTranslation } from "next-i18next/pages"; import useWidgetAPI from "utils/proxy/use-widget-api"; import withWidgetFields from "utils/widget-fields"; -const MAX_ALLOWED_FIELDS = 4; const DEFAULT_FIELDS = ["sites", "resources", "targets", "traffic"]; export default function Component({ service: configuredService }) { const { t } = useTranslation(); - const service = withWidgetFields(configuredService, DEFAULT_FIELDS, MAX_ALLOWED_FIELDS); + const service = withWidgetFields(configuredService, DEFAULT_FIELDS); const { widget } = service; const { data: sitesData, error: sitesError } = useWidgetAPI(widget, "sites"); diff --git a/src/widgets/romm/component.jsx b/src/widgets/romm/component.jsx index 97a0afb9d..00fb25d5a 100644 --- a/src/widgets/romm/component.jsx +++ b/src/widgets/romm/component.jsx @@ -6,10 +6,9 @@ import useWidgetAPI from "utils/proxy/use-widget-api"; import withWidgetFields from "utils/widget-fields"; const ROMM_DEFAULT_FIELDS = ["platforms", "totalRoms", "saves", "states"]; -const MAX_ALLOWED_FIELDS = 4; export default function Component({ service: configuredService }) { - const service = withWidgetFields(configuredService, ROMM_DEFAULT_FIELDS, MAX_ALLOWED_FIELDS); + const service = withWidgetFields(configuredService, ROMM_DEFAULT_FIELDS); const { widget } = service; const { t } = useTranslation(); const { data: response, error: responseError } = useWidgetAPI(widget, "statistics"); diff --git a/src/widgets/seerr/component.jsx b/src/widgets/seerr/component.jsx index 9415b256e..c1a7cb83d 100644 --- a/src/widgets/seerr/component.jsx +++ b/src/widgets/seerr/component.jsx @@ -5,10 +5,9 @@ import useWidgetAPI from "utils/proxy/use-widget-api"; import withWidgetFields from "utils/widget-fields"; export const seerrDefaultFields = ["pending", "approved", "completed"]; -const MAX_ALLOWED_FIELDS = 4; export default function Component({ service: configuredService }) { - const service = withWidgetFields(configuredService, seerrDefaultFields, MAX_ALLOWED_FIELDS); + const service = withWidgetFields(configuredService, seerrDefaultFields); const { widget } = service; const isIssueEnabled = widget.fields.includes("issues"); diff --git a/src/widgets/slskd/component.jsx b/src/widgets/slskd/component.jsx index 7c068ac14..7c3a56001 100644 --- a/src/widgets/slskd/component.jsx +++ b/src/widgets/slskd/component.jsx @@ -6,11 +6,10 @@ import useWidgetAPI from "utils/proxy/use-widget-api"; import withWidgetFields from "utils/widget-fields"; const slskdDefaultFields = ["slskStatus", "downloads", "uploads", "sharedFiles"]; -const MAX_ALLOWED_FIELDS = 4; export default function Component({ service: configuredService }) { const { t } = useTranslation(); - const service = withWidgetFields(configuredService, slskdDefaultFields, MAX_ALLOWED_FIELDS); + const service = withWidgetFields(configuredService, slskdDefaultFields); const { widget } = service; const { data: appData, error: appError } = useWidgetAPI(widget, "application"); diff --git a/src/widgets/tailscale/component.jsx b/src/widgets/tailscale/component.jsx index 77b80d348..4c05c5d30 100644 --- a/src/widgets/tailscale/component.jsx +++ b/src/widgets/tailscale/component.jsx @@ -6,12 +6,11 @@ import useWidgetAPI from "utils/proxy/use-widget-api"; import withWidgetFields from "utils/widget-fields"; const DEFAULT_FIELDS = ["address", "last_seen", "expires"]; -const MAX_ALLOWED_FIELDS = 4; export default function Component({ service: configuredService }) { const { t } = useTranslation(); - const service = withWidgetFields(configuredService, DEFAULT_FIELDS, MAX_ALLOWED_FIELDS); + const service = withWidgetFields(configuredService, DEFAULT_FIELDS); const { widget } = service; const { data: tailscaleData, error: tailscaleError } = useWidgetAPI(widget, "device"); diff --git a/src/widgets/technitium/component.jsx b/src/widgets/technitium/component.jsx index 84d28226e..6d676c82a 100644 --- a/src/widgets/technitium/component.jsx +++ b/src/widgets/technitium/component.jsx @@ -5,14 +5,12 @@ import { useTranslation } from "next-i18next/pages"; import useWidgetAPI from "utils/proxy/use-widget-api"; import withWidgetFields from "utils/widget-fields"; -const MAX_ALLOWED_FIELDS = 4; - export const technitiumDefaultFields = ["totalQueries", "totalAuthoritative", "totalCached", "totalServerFailure"]; export default function Component({ service: configuredService }) { const { t } = useTranslation(); - const service = withWidgetFields(configuredService, technitiumDefaultFields, MAX_ALLOWED_FIELDS); + const service = withWidgetFields(configuredService, technitiumDefaultFields); const { widget } = service; const params = { diff --git a/src/widgets/unraid/component.jsx b/src/widgets/unraid/component.jsx index 99ec6244c..2b7ed6ea1 100644 --- a/src/widgets/unraid/component.jsx +++ b/src/widgets/unraid/component.jsx @@ -3,9 +3,9 @@ import Container from "components/services/widget/container"; import { useTranslation } from "next-i18next/pages"; import useWidgetAPI from "utils/proxy/use-widget-api"; +import withWidgetFields from "utils/widget-fields"; const UNRAID_DEFAULT_FIELDS = ["status", "cpu", "memoryPercent", "notifications"]; -const MAX_ALLOWED_FIELDS = 4; const POOLS = ["pool1", "pool2", "pool3", "pool4"]; const POOL_FIELDS = [ @@ -14,8 +14,9 @@ const POOL_FIELDS = [ { param: "UsedPercent", label: "poolUsed", valueKey: "fsUsedPercent", valueType: "common.percent" }, ]; -export default function Component({ service }) { +export default function Component({ service: configuredService }) { const { t } = useTranslation(); + const service = withWidgetFields(configuredService, UNRAID_DEFAULT_FIELDS); const { widget } = service; const { data, error } = useWidgetAPI(widget); @@ -24,12 +25,6 @@ export default function Component({ service }) { return ; } - if (!widget.fields?.length) { - widget.fields = UNRAID_DEFAULT_FIELDS; - } else if (widget.fields.length > MAX_ALLOWED_FIELDS) { - widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS); - } - if (!data) { return ( diff --git a/src/widgets/unraid/component.test.jsx b/src/widgets/unraid/component.test.jsx index e6b82743d..e5d80b279 100644 --- a/src/widgets/unraid/component.test.jsx +++ b/src/widgets/unraid/component.test.jsx @@ -20,14 +20,17 @@ describe("widgets/unraid/component", () => { vi.clearAllMocks(); }); - it("defaults widget.fields and filters down to 4 visible blocks while loading", () => { + it("defaults fields and filters down to 4 visible blocks while loading", () => { useWidgetAPI.mockReturnValue({ data: undefined, error: undefined }); const service = { widget: { type: "unraid" } }; const { container } = renderWithProviders(, { settings: { hideErrors: false } }); - // Component sets default fields - expect(service.widget.fields).toEqual(["status", "cpu", "memoryPercent", "notifications"]); + expect(service.widget.fields).toBeUndefined(); + expect(useWidgetAPI).toHaveBeenCalledWith({ + type: "unraid", + fields: ["status", "cpu", "memoryPercent", "notifications"], + }); // Container filters the many placeholder Blocks down to the selected fields. expect(container.querySelectorAll(".service-block")).toHaveLength(4); diff --git a/src/widgets/wallos/component.jsx b/src/widgets/wallos/component.jsx index 16d5c659b..60606e9a4 100644 --- a/src/widgets/wallos/component.jsx +++ b/src/widgets/wallos/component.jsx @@ -5,7 +5,6 @@ import { useTranslation } from "next-i18next/pages"; import useWidgetAPI from "utils/proxy/use-widget-api"; import withWidgetFields from "utils/widget-fields"; -const MAX_ALLOWED_FIELDS = 4; const DEFAULT_FIELDS = ["activeSubscriptions", "nextRenewingSubscription", "thisMonthlyCost", "nextMonthlyCost"]; const todayDate = new Date(); @@ -20,7 +19,7 @@ function toApiMonthYear(offset = 0) { export default function Component({ service: configuredService }) { const { t } = useTranslation(); - const service = withWidgetFields(configuredService, DEFAULT_FIELDS, MAX_ALLOWED_FIELDS); + const service = withWidgetFields(configuredService, DEFAULT_FIELDS); const { widget } = service; const subscriptionsEndPoint =