feat(widgets): consolidate integration metadata + shared response caching (#6691)
This commit is contained in:
1 parent
7d1d9062ed
commit
8d8dec5749
206 files changed
+3893
-1946
No files matched your search
@@ -0,0 +1,102 @@
|
||||
---
|
||||
title: Widgets and integrations
|
||||
tags:
|
||||
- Developer
|
||||
- Technical documentation
|
||||
- Widgets
|
||||
- Integrations
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
Homarr keeps feature registration explicit and typed. Feature registration has no generator or generated registry.
|
||||
A new feature is available when its typed metadata and lazy loader are registered.
|
||||
|
||||
## Add a widget
|
||||
|
||||
Keep the UI, options, helpers, and tests together in `packages/widgets/src/<widget>/`. The usual folder only needs an
|
||||
`index.ts` definition and a lazily imported `component.tsx`; complex widgets can add local files as needed.
|
||||
|
||||
1. Add the stable kind to `packages/definitions/src/widget.ts` and its documentation slug to the direct typed map in
|
||||
`packages/definitions/src/docs/widget-doc-slugs.ts`.
|
||||
2. Export `definition` and `componentLoader` from `packages/widgets/src/<widget>/index.ts` with
|
||||
`createWidgetDefinition`. The local definition owns its icon, query keys, refresh interval, options, matchers, and
|
||||
errors. Keep the component behind `withDynamicImport` so adding a widget does not load it on every dashboard.
|
||||
3. For an integration-backed widget, add its integration kinds and selection rules once to
|
||||
`packages/definitions/src/widget-integration-map.ts`, then spread `getWidgetIntegrationConfig(kind)` into the local
|
||||
definition. This server-safe config is also the API authorization boundary, avoiding a package cycle without
|
||||
duplicating widget metadata.
|
||||
4. Register the widget in both explicit literal loader maps and the type-only module map in
|
||||
`packages/widgets/src/registry.ts`, then add its icon to `packages/ui/src/widget-icons.ts`. The module and component
|
||||
loaders must both use literal import paths. Keeping them separate lets a cold widget load request its definition
|
||||
and component chunks in parallel; the type-only map preserves exact option and component inference. Keep all three
|
||||
maps in the same order so drift is obvious in review.
|
||||
5. If the widget needs server data, add its tRPC router under `packages/api/src/router/widgets/` and add its explicit
|
||||
lazy import to `packages/api/src/router/widgets/index.ts`. Keep upstream requests and Redis response caching in
|
||||
`@homarr/request-handler`. Use the widget integration middleware for integration-backed procedures.
|
||||
6. Add canonical documentation in `apps/docs/docs/widgets/<slug>/`.
|
||||
|
||||
Weather is the small integration-free example. Downloads shows integration selection, polling, server caching,
|
||||
mutations, partial upstream failures, and a larger options surface.
|
||||
|
||||
## Add an integration
|
||||
|
||||
Keep the HTTP client and its capability-specific types in `packages/integrations/src/<integration>/`.
|
||||
|
||||
1. Add the typed definition to `packages/definitions/src/integration.ts`, including credential alternatives,
|
||||
categories, documentation slug, ports, Docker aliases, and onboarding metadata when relevant.
|
||||
2. Add an explicit lazy creator to `packages/integrations/src/base/creator.ts`. The literal import keeps the
|
||||
implementation out of the initial server bundle and makes the registration easy to find.
|
||||
3. Implement only the interfaces required by the widgets that use the service.
|
||||
4. Add canonical documentation in `apps/docs/docs/integrations/<slug>/`. Its `index.ts` must satisfy
|
||||
`IntegrationDefinition` so invalid metadata fails type checking without widening the object. Use a type-only import
|
||||
and `satisfies IntegrationDefinition`; do not cast the object.
|
||||
|
||||
Beszel is a compact example with several widgets sharing one integration client.
|
||||
|
||||
## Write user documentation
|
||||
|
||||
Write for technically capable self-hosters. Keep each page short and operational.
|
||||
|
||||
- Start with one sentence describing what the integration or widget adds to Homarr.
|
||||
- Include prerequisites, credentials, permissions, destructive actions, and non-obvious limits only when they matter.
|
||||
- Use numbered steps only when order matters. Prefer the exact UI labels a user must select.
|
||||
- Do not add marketing, implementation history, architecture internals, or explanations of basic self-hosting concepts.
|
||||
- End when the user can configure and verify the feature; do not repeat the same information in a summary.
|
||||
|
||||
The usual page needs only an introduction and a `Configuration` section. Add `Notes` or `Troubleshooting` only for
|
||||
real caveats.
|
||||
|
||||
## Data and errors
|
||||
|
||||
- TanStack Query owns browser caching and refresh behavior. Redis owns shared server response caching for bounded key
|
||||
spaces. Public handlers whose keys come from arbitrary URLs, domains, symbols, or coordinates stay in the bounded
|
||||
process-local cache.
|
||||
- Cache raw service responses, never authorization decisions or decrypted credentials. Integration-backed cache keys
|
||||
include keyed credential fingerprints and the complete integration identity, and are invalidated after mutations.
|
||||
The fingerprints change with credentials without exposing their values in Redis keys.
|
||||
- Give upstream requests a finite deadline and pass the request handler's `AbortSignal` into clients that support it.
|
||||
Admission is bounded per handler. Request deduplication and distributed lock ownership remain active until the
|
||||
upstream settles or a bounded cleanup grace expires, while unresolved upstream work stays separately capped.
|
||||
- Keep validation and authorization on the server. Client-side checks improve UX but do not replace tRPC guards.
|
||||
- Use the shared logger with safe identifiers, operation names, duration, and the original error cause. Never log
|
||||
secrets or full upstream payloads.
|
||||
- Multi-integration reads should return successful providers when one service fails and surface a clear error when all
|
||||
providers fail.
|
||||
|
||||
Registration files intentionally contain boring, explicit imports. This small amount of repetition preserves exact
|
||||
TypeScript inference, gives Next.js discoverable lazy chunks, and keeps Ctrl+click navigation reliable.
|
||||
|
||||
## Registry imports
|
||||
|
||||
Use `@homarr/widgets/manifest` to load registered widgets. `loadWidgetDefinition` is the metadata-only path;
|
||||
`loadWidgetComponent` loads only the UI module; and `loadWidgetResources` requests both paths in parallel. These
|
||||
loaders cache in-flight and successful promises and remove rejected promises so a later request can retry.
|
||||
|
||||
The former root `widgetImports` and `loadWidgetDynamic` exports are intentionally not compatibility aliases.
|
||||
`widgetImports` synchronously imported every widget module, and `loadWidgetDynamic` depended on that eager map.
|
||||
Restoring them at the package root would defeat lazy definition chunks or add client-only runtime dependencies to
|
||||
every root consumer. Migrate metadata consumers to `loadWidgetDefinition`, renderers to `loadWidgetResources`, and
|
||||
type-only registry consumers from the removed internal `WidgetImportRecord` helper to the root `WidgetImports` type.
|
||||
|
||||
`@homarr/widgets/catalog` remains as a deprecated compatibility subpath for `widgetCatalogIcons`. New imports should
|
||||
use `@homarr/ui/widget-icons`, which is the canonical server-safe icon catalog.
|
||||
@@ -1,9 +1,9 @@
|
||||
import { IntegrationDefinition } from "@site/src/types";
|
||||
import type { IntegrationDefinition } from "@site/src/types";
|
||||
|
||||
export const beszelIntegration: IntegrationDefinition = {
|
||||
export const beszelIntegration = {
|
||||
name: "Beszel",
|
||||
description:
|
||||
"Beszel is a lightweight server monitoring platform with Docker stats, historical data, and alert functions.",
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/beszel.svg",
|
||||
path: "../../integrations/beszel",
|
||||
};
|
||||
} satisfies IntegrationDefinition;
|
||||
@@ -113,7 +113,10 @@ The OpenAPI and MCP APIs include board management endpoints for common automatio
|
||||
- Set desktop and mobile home boards for the current user
|
||||
- Configure global board defaults such as public home boards and status behavior
|
||||
|
||||
Board layout responses identify each layout with a `mobile`, `base`, or `custom` role. Mobile and Base are protected. Layout-saving clients must preserve existing roles, keep Mobile at breakpoint `0`, and use a unique breakpoint for every layout. Base and custom layouts can appear in any breakpoint order. The layout save operation returns the canonical saved layouts, including server-generated IDs for new custom layouts. A non-Base layout can be regenerated from Base with the protected reset operation.
|
||||
Creating, duplicating, or reconfiguring a widget validates its integration selection on the server. The caller needs
|
||||
use access for each newly linked integration; unchanged links remain editable through normal board permissions.
|
||||
|
||||
Board layout responses identify each layout with a `mobile`, `base`, or `custom` role. Mobile and Base are protected. Layout-saving clients must preserve existing roles, keep Mobile at breakpoint `0`, keep Base as the highest breakpoint, and use unique custom breakpoints between them. The layout save operation returns the canonical saved layouts, including server-generated IDs for new custom layouts. A non-Base layout can be regenerated from Base with the protected reset operation.
|
||||
|
||||
The internal `board.getManageOverview` tRPC query returns compact board previews limited to 12 layout rows by default. Callers may provide `previewRowLimit` from 1 through 48; `fullPreview: true` requests the server's 48-row maximum.
|
||||
|
||||
|
||||
Reference in new issue
Block a user