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
@@ -1,5 +1,7 @@
|
||||
import { IconNotes } from "@tabler/icons-react";
|
||||
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
@@ -7,8 +9,7 @@ export const { definition, componentLoader } = createWidgetDefinition("anchorNot
|
||||
icon: IconNotes,
|
||||
supportsAdvancedFocus: true,
|
||||
queryKey: [["widget", "anchorNotes"]],
|
||||
maxIntegrations: 1,
|
||||
supportedIntegrations: ["anchor", "mock"],
|
||||
...getWidgetIntegrationConfig("anchorNote"),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
noteId: factory.anchorNote(),
|
||||
|
||||
@@ -7,8 +7,11 @@ import { createTrpcQueryKey } from "../trpc-query-key";
|
||||
|
||||
const logger = createLogger({ module: "appWidgetPrefetch" });
|
||||
|
||||
const prefetchAllAsync: Prefetch<"app"> = async (queryClient, items) => {
|
||||
const appIds = items.map((item) => item.options.appId);
|
||||
const prefetchAllAsync: Prefetch = async (queryClient, items) => {
|
||||
const appIds: string[] = [];
|
||||
for (const item of items) {
|
||||
if (typeof item.options.appId === "string") appIds.push(item.options.appId);
|
||||
}
|
||||
const distinctAppIds = [...new Set(appIds)];
|
||||
|
||||
const dbApps = await db.query.apps.findMany({
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { IconArchive } from "@tabler/icons-react";
|
||||
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("archiveTeamWarrior", {
|
||||
icon: IconArchive,
|
||||
supportsAdvancedFocus: true,
|
||||
maxIntegrations: 1,
|
||||
...getWidgetIntegrationConfig("archiveTeamWarrior"),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
showBroadcastMessage: factory.switch({
|
||||
@@ -14,5 +16,4 @@ export const { definition, componentLoader } = createWidgetDefinition("archiveTe
|
||||
}),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: ["archiveTeamWarrior", "mock"],
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IconHeadphones, IconServerOff } from "@tabler/icons-react";
|
||||
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition, matchesWidgetRuntimeQuery } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
@@ -17,9 +19,7 @@ export const { definition, componentLoader } = createWidgetDefinition("audioStat
|
||||
supportsAdvancedFocus: true,
|
||||
queryKeys: [[["widget", "audioStats", "getStats"]], [["widget", "mediaServer", "getCurrentStreams"]]],
|
||||
queryMatcher: matchesWidgetRuntimeQuery,
|
||||
supportedIntegrations: ["navidrome", "audiobookshelf", "mock"],
|
||||
integrationsRequired: true,
|
||||
maxIntegrations: 1,
|
||||
...getWidgetIntegrationConfig("audioStats"),
|
||||
createOptions() {
|
||||
return optionsBuilder.from(
|
||||
(factory) => ({
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { IconBadgeCc, IconServerOff } from "@tabler/icons-react";
|
||||
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("bazarr", {
|
||||
supportsAdvancedFocus: true,
|
||||
icon: IconBadgeCc,
|
||||
supportedIntegrations: ["bazarr", "mock"],
|
||||
integrationsRequired: true,
|
||||
maxIntegrations: 1,
|
||||
...getWidgetIntegrationConfig("bazarr"),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
showMissingEpisodes: factory.switch({
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { IconBell, IconServerOff } from "@tabler/icons-react";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition, matchesWidgetRuntimeQuery, widgetQueryInputMatches } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
@@ -17,8 +19,7 @@ export const { definition, componentLoader } = createWidgetDefinition("beszelAle
|
||||
maxHistoryItems: scope.options.maxHistoryItems,
|
||||
});
|
||||
},
|
||||
supportedIntegrations: ["beszel", "mock"],
|
||||
integrationsRequired: true,
|
||||
...getWidgetIntegrationConfig("beszelAlerts"),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
showHistory: factory.switch({ defaultValue: true }),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IconLayoutGrid, IconServerOff } from "@tabler/icons-react";
|
||||
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { statusOptions } from "../beszel/_shared/options";
|
||||
import { optionsBuilder } from "../options";
|
||||
@@ -7,8 +9,7 @@ import { optionsBuilder } from "../options";
|
||||
export const { definition, componentLoader } = createWidgetDefinition("beszelSystemGrid", {
|
||||
icon: IconLayoutGrid,
|
||||
queryKey: [["widget", "beszel", "getSystems"]],
|
||||
supportedIntegrations: ["beszel", "mock"],
|
||||
integrationsRequired: true,
|
||||
...getWidgetIntegrationConfig("beszelSystemGrid"),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
statusFilter: factory.select({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IconChartAreaLine, IconServerOff } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition, matchesWidgetRuntimeQuery, widgetQueryInputMatches } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
@@ -40,8 +41,7 @@ export const { definition, componentLoader } = createWidgetDefinition("beszelSys
|
||||
(dockerEnabled && widgetQueryInputMatches(query.input, { ...expected, includeDocker: true }))
|
||||
);
|
||||
},
|
||||
supportedIntegrations: ["beszel", "mock"],
|
||||
integrationsRequired: true,
|
||||
...getWidgetIntegrationConfig("beszelSystemStats"),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
systemId: factory.integrationSelect({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconServerOff, IconTable } from "@tabler/icons-react";
|
||||
|
||||
import { invariantTechnicalLabels } from "@homarr/definitions";
|
||||
import { getWidgetIntegrationConfig, invariantTechnicalLabels } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition, matchesWidgetRuntimeQuery, widgetQueryInputMatches } from "../definition";
|
||||
import type { SelectOption } from "../_inputs/widget-select-input";
|
||||
@@ -33,8 +33,7 @@ export const { definition, componentLoader } = createWidgetDefinition("beszelSys
|
||||
query.path.at(-1) === "getSystems"
|
||||
? widgetQueryInputMatches(query.input, { integrationIds: scope.integrationIds })
|
||||
: matchesWidgetRuntimeQuery(query, scope),
|
||||
supportedIntegrations: ["beszel", "mock"],
|
||||
integrationsRequired: true,
|
||||
...getWidgetIntegrationConfig("beszelSystemTable"),
|
||||
createOptions() {
|
||||
return optionsBuilder.from(
|
||||
(factory) => ({
|
||||
|
||||
@@ -8,8 +8,13 @@ import { getDirectBookmarkUrl } from "./bookmark-item";
|
||||
|
||||
const logger = createLogger({ module: "bookmarksWidgetPrefetch" });
|
||||
|
||||
const prefetchAllAsync: Prefetch<"bookmarks"> = async (queryClient, items) => {
|
||||
const appIds = items.flatMap((item) => item.options.items.filter((value) => !getDirectBookmarkUrl(value)));
|
||||
const getAppIds = (options: Record<string, unknown>) => {
|
||||
if (!Array.isArray(options.items)) return [];
|
||||
return options.items.filter((value): value is string => typeof value === "string" && !getDirectBookmarkUrl(value));
|
||||
};
|
||||
|
||||
const prefetchAllAsync: Prefetch = async (queryClient, items) => {
|
||||
const appIds = items.flatMap((item) => getAppIds(item.options));
|
||||
const distinctAppIds = [...new Set(appIds)];
|
||||
|
||||
const dbApps = await db.query.apps.findMany({
|
||||
@@ -17,7 +22,7 @@ const prefetchAllAsync: Prefetch<"bookmarks"> = async (queryClient, items) => {
|
||||
});
|
||||
|
||||
for (const item of items) {
|
||||
const itemAppIds = item.options.items.filter((value) => !getDirectBookmarkUrl(value));
|
||||
const itemAppIds = getAppIds(item.options);
|
||||
if (itemAppIds.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IconCalendar } from "@tabler/icons-react";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
import { radarrReleaseTypes } from "@homarr/integrations/types";
|
||||
|
||||
import { createWidgetDefinition, matchesWidgetRuntimeQuery } from "../definition";
|
||||
@@ -34,6 +34,5 @@ export const { definition, componentLoader } = createWidgetDefinition("calendar"
|
||||
}),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("calendar"),
|
||||
integrationsRequired: false,
|
||||
...getWidgetIntegrationConfig("calendar"),
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
@@ -1,119 +1,2 @@
|
||||
import {
|
||||
IconActivityHeartbeat,
|
||||
IconAd,
|
||||
IconApi,
|
||||
IconApps,
|
||||
IconArchive,
|
||||
IconBadgeCc,
|
||||
IconBatteryCharging,
|
||||
IconBell,
|
||||
IconBinaryTree,
|
||||
IconBookmark,
|
||||
IconBrandDocker,
|
||||
IconBrandMinecraft,
|
||||
IconBrowser,
|
||||
IconBuildingBank,
|
||||
IconBusStop,
|
||||
IconCalendar,
|
||||
IconChartAreaLine,
|
||||
IconChartBar,
|
||||
IconClock,
|
||||
IconClockPlay,
|
||||
IconCloud,
|
||||
IconDeviceCctv,
|
||||
IconDeviceGamepad,
|
||||
IconDownload,
|
||||
IconFileText,
|
||||
IconGraphFilled,
|
||||
IconHourglass,
|
||||
IconHeadphones,
|
||||
IconHeartRateMonitor,
|
||||
IconHeartbeat,
|
||||
IconLayoutGrid,
|
||||
IconMessage,
|
||||
IconMovie,
|
||||
IconNotes,
|
||||
IconPhoto,
|
||||
IconReportSearch,
|
||||
IconRocket,
|
||||
IconRobot,
|
||||
IconRoute,
|
||||
IconRss,
|
||||
IconServer2,
|
||||
IconShieldCheck,
|
||||
IconShieldLock,
|
||||
IconSpeedboat,
|
||||
IconTable,
|
||||
IconTicket,
|
||||
IconTopologyFull,
|
||||
IconTransform,
|
||||
IconVideo,
|
||||
IconWall,
|
||||
IconWind,
|
||||
IconZoomQuestion,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
|
||||
export const widgetCatalogIcons: Record<WidgetKind, TablerIcon> = {
|
||||
clock: IconClock,
|
||||
weather: IconCloud,
|
||||
airQuality: IconWind,
|
||||
countdown: IconHourglass,
|
||||
timer: IconClockPlay,
|
||||
app: IconApps,
|
||||
assistant: IconRobot,
|
||||
iframe: IconBrowser,
|
||||
video: IconDeviceCctv,
|
||||
notebook: IconNotes,
|
||||
anchorNote: IconNotes,
|
||||
dnsHoleSummary: IconAd,
|
||||
dnsHoleControls: IconDeviceGamepad,
|
||||
"smartHome-entityState": IconBinaryTree,
|
||||
"smartHome-executeAutomation": IconBinaryTree,
|
||||
stockPrice: IconBuildingBank,
|
||||
mediaServer: IconVideo,
|
||||
calendar: IconCalendar,
|
||||
downloads: IconDownload,
|
||||
"mediaRequests-requestList": IconZoomQuestion,
|
||||
"mediaRequests-requestStats": IconChartBar,
|
||||
mediaTranscoding: IconTransform,
|
||||
mediaMissing: IconMovie,
|
||||
minecraftServerStatus: IconBrandMinecraft,
|
||||
networkControllerSummary: IconTopologyFull,
|
||||
networkControllerStatus: IconTopologyFull,
|
||||
rssFeed: IconRss,
|
||||
bookmarks: IconBookmark,
|
||||
indexerManager: IconReportSearch,
|
||||
healthMonitoring: IconHeartRateMonitor,
|
||||
releases: IconRocket,
|
||||
mediaReleases: IconTicket,
|
||||
dockerContainers: IconBrandDocker,
|
||||
firewall: IconWall,
|
||||
notifications: IconMessage,
|
||||
systemResources: IconGraphFilled,
|
||||
coolify: IconCloud,
|
||||
systemDisks: IconServer2,
|
||||
timetable: IconBusStop,
|
||||
"immich-serverStats": IconGraphFilled,
|
||||
"immich-albumCarousel": IconPhoto,
|
||||
paperlessNgx: IconFileText,
|
||||
patchmon: IconShieldCheck,
|
||||
bazarr: IconBadgeCc,
|
||||
tracearr: IconActivityHeartbeat,
|
||||
speedtestTracker: IconSpeedboat,
|
||||
uptimeKuma: IconHeartbeat,
|
||||
audioStats: IconHeadphones,
|
||||
umami: IconChartBar,
|
||||
vpn: IconShieldLock,
|
||||
archiveTeamWarrior: IconArchive,
|
||||
ups: IconBatteryCharging,
|
||||
beszelSystemTable: IconTable,
|
||||
beszelSystemGrid: IconLayoutGrid,
|
||||
beszelAlerts: IconBell,
|
||||
beszelSystemStats: IconChartAreaLine,
|
||||
traefik: IconRoute,
|
||||
customApi: IconApi,
|
||||
wud: IconBrandDocker,
|
||||
};
|
||||
/** @deprecated Import widgetCatalogIcons from "@homarr/ui/widget-icons" instead. */
|
||||
export { widgetCatalogIcons } from "@homarr/ui/widget-icons";
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IconCloud, IconServerOff } from "@tabler/icons-react";
|
||||
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
@@ -19,7 +21,7 @@ export const { definition, componentLoader } = createWidgetDefinition("coolify",
|
||||
}),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: ["coolify", "mock"],
|
||||
...getWidgetIntegrationConfig("coolify"),
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
icon: IconServerOff,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type React from "react";
|
||||
import type { LoaderComponent } from "next/dynamic";
|
||||
import type { QueryClient, QueryKey } from "@tanstack/react-query";
|
||||
import { hashKey } from "@tanstack/react-query";
|
||||
import type { DefaultErrorData } from "@trpc/server/unstable-core-do-not-import";
|
||||
@@ -10,7 +9,7 @@ import type { SettingsContextProps } from "@homarr/settings/creator";
|
||||
import type { stringOrTranslation } from "@homarr/translation";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
|
||||
import type { WidgetImports } from ".";
|
||||
import type { WidgetImports } from "./registry";
|
||||
import type { inferOptionsFromCreator, WidgetOptionsRecord } from "./options";
|
||||
|
||||
export interface WidgetContextMenuAction {
|
||||
@@ -75,7 +74,7 @@ export interface WidgetContextActionProps {
|
||||
|
||||
const createWithDynamicImport =
|
||||
<TKind extends WidgetKind, TDefinition extends WidgetDefinition>(kind: TKind, definition: TDefinition) =>
|
||||
(componentLoader: () => LoaderComponent<WidgetComponentProps<TKind>>) => ({
|
||||
(componentLoader: () => Promise<{ default: React.ComponentType<WidgetComponentProps<TKind>> }>) => ({
|
||||
definition: {
|
||||
...definition,
|
||||
kind,
|
||||
@@ -84,11 +83,11 @@ const createWithDynamicImport =
|
||||
componentLoader,
|
||||
});
|
||||
|
||||
export type PrefetchLoader<TKind extends WidgetKind> = () => Promise<{ default: Prefetch<TKind> }>;
|
||||
export type Prefetch<TKind extends WidgetKind> = (
|
||||
export type PrefetchLoader = () => Promise<{ default: Prefetch }>;
|
||||
export type Prefetch = (
|
||||
queryClient: QueryClient,
|
||||
items: {
|
||||
options: inferOptionsFromCreator<WidgetOptionsRecordOf<TKind>>;
|
||||
options: Record<string, unknown>;
|
||||
integrationIds: string[];
|
||||
}[],
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconDeviceGamepad, IconServerOff } from "@tabler/icons-react";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../../definition";
|
||||
import { optionsBuilder } from "../../options";
|
||||
@@ -19,7 +19,7 @@ export const { definition, componentLoader } = createWidgetDefinition(widgetKind
|
||||
}),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("dnsHole"),
|
||||
...getWidgetIntegrationConfig("dnsHoleControls"),
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
icon: IconServerOff,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconAd, IconServerOff } from "@tabler/icons-react";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../../definition";
|
||||
import { optionsBuilder } from "../../options";
|
||||
@@ -30,7 +30,7 @@ export const { definition, componentLoader } = createWidgetDefinition(widgetKind
|
||||
}),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("dnsHole"),
|
||||
...getWidgetIntegrationConfig("dnsHoleSummary"),
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
icon: IconServerOff,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IconDownload } from "@tabler/icons-react";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { getIntegrationKindsByCategory, getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition, widgetQueryInputMatches } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
@@ -111,5 +111,5 @@ export const { definition, componentLoader } = createWidgetDefinition("downloads
|
||||
},
|
||||
);
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("downloadClient"),
|
||||
...getWidgetIntegrationConfig("downloads"),
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconWall, IconWallOff } from "@tabler/icons-react";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
@@ -12,7 +12,7 @@ export const { definition, componentLoader } = createWidgetDefinition("firewall"
|
||||
createOptions() {
|
||||
return optionsBuilder.from(() => ({}));
|
||||
},
|
||||
supportedIntegrations: [...getIntegrationKindsByCategory("firewall"), "mock"],
|
||||
...getWidgetIntegrationConfig("firewall"),
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
icon: IconWallOff,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconHeartRateMonitor, IconServerOff } from "@tabler/icons-react";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
@@ -114,9 +114,7 @@ export const { definition, componentLoader } = createWidgetDefinition("healthMon
|
||||
},
|
||||
);
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("healthMonitoring").filter(
|
||||
(kind) => kind !== "patchmon" && kind !== "wud",
|
||||
),
|
||||
...getWidgetIntegrationConfig("healthMonitoring"),
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
icon: IconServerOff,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { healthMonitoringClusterIntegrationKinds, healthMonitoringSystemIntegrationKinds } from "@homarr/definitions";
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
|
||||
interface HealthMonitoringIntegration {
|
||||
@@ -5,8 +6,11 @@ interface HealthMonitoringIntegration {
|
||||
kind: IntegrationKind;
|
||||
}
|
||||
|
||||
const systemIntegrationKinds = new Set<IntegrationKind>(healthMonitoringSystemIntegrationKinds);
|
||||
const clusterIntegrationKinds = new Set<IntegrationKind>(healthMonitoringClusterIntegrationKinds);
|
||||
|
||||
export const partitionHealthMonitoringIntegrations = (integrations: HealthMonitoringIntegration[]) => ({
|
||||
clusterIntegrationIds: integrations.filter(({ kind }) => kind === "proxmox" || kind === "mock").map(({ id }) => id),
|
||||
clusterIntegrationIds: integrations.filter(({ kind }) => clusterIntegrationKinds.has(kind)).map(({ id }) => id),
|
||||
// Mock implements both contracts and remains visible in both views.
|
||||
systemIntegrationIds: integrations.filter(({ kind }) => kind !== "proxmox").map(({ id }) => id),
|
||||
systemIntegrationIds: integrations.filter(({ kind }) => systemIntegrationKinds.has(kind)).map(({ id }) => id),
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { IconChevronLeft, IconChevronRight, IconPhoto, IconPlayerPause } from "@
|
||||
import z from "zod";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { createWidgetDefinition, widgetQueryInputMatches } from "../../definition";
|
||||
@@ -62,9 +63,7 @@ export const { definition, componentLoader } = createWidgetDefinition("immich-al
|
||||
: undefined,
|
||||
}),
|
||||
refetchInterval: null,
|
||||
supportedIntegrations: ["immich", "mock"],
|
||||
integrationsRequired: true,
|
||||
maxIntegrations: 1,
|
||||
...getWidgetIntegrationConfig("immich-albumCarousel"),
|
||||
contextActions: ({ widgetRuntimeRef }) => {
|
||||
const actions = widgetRuntimeRef.current.actions;
|
||||
return [
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IconGraphFilled } from "@tabler/icons-react";
|
||||
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition, matchesWidgetRuntimeQuery } from "../../definition";
|
||||
import { optionsBuilder } from "../../options";
|
||||
|
||||
@@ -9,9 +11,7 @@ export const { definition, componentLoader } = createWidgetDefinition("immich-se
|
||||
queryKeys: [[["widget", "immich", "getServerStats"]], [["widget", "immich", "getAlbums"]]],
|
||||
queryMatcher: matchesWidgetRuntimeQuery,
|
||||
refetchInterval: null,
|
||||
supportedIntegrations: ["immich", "mock"],
|
||||
integrationsRequired: true,
|
||||
maxIntegrations: 1,
|
||||
...getWidgetIntegrationConfig("immich-serverStats"),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
showUsers: factory.switch({
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
|
||||
export type WidgetImportRecord = Record<WidgetKind, unknown>;
|
||||
@@ -1,72 +1,6 @@
|
||||
import type { ComponentType } from "react";
|
||||
import type { Loader } from "next/dynamic";
|
||||
import dynamic from "next/dynamic";
|
||||
import { Center, Loader as UiLoader } from "@mantine/core";
|
||||
|
||||
import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
|
||||
|
||||
import * as anchorNote from "./anchor-note";
|
||||
import * as audioStats from "./audio-stats";
|
||||
import * as app from "./app";
|
||||
import * as archiveTeamWarrior from "./archive-team-warrior";
|
||||
import * as bookmarks from "./bookmarks";
|
||||
import * as bazarr from "./bazarr";
|
||||
import * as calendar from "./calendar";
|
||||
import * as clock from "./clock";
|
||||
import * as coolify from "./coolify";
|
||||
import type { WidgetComponentProps } from "./definition";
|
||||
import * as dnsHoleControls from "./dns-hole/controls";
|
||||
import * as dnsHoleSummary from "./dns-hole/summary";
|
||||
import * as dockerContainers from "./docker";
|
||||
import * as downloads from "./downloads";
|
||||
import * as firewall from "./firewall";
|
||||
import * as healthMonitoring from "./health-monitoring";
|
||||
import * as iframe from "./iframe";
|
||||
import * as immichAlbumCarousel from "./immich/album-carousel";
|
||||
import * as immichServerStats from "./immich/server-stats";
|
||||
import type { WidgetImportRecord } from "./import";
|
||||
import * as indexerManager from "./indexer-manager";
|
||||
import * as mediaReleases from "./media-releases";
|
||||
import * as mediaRequestsList from "./media-requests/list";
|
||||
import * as mediaRequestsStats from "./media-requests/stats";
|
||||
import * as mediaServer from "./media-server";
|
||||
import * as mediaTranscoding from "./media-transcoding";
|
||||
import * as mediaMissing from "./media-missing";
|
||||
import * as minecraftServerStatus from "./minecraft/server-status";
|
||||
import * as networkControllerStatus from "./network-controller/network-status";
|
||||
import * as networkControllerSummary from "./network-controller/summary";
|
||||
import * as notebook from "./notebook";
|
||||
import * as paperlessNgx from "./paperless-ngx";
|
||||
import * as patchmon from "./patchmon";
|
||||
import * as notifications from "./notifications";
|
||||
import * as releases from "./releases";
|
||||
import * as rssFeed from "./rssFeed";
|
||||
import * as smartHomeEntityState from "./smart-home/entity-state";
|
||||
import * as smartHomeExecuteAutomation from "./smart-home/execute-automation";
|
||||
import * as speedtestTracker from "./speedtest-tracker";
|
||||
import * as uptimeKuma from "./uptime-kuma";
|
||||
import * as stockPrice from "./stocks";
|
||||
import * as systemDisks from "./system-disks";
|
||||
import * as systemResources from "./system-resources";
|
||||
import * as timetable from "./timetable";
|
||||
import * as traefik from "./traefik";
|
||||
import * as tracearr from "./tracearr";
|
||||
import * as umami from "./umami";
|
||||
import * as ups from "./ups";
|
||||
import * as vpn from "./vpn";
|
||||
import * as beszelSystemTable from "./beszel-system-table";
|
||||
import * as beszelSystemGrid from "./beszel-system-grid";
|
||||
import * as beszelAlerts from "./beszel-alerts";
|
||||
import * as beszelSystemStats from "./beszel-system-stats";
|
||||
import * as video from "./video";
|
||||
import * as weather from "./weather";
|
||||
import * as airQuality from "./air-quality";
|
||||
import * as countdown from "./countdown";
|
||||
import * as timer from "./timer";
|
||||
import * as customApi from "./custom-api";
|
||||
import * as assistant from "./assistant";
|
||||
import * as wud from "./wud";
|
||||
|
||||
export { AssistantWidgetRendererProvider } from "./assistant/context";
|
||||
export type {
|
||||
NormalizedWidgetQuery,
|
||||
WidgetDefinition,
|
||||
@@ -89,102 +23,9 @@ export {
|
||||
} from "./definition";
|
||||
export type { WidgetComponentProps };
|
||||
export type { WidgetOptionDefinition, WidgetOptionType } from "./options";
|
||||
export { AssistantWidgetRendererProvider } from "./assistant/context";
|
||||
|
||||
export const widgetImports = {
|
||||
clock,
|
||||
weather,
|
||||
airQuality,
|
||||
countdown,
|
||||
timer,
|
||||
app,
|
||||
archiveTeamWarrior,
|
||||
anchorNote,
|
||||
notebook,
|
||||
iframe,
|
||||
video,
|
||||
dnsHoleSummary,
|
||||
dnsHoleControls,
|
||||
"smartHome-entityState": smartHomeEntityState,
|
||||
"smartHome-executeAutomation": smartHomeExecuteAutomation,
|
||||
stockPrice,
|
||||
mediaServer,
|
||||
calendar,
|
||||
downloads,
|
||||
"mediaRequests-requestList": mediaRequestsList,
|
||||
"mediaRequests-requestStats": mediaRequestsStats,
|
||||
mediaMissing,
|
||||
networkControllerSummary,
|
||||
networkControllerStatus,
|
||||
rssFeed,
|
||||
bookmarks,
|
||||
bazarr,
|
||||
indexerManager,
|
||||
healthMonitoring,
|
||||
mediaTranscoding,
|
||||
minecraftServerStatus,
|
||||
dockerContainers,
|
||||
releases,
|
||||
firewall,
|
||||
notifications,
|
||||
mediaReleases,
|
||||
systemResources,
|
||||
coolify,
|
||||
systemDisks,
|
||||
timetable,
|
||||
"immich-serverStats": immichServerStats,
|
||||
"immich-albumCarousel": immichAlbumCarousel,
|
||||
paperlessNgx,
|
||||
patchmon,
|
||||
tracearr,
|
||||
speedtestTracker,
|
||||
uptimeKuma,
|
||||
audioStats,
|
||||
umami,
|
||||
vpn,
|
||||
ups,
|
||||
beszelSystemTable,
|
||||
beszelSystemGrid,
|
||||
beszelAlerts,
|
||||
beszelSystemStats,
|
||||
traefik,
|
||||
customApi,
|
||||
assistant,
|
||||
wud,
|
||||
} satisfies WidgetImportRecord;
|
||||
|
||||
export type WidgetImports = typeof widgetImports;
|
||||
export type WidgetImportKey = keyof WidgetImports;
|
||||
|
||||
const loadedComponents = new Map<WidgetKind, ComponentType<WidgetComponentProps<WidgetKind>>>();
|
||||
|
||||
export const loadWidgetDynamic = <TKind extends WidgetKind>(kind: TKind) => {
|
||||
const existingComponent = loadedComponents.get(kind);
|
||||
if (existingComponent) return existingComponent;
|
||||
|
||||
const newlyLoadedComponent = dynamic<WidgetComponentProps<TKind>>(
|
||||
widgetImports[kind].componentLoader as Loader<WidgetComponentProps<TKind>>,
|
||||
{
|
||||
loading: () => (
|
||||
<Center w="100%" h="100%">
|
||||
<UiLoader />
|
||||
</Center>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
loadedComponents.set(kind, newlyLoadedComponent as never);
|
||||
return newlyLoadedComponent;
|
||||
};
|
||||
|
||||
export type inferSupportedIntegrations<TKind extends WidgetKind> = (WidgetImports[TKind]["definition"] extends {
|
||||
supportedIntegrations: string[];
|
||||
}
|
||||
? WidgetImports[TKind]["definition"]["supportedIntegrations"]
|
||||
: string[])[number];
|
||||
|
||||
export type inferSupportedIntegrationsStrict<TKind extends WidgetKind> = (WidgetImports[TKind]["definition"] extends {
|
||||
supportedIntegrations: IntegrationKind[];
|
||||
}
|
||||
? WidgetImports[TKind]["definition"]["supportedIntegrations"]
|
||||
: never[])[number];
|
||||
export type {
|
||||
inferSupportedIntegrations,
|
||||
inferSupportedIntegrationsStrict,
|
||||
WidgetImportKey,
|
||||
WidgetImports,
|
||||
} from "./registry";
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconReportSearch, IconServerOff, IconTestPipe } from "@tabler/icons-react";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
@@ -30,7 +30,7 @@ export const { definition, componentLoader } = createWidgetDefinition("indexerMa
|
||||
],
|
||||
refetchInterval: null,
|
||||
createOptions,
|
||||
supportedIntegrations: getIntegrationKindsByCategory("indexerManager"),
|
||||
...getWidgetIntegrationConfig("indexerManager"),
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
icon: IconServerOff,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { QueryKey } from "@tanstack/react-query";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { widgetIntegrationSupport, widgetKinds as definedWidgetKinds } from "@homarr/definitions";
|
||||
import { widgetIntegrationSupport, widgetKinds as declaredWidgetKinds } from "@homarr/definitions";
|
||||
|
||||
import {
|
||||
createRetryableLoader,
|
||||
@@ -14,10 +14,16 @@ import {
|
||||
widgetKinds,
|
||||
} from "./manifest";
|
||||
import { widgetQueryRefetchIntervals } from "./refetch-intervals";
|
||||
import { widgetCatalogIcons } from "./catalog";
|
||||
import { widgetCatalogIcons } from "@homarr/ui/widget-icons";
|
||||
import { getWidgetQueryKeys } from "./definition";
|
||||
|
||||
const serializePollingPolicy = (entry: { queryKey: QueryKey; intervalSeconds: number | null }) => JSON.stringify(entry);
|
||||
const serializePollingPolicy = ({
|
||||
queryKey,
|
||||
intervalSeconds,
|
||||
}: {
|
||||
queryKey: QueryKey;
|
||||
intervalSeconds: number | null;
|
||||
}) => JSON.stringify({ queryKey, intervalSeconds });
|
||||
|
||||
describe("widget manifest promise stability", () => {
|
||||
it("returns the same module promise for every render", () => {
|
||||
@@ -58,7 +64,7 @@ describe("widget manifest promise stability", () => {
|
||||
});
|
||||
|
||||
it("covers every declared widget kind", () => {
|
||||
expect(new Set(widgetKinds)).toEqual(new Set(definedWidgetKinds));
|
||||
expect(new Set(widgetKinds)).toEqual(new Set(declaredWidgetKinds));
|
||||
});
|
||||
|
||||
it("loads matching definitions and component loaders for every widget", async () => {
|
||||
@@ -70,6 +76,7 @@ describe("widget manifest promise stability", () => {
|
||||
const canonicalComponent = await module.componentLoader();
|
||||
const component = await loadWidgetComponent(kind);
|
||||
const resources = await loadWidgetResources(kind);
|
||||
expect(module.definition.kind).toBe(kind);
|
||||
expect(definitions.get(kind)).toBe(module.definition);
|
||||
expect(component.default).toBe(canonicalComponent.default);
|
||||
expect(component.default).toBeDefined();
|
||||
@@ -98,7 +105,8 @@ describe("widget manifest promise stability", () => {
|
||||
const definition = definitions.get(kind);
|
||||
const supportedIntegrations =
|
||||
definition && "supportedIntegrations" in definition ? (definition.supportedIntegrations ?? []) : [];
|
||||
expect([...(widgetIntegrationSupport[kind] ?? [])].toSorted()).toEqual([...supportedIntegrations].toSorted());
|
||||
const expectedIntegrations = widgetIntegrationSupport[kind] ?? [];
|
||||
expect(expectedIntegrations.toSorted()).toEqual([...supportedIntegrations].toSorted());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -2,156 +2,54 @@ import type { ComponentType } from "react";
|
||||
|
||||
import { objectEntries } from "@homarr/common";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { widgetKinds as declaredWidgetKinds } from "@homarr/definitions";
|
||||
import type { SettingsContextProps } from "@homarr/settings/creator";
|
||||
|
||||
import type { WidgetComponentProps, WidgetDefinition } from "./definition";
|
||||
import type { WidgetOptionDefinition } from "./options";
|
||||
import type { WidgetImports } from "./registry";
|
||||
import { widgetComponentLoaders, widgetModuleLoaders } from "./registry";
|
||||
|
||||
type WidgetModule = {
|
||||
type RegisteredWidgetModule = WidgetImports[WidgetKind];
|
||||
type RegisteredWidgetComponentModule = Awaited<ReturnType<RegisteredWidgetModule["componentLoader"]>>;
|
||||
type WidgetDefinitionFor<TKind extends WidgetKind> = WidgetImports[TKind]["definition"] & WidgetDefinition;
|
||||
|
||||
type WidgetComponentModule<TKind extends WidgetKind> = {
|
||||
default: ComponentType<WidgetComponentProps<TKind>>;
|
||||
};
|
||||
|
||||
export type WidgetModule<TKind extends WidgetKind> = {
|
||||
definition: WidgetDefinitionFor<TKind>;
|
||||
componentLoader: () => Promise<WidgetComponentModule<TKind>>;
|
||||
};
|
||||
|
||||
export type WidgetResources<TKind extends WidgetKind = WidgetKind> = {
|
||||
definition: WidgetDefinitionFor<TKind>;
|
||||
Component: ComponentType<WidgetComponentProps<TKind>>;
|
||||
};
|
||||
|
||||
type RegisteredWidgetResources = {
|
||||
definition: RegisteredWidgetModule["definition"];
|
||||
Component: RegisteredWidgetComponentModule["default"];
|
||||
};
|
||||
|
||||
type WidgetManifestValue<TKind extends WidgetKind> = {
|
||||
module: WidgetModule<TKind>;
|
||||
component: WidgetComponentModule<TKind>;
|
||||
resources: WidgetResources<TKind>;
|
||||
definition: WidgetDefinitionFor<TKind>;
|
||||
};
|
||||
|
||||
type RegisteredWidgetManifestValue = {
|
||||
module: RegisteredWidgetModule;
|
||||
component: RegisteredWidgetComponentModule;
|
||||
resources: RegisteredWidgetResources;
|
||||
definition: WidgetDefinition;
|
||||
componentLoader: () => Promise<{ default: ComponentType<WidgetComponentProps<WidgetKind>> }>;
|
||||
};
|
||||
|
||||
type WidgetComponentModule = {
|
||||
default: ComponentType<WidgetComponentProps<WidgetKind>>;
|
||||
};
|
||||
|
||||
export type WidgetResources = {
|
||||
definition: WidgetDefinition;
|
||||
Component: ComponentType<WidgetComponentProps<WidgetKind>>;
|
||||
};
|
||||
|
||||
const moduleLoaders: Record<WidgetKind, () => Promise<WidgetModule>> = {
|
||||
clock: () => import("./clock") as Promise<WidgetModule>,
|
||||
weather: () => import("./weather") as Promise<WidgetModule>,
|
||||
airQuality: () => import("./air-quality") as Promise<WidgetModule>,
|
||||
countdown: () => import("./countdown") as Promise<WidgetModule>,
|
||||
timer: () => import("./timer") as Promise<WidgetModule>,
|
||||
app: () => import("./app") as Promise<WidgetModule>,
|
||||
assistant: () => import("./assistant") as Promise<WidgetModule>,
|
||||
archiveTeamWarrior: () => import("./archive-team-warrior") as Promise<WidgetModule>,
|
||||
anchorNote: () => import("./anchor-note") as Promise<WidgetModule>,
|
||||
notebook: () => import("./notebook") as Promise<WidgetModule>,
|
||||
iframe: () => import("./iframe") as Promise<WidgetModule>,
|
||||
video: () => import("./video") as Promise<WidgetModule>,
|
||||
dnsHoleSummary: () => import("./dns-hole/summary") as Promise<WidgetModule>,
|
||||
dnsHoleControls: () => import("./dns-hole/controls") as Promise<WidgetModule>,
|
||||
"smartHome-entityState": () => import("./smart-home/entity-state") as Promise<WidgetModule>,
|
||||
"smartHome-executeAutomation": () => import("./smart-home/execute-automation") as Promise<WidgetModule>,
|
||||
stockPrice: () => import("./stocks") as Promise<WidgetModule>,
|
||||
mediaServer: () => import("./media-server") as Promise<WidgetModule>,
|
||||
calendar: () => import("./calendar") as Promise<WidgetModule>,
|
||||
downloads: () => import("./downloads") as Promise<WidgetModule>,
|
||||
"mediaRequests-requestList": () => import("./media-requests/list") as Promise<WidgetModule>,
|
||||
"mediaRequests-requestStats": () => import("./media-requests/stats") as Promise<WidgetModule>,
|
||||
mediaMissing: () => import("./media-missing") as Promise<WidgetModule>,
|
||||
networkControllerSummary: () => import("./network-controller/summary") as Promise<WidgetModule>,
|
||||
networkControllerStatus: () => import("./network-controller/network-status") as Promise<WidgetModule>,
|
||||
rssFeed: () => import("./rssFeed") as Promise<WidgetModule>,
|
||||
bookmarks: () => import("./bookmarks") as Promise<WidgetModule>,
|
||||
bazarr: () => import("./bazarr") as Promise<WidgetModule>,
|
||||
indexerManager: () => import("./indexer-manager") as Promise<WidgetModule>,
|
||||
healthMonitoring: () => import("./health-monitoring") as Promise<WidgetModule>,
|
||||
mediaTranscoding: () => import("./media-transcoding") as Promise<WidgetModule>,
|
||||
minecraftServerStatus: () => import("./minecraft/server-status") as Promise<WidgetModule>,
|
||||
dockerContainers: () => import("./docker") as Promise<WidgetModule>,
|
||||
releases: () => import("./releases") as Promise<WidgetModule>,
|
||||
firewall: () => import("./firewall") as Promise<WidgetModule>,
|
||||
notifications: () => import("./notifications") as Promise<WidgetModule>,
|
||||
mediaReleases: () => import("./media-releases") as Promise<WidgetModule>,
|
||||
systemResources: () => import("./system-resources") as Promise<WidgetModule>,
|
||||
coolify: () => import("./coolify") as Promise<WidgetModule>,
|
||||
systemDisks: () => import("./system-disks") as Promise<WidgetModule>,
|
||||
timetable: () => import("./timetable") as Promise<WidgetModule>,
|
||||
"immich-serverStats": () => import("./immich/server-stats") as Promise<WidgetModule>,
|
||||
"immich-albumCarousel": () => import("./immich/album-carousel") as Promise<WidgetModule>,
|
||||
paperlessNgx: () => import("./paperless-ngx") as Promise<WidgetModule>,
|
||||
patchmon: () => import("./patchmon") as Promise<WidgetModule>,
|
||||
tracearr: () => import("./tracearr") as Promise<WidgetModule>,
|
||||
speedtestTracker: () => import("./speedtest-tracker") as Promise<WidgetModule>,
|
||||
uptimeKuma: () => import("./uptime-kuma") as Promise<WidgetModule>,
|
||||
audioStats: () => import("./audio-stats") as Promise<WidgetModule>,
|
||||
umami: () => import("./umami") as Promise<WidgetModule>,
|
||||
vpn: () => import("./vpn") as Promise<WidgetModule>,
|
||||
ups: () => import("./ups") as Promise<WidgetModule>,
|
||||
beszelSystemTable: () => import("./beszel-system-table") as Promise<WidgetModule>,
|
||||
beszelSystemGrid: () => import("./beszel-system-grid") as Promise<WidgetModule>,
|
||||
beszelAlerts: () => import("./beszel-alerts") as Promise<WidgetModule>,
|
||||
beszelSystemStats: () => import("./beszel-system-stats") as Promise<WidgetModule>,
|
||||
traefik: () => import("./traefik") as Promise<WidgetModule>,
|
||||
customApi: () => import("./custom-api") as Promise<WidgetModule>,
|
||||
wud: () => import("./wud") as Promise<WidgetModule>,
|
||||
};
|
||||
|
||||
// Keep these imports explicit so Turbopack can create one discoverable chunk per
|
||||
// widget implementation. Loading this registry does not load the implementations.
|
||||
const componentLoaders: Record<WidgetKind, () => Promise<WidgetComponentModule>> = {
|
||||
clock: () => import("./clock/component") as Promise<WidgetComponentModule>,
|
||||
weather: () => import("./weather/component") as Promise<WidgetComponentModule>,
|
||||
airQuality: () => import("./air-quality/component") as Promise<WidgetComponentModule>,
|
||||
countdown: () => import("./countdown/component") as Promise<WidgetComponentModule>,
|
||||
timer: () => import("./timer/component") as Promise<WidgetComponentModule>,
|
||||
app: () => import("./app/component") as Promise<WidgetComponentModule>,
|
||||
assistant: () => import("./assistant/component") as Promise<WidgetComponentModule>,
|
||||
archiveTeamWarrior: () => import("./archive-team-warrior/component") as Promise<WidgetComponentModule>,
|
||||
anchorNote: () => import("./anchor-note/component") as Promise<WidgetComponentModule>,
|
||||
notebook: () => import("./notebook/component") as Promise<WidgetComponentModule>,
|
||||
iframe: () => import("./iframe/component") as Promise<WidgetComponentModule>,
|
||||
video: () => import("./video/component") as Promise<WidgetComponentModule>,
|
||||
dnsHoleSummary: () => import("./dns-hole/summary/component") as Promise<WidgetComponentModule>,
|
||||
dnsHoleControls: () => import("./dns-hole/controls/component") as Promise<WidgetComponentModule>,
|
||||
"smartHome-entityState": () => import("./smart-home/entity-state/component") as Promise<WidgetComponentModule>,
|
||||
"smartHome-executeAutomation": () =>
|
||||
import("./smart-home/execute-automation/component") as Promise<WidgetComponentModule>,
|
||||
stockPrice: () => import("./stocks/component") as Promise<WidgetComponentModule>,
|
||||
mediaServer: () => import("./media-server/component") as Promise<WidgetComponentModule>,
|
||||
calendar: () => import("./calendar/component") as Promise<WidgetComponentModule>,
|
||||
downloads: () => import("./downloads/component") as Promise<WidgetComponentModule>,
|
||||
"mediaRequests-requestList": () => import("./media-requests/list/component") as Promise<WidgetComponentModule>,
|
||||
"mediaRequests-requestStats": () => import("./media-requests/stats/component") as Promise<WidgetComponentModule>,
|
||||
mediaMissing: () => import("./media-missing/component") as Promise<WidgetComponentModule>,
|
||||
networkControllerSummary: () => import("./network-controller/summary/component") as Promise<WidgetComponentModule>,
|
||||
networkControllerStatus: () =>
|
||||
import("./network-controller/network-status/component") as Promise<WidgetComponentModule>,
|
||||
rssFeed: () => import("./rssFeed/component") as Promise<WidgetComponentModule>,
|
||||
bookmarks: () => import("./bookmarks/component") as Promise<WidgetComponentModule>,
|
||||
bazarr: () => import("./bazarr/component") as Promise<WidgetComponentModule>,
|
||||
indexerManager: () => import("./indexer-manager/component") as Promise<WidgetComponentModule>,
|
||||
healthMonitoring: () => import("./health-monitoring/component") as Promise<WidgetComponentModule>,
|
||||
mediaTranscoding: () => import("./media-transcoding/component") as Promise<WidgetComponentModule>,
|
||||
minecraftServerStatus: () => import("./minecraft/server-status/component") as Promise<WidgetComponentModule>,
|
||||
dockerContainers: () => import("./docker/component") as Promise<WidgetComponentModule>,
|
||||
releases: () => import("./releases/component") as Promise<WidgetComponentModule>,
|
||||
firewall: () => import("./firewall/component") as Promise<WidgetComponentModule>,
|
||||
notifications: () => import("./notifications/component") as Promise<WidgetComponentModule>,
|
||||
mediaReleases: () => import("./media-releases/component") as Promise<WidgetComponentModule>,
|
||||
systemResources: () => import("./system-resources/component") as Promise<WidgetComponentModule>,
|
||||
coolify: () => import("./coolify/component") as Promise<WidgetComponentModule>,
|
||||
systemDisks: () => import("./system-disks/component") as Promise<WidgetComponentModule>,
|
||||
timetable: () => import("./timetable/component") as Promise<WidgetComponentModule>,
|
||||
"immich-serverStats": () => import("./immich/server-stats/component") as Promise<WidgetComponentModule>,
|
||||
"immich-albumCarousel": () => import("./immich/album-carousel/component") as Promise<WidgetComponentModule>,
|
||||
paperlessNgx: () => import("./paperless-ngx/component") as Promise<WidgetComponentModule>,
|
||||
patchmon: () => import("./patchmon/component") as Promise<WidgetComponentModule>,
|
||||
tracearr: () => import("./tracearr/component") as Promise<WidgetComponentModule>,
|
||||
speedtestTracker: () => import("./speedtest-tracker/component") as Promise<WidgetComponentModule>,
|
||||
uptimeKuma: () => import("./uptime-kuma/component") as Promise<WidgetComponentModule>,
|
||||
audioStats: () => import("./audio-stats/component") as Promise<WidgetComponentModule>,
|
||||
umami: () => import("./umami/component") as Promise<WidgetComponentModule>,
|
||||
vpn: () => import("./vpn/component") as Promise<WidgetComponentModule>,
|
||||
ups: () => import("./ups/component") as Promise<WidgetComponentModule>,
|
||||
beszelSystemTable: () => import("./beszel-system-table/component") as Promise<WidgetComponentModule>,
|
||||
beszelSystemGrid: () => import("./beszel-system-grid/component") as Promise<WidgetComponentModule>,
|
||||
beszelAlerts: () => import("./beszel-alerts/component") as Promise<WidgetComponentModule>,
|
||||
beszelSystemStats: () => import("./beszel-system-stats/component") as Promise<WidgetComponentModule>,
|
||||
traefik: () => import("./traefik/component") as Promise<WidgetComponentModule>,
|
||||
customApi: () => import("./custom-api/component") as Promise<WidgetComponentModule>,
|
||||
wud: () => import("./wud/component") as Promise<WidgetComponentModule>,
|
||||
};
|
||||
|
||||
const definitionPromises = new Map<WidgetKind, Promise<WidgetDefinition>>();
|
||||
|
||||
export const widgetKinds = Object.keys(moduleLoaders) as WidgetKind[];
|
||||
export const widgetKinds = [...declaredWidgetKinds];
|
||||
|
||||
export const createRetryableLoader = <TKey, TValue>(loaders: ReadonlyMap<TKey, () => Promise<TValue>>) => {
|
||||
const promises = new Map<TKey, Promise<TValue>>();
|
||||
@@ -172,39 +70,59 @@ export const createRetryableLoader = <TKey, TValue>(loaders: ReadonlyMap<TKey, (
|
||||
};
|
||||
};
|
||||
|
||||
export const loadWidgetModule = createRetryableLoader(
|
||||
new Map(Object.entries(moduleLoaders) as [WidgetKind, () => Promise<WidgetModule>][]),
|
||||
const loadRegisteredWidgetModule = createRetryableLoader(
|
||||
new Map<WidgetKind, () => Promise<RegisteredWidgetModule>>(objectEntries(widgetModuleLoaders)),
|
||||
);
|
||||
|
||||
export const loadWidgetComponent = createRetryableLoader(
|
||||
new Map(Object.entries(componentLoaders) as [WidgetKind, () => Promise<WidgetComponentModule>][]),
|
||||
// ponytail: TypeScript cannot correlate a generic key with the matching value
|
||||
// in a Map. The registry itself is checked against WidgetImports, so this
|
||||
// assertion only restores that already-verified relationship after caching.
|
||||
const correlateWidgetPromise = <TKind extends WidgetKind, TType extends keyof WidgetManifestValue<TKind>>(
|
||||
_kind: TKind,
|
||||
_type: TType,
|
||||
promise: Promise<RegisteredWidgetManifestValue[TType]>,
|
||||
) => promise as Promise<WidgetManifestValue<TKind>[TType]>;
|
||||
|
||||
export const loadWidgetModule = <TKind extends WidgetKind>(kind: TKind) =>
|
||||
correlateWidgetPromise(kind, "module", loadRegisteredWidgetModule(kind));
|
||||
|
||||
const loadRegisteredWidgetComponent = createRetryableLoader(
|
||||
new Map<WidgetKind, () => Promise<RegisteredWidgetComponentModule>>(objectEntries(widgetComponentLoaders)),
|
||||
);
|
||||
|
||||
export const loadWidgetResources = createRetryableLoader(
|
||||
new Map(
|
||||
(Object.keys(moduleLoaders) as WidgetKind[]).map((kind) => [
|
||||
export const loadWidgetComponent = <TKind extends WidgetKind>(kind: TKind) =>
|
||||
correlateWidgetPromise(kind, "component", loadRegisteredWidgetComponent(kind));
|
||||
|
||||
const loadRegisteredWidgetResources = createRetryableLoader(
|
||||
new Map<WidgetKind, () => Promise<RegisteredWidgetResources>>(
|
||||
widgetKinds.map((kind) => [
|
||||
kind,
|
||||
async () => {
|
||||
const [{ definition }, { default: Component }] = await Promise.all([
|
||||
loadWidgetModule(kind),
|
||||
loadWidgetComponent(kind),
|
||||
loadRegisteredWidgetModule(kind),
|
||||
loadRegisteredWidgetComponent(kind),
|
||||
]);
|
||||
return { definition, Component } satisfies WidgetResources;
|
||||
return { definition, Component };
|
||||
},
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
export const loadWidgetDefinition = (kind: WidgetKind) => {
|
||||
const existing = definitionPromises.get(kind);
|
||||
if (existing) return existing;
|
||||
export const loadWidgetResources = <TKind extends WidgetKind>(kind: TKind) =>
|
||||
correlateWidgetPromise(kind, "resources", loadRegisteredWidgetResources(kind));
|
||||
|
||||
const promise = loadWidgetModule(kind).then(({ definition }) => definition);
|
||||
export const loadWidgetDefinition = <TKind extends WidgetKind>(kind: TKind) => {
|
||||
const existing = definitionPromises.get(kind);
|
||||
if (existing) {
|
||||
return correlateWidgetPromise(kind, "definition", existing);
|
||||
}
|
||||
|
||||
const promise = loadRegisteredWidgetModule(kind).then(({ definition }) => definition);
|
||||
definitionPromises.set(kind, promise);
|
||||
void promise.catch(() => {
|
||||
if (definitionPromises.get(kind) === promise) definitionPromises.delete(kind);
|
||||
});
|
||||
return promise;
|
||||
return correlateWidgetPromise(kind, "definition", promise);
|
||||
};
|
||||
|
||||
let allWidgetDefinitionsPromise: Promise<Map<WidgetKind, WidgetDefinition>> | undefined;
|
||||
@@ -228,12 +146,10 @@ export const reduceWidgetOptionsWithDefinition = (
|
||||
settings: Pick<SettingsContextProps, "enableStatusByDefault" | "forceDisableStatus">,
|
||||
currentValue: Record<string, unknown> = {},
|
||||
) => {
|
||||
const options = definition.createOptions(settings) as Record<string, WidgetOptionDefinition>;
|
||||
return objectEntries(options).reduce(
|
||||
(prev, [key, value]) => ({
|
||||
...prev,
|
||||
[key]: currentValue[key] ?? value.defaultValue,
|
||||
}),
|
||||
{} as Record<string, unknown>,
|
||||
);
|
||||
const options: Record<string, WidgetOptionDefinition> = definition.createOptions(settings);
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const [key, value] of objectEntries(options)) {
|
||||
result[key] = currentValue[key] ?? value.defaultValue;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconMovie } from "@tabler/icons-react";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition, matchesWidgetRuntimeQuery } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
@@ -25,5 +25,5 @@ export const { componentLoader, definition } = createWidgetDefinition("mediaMiss
|
||||
}),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("mediaOrganizer"),
|
||||
...getWidgetIntegrationConfig("mediaMissing"),
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IconTicket } from "@tabler/icons-react";
|
||||
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
@@ -34,5 +36,5 @@ export const { definition, componentLoader } = createWidgetDefinition("mediaRele
|
||||
}),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: ["mock", "emby", "jellyfin", "plex"],
|
||||
...getWidgetIntegrationConfig("mediaReleases"),
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IconSearch, IconZoomQuestion } from "@tabler/icons-react";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
import { openMediaRequestSearch } from "@homarr/spotlight";
|
||||
|
||||
import { createWidgetDefinition, widgetQueryInputMatches } from "../../definition";
|
||||
@@ -50,5 +50,5 @@ export const { componentLoader, definition } = createWidgetDefinition("mediaRequ
|
||||
},
|
||||
},
|
||||
],
|
||||
supportedIntegrations: getIntegrationKindsByCategory("mediaRequest"),
|
||||
...getWidgetIntegrationConfig("mediaRequests-requestList"),
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconChartBar, IconSearch } from "@tabler/icons-react";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
import { openMediaRequestSearch } from "@homarr/spotlight";
|
||||
|
||||
import { createWidgetDefinition } from "../../definition";
|
||||
@@ -22,5 +22,5 @@ export const { componentLoader, definition } = createWidgetDefinition("mediaRequ
|
||||
},
|
||||
},
|
||||
],
|
||||
supportedIntegrations: getIntegrationKindsByCategory("mediaRequest"),
|
||||
...getWidgetIntegrationConfig("mediaRequests-requestStats"),
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
@@ -425,9 +425,7 @@ export default function MediaServerWidget({
|
||||
style={isAdvanced ? { flexShrink: 0 } : zoomCompensatedSize(16)}
|
||||
/>
|
||||
<Text size="sm" fw={isAdvanced ? 500 : undefined} style={{ whiteSpace: "nowrap" }}>
|
||||
{(t as unknown as (key: string, params?: { count: number }) => string)("footer.streams", {
|
||||
count: flatSessions.length,
|
||||
})}
|
||||
{t("footer.streams", { count: flatSessions.length })}
|
||||
</Text>
|
||||
{totalBitrateLabel && (
|
||||
<>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconVideo } from "@tabler/icons-react";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition, widgetQueryInputMatches } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
@@ -22,5 +22,5 @@ export const { componentLoader, definition } = createWidgetDefinition("mediaServ
|
||||
showLocation: factory.switch({ defaultValue: true, withDescription: true }),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("mediaService"),
|
||||
...getWidgetIntegrationConfig("mediaServer"),
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
@@ -3,25 +3,13 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { getQueryKey } from "@trpc/react-query";
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Group,
|
||||
Pagination,
|
||||
Paper,
|
||||
ScrollArea,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { Button, Divider, Group, Pagination, Paper, ScrollArea, Select, SimpleGrid, Stack, Text } from "@mantine/core";
|
||||
import { IconClipboardList, IconCpu2, IconReportAnalytics } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
|
||||
import { views } from ".";
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
@@ -30,6 +18,7 @@ import { HealthCheckStatus } from "./health-check-status";
|
||||
import { QueuePanel } from "./panels/queue.panel";
|
||||
import { StatisticsPanel } from "./panels/statistics.panel";
|
||||
import { WorkersPanel } from "./panels/workers.panel";
|
||||
import { views } from "./views";
|
||||
|
||||
type View = (typeof views)[number];
|
||||
|
||||
|
||||
@@ -2,19 +2,20 @@ import { IconTransform } from "@tabler/icons-react";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { capitalize } from "@homarr/common";
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition, matchesWidgetRuntimeQuery } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
import { views } from "./views";
|
||||
|
||||
export const views = ["workers", "queue", "statistics"] as const;
|
||||
export { views };
|
||||
|
||||
export const { componentLoader, definition } = createWidgetDefinition("mediaTranscoding", {
|
||||
icon: IconTransform,
|
||||
supportsAdvancedFocus: true,
|
||||
queryKey: [["widget", "mediaTranscoding", "getDataAsync"]],
|
||||
queryMatcher: matchesWidgetRuntimeQuery,
|
||||
maxIntegrations: 1,
|
||||
...getWidgetIntegrationConfig("mediaTranscoding"),
|
||||
refetchInterval: null,
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
@@ -25,5 +26,4 @@ export const { componentLoader, definition } = createWidgetDefinition("mediaTran
|
||||
queuePageSize: factory.number({ defaultValue: 10, validate: z.number().min(1).max(30) }),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("mediaTranscoding"),
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
@@ -0,0 +1 @@
|
||||
export const views = ["workers", "queue", "statistics"] as const;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconServerOff, IconTopologyFull } from "@tabler/icons-react";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../../definition";
|
||||
import { optionsBuilder } from "../../options";
|
||||
@@ -21,7 +21,7 @@ export const { definition, componentLoader } = createWidgetDefinition("networkCo
|
||||
}),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("networkController"),
|
||||
...getWidgetIntegrationConfig("networkControllerStatus"),
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
icon: IconServerOff,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconServerOff, IconTopologyFull } from "@tabler/icons-react";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../../definition";
|
||||
import { optionsBuilder } from "../../options";
|
||||
@@ -13,7 +13,7 @@ export const { definition, componentLoader } = createWidgetDefinition("networkCo
|
||||
createOptions() {
|
||||
return optionsBuilder.from(() => ({}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("networkController"),
|
||||
...getWidgetIntegrationConfig("networkControllerSummary"),
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
icon: IconServerOff,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconMessage } from "@tabler/icons-react";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition, widgetQueryInputMatches } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
@@ -19,5 +19,5 @@ export const { componentLoader, definition } = createWidgetDefinition("notificat
|
||||
hideLogos: factory.switch({ defaultValue: false }),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("notifications"),
|
||||
...getWidgetIntegrationConfig("notifications"),
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IconFileText, IconServerOff } from "@tabler/icons-react";
|
||||
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
@@ -7,9 +9,7 @@ export const { definition, componentLoader } = createWidgetDefinition("paperless
|
||||
supportsAdvancedFocus: false,
|
||||
icon: IconFileText,
|
||||
refetchInterval: null,
|
||||
supportedIntegrations: ["paperlessNgx", "mock"],
|
||||
integrationsRequired: true,
|
||||
maxIntegrations: 1,
|
||||
...getWidgetIntegrationConfig("paperlessNgx"),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
showInboxRatio: factory.switch({
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { IconServerOff, IconShieldCheck } from "@tabler/icons-react";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
import { PATCHMON_STAT_COLOR_PRESETS } from "./stat-colors";
|
||||
@@ -9,9 +11,7 @@ import { patchmonOptionsSuperRefine } from "./threshold-validation";
|
||||
export const { definition, componentLoader } = createWidgetDefinition("patchmon", {
|
||||
supportsAdvancedFocus: true,
|
||||
icon: IconShieldCheck,
|
||||
supportedIntegrations: ["patchmon", "mock"],
|
||||
integrationsRequired: true,
|
||||
maxIntegrations: 1,
|
||||
...getWidgetIntegrationConfig("patchmon"),
|
||||
createOptions() {
|
||||
return optionsBuilder.from(
|
||||
(factory) => ({
|
||||
|
||||
@@ -5,32 +5,40 @@ import type { WidgetKind } from "@homarr/definitions";
|
||||
import { createSettings } from "@homarr/settings/creator";
|
||||
|
||||
import { loadWidgetDefinition, reduceWidgetOptionsWithDefinition } from "./manifest";
|
||||
import prefetchForApps from "./app/prefetch";
|
||||
import prefetchForBookmarks from "./bookmarks/prefetch";
|
||||
import type { Prefetch, WidgetOptionsRecordOf } from "./definition";
|
||||
import type { inferOptionsFromCreator } from "./options";
|
||||
import type { PrefetchLoader } from "./definition";
|
||||
|
||||
const prefetchCallbacks: Partial<{
|
||||
[TKind in WidgetKind]: Prefetch<TKind>;
|
||||
}> = {
|
||||
bookmarks: prefetchForBookmarks,
|
||||
app: prefetchForApps,
|
||||
};
|
||||
const definePrefetchLoaders = <TLoaders extends Partial<Record<WidgetKind, PrefetchLoader>>>(loaders: TLoaders) =>
|
||||
loaders;
|
||||
|
||||
export const prefetchForKindAsync = async <TKind extends WidgetKind>(
|
||||
kind: TKind,
|
||||
// Keep these imports explicit so Next.js can trace each optional prefetch
|
||||
// module without loading its database-specific implementation eagerly.
|
||||
const prefetchLoaders = definePrefetchLoaders({
|
||||
app: () => import("./app/prefetch"),
|
||||
bookmarks: () => import("./bookmarks/prefetch"),
|
||||
});
|
||||
|
||||
type PrefetchWidgetKind = keyof typeof prefetchLoaders;
|
||||
|
||||
const hasPrefetchLoader = (kind: WidgetKind): kind is PrefetchWidgetKind =>
|
||||
Object.prototype.hasOwnProperty.call(prefetchLoaders, kind);
|
||||
|
||||
export const prefetchForKindAsync = async (
|
||||
kind: WidgetKind,
|
||||
queryClient: QueryClient,
|
||||
items: {
|
||||
options: inferOptionsFromCreator<WidgetOptionsRecordOf<TKind>>;
|
||||
options: Record<string, unknown>;
|
||||
integrationIds: string[];
|
||||
}[],
|
||||
) => {
|
||||
const callback = prefetchCallbacks[kind];
|
||||
if (!callback) {
|
||||
if (!hasPrefetchLoader(kind)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [serverSettings, definition] = await Promise.all([getRscServerSettingsAsync(), loadWidgetDefinition(kind)]);
|
||||
const [{ default: callback }, serverSettings, definition] = await Promise.all([
|
||||
prefetchLoaders[kind](),
|
||||
getRscServerSettingsAsync(),
|
||||
loadWidgetDefinition(kind),
|
||||
]);
|
||||
|
||||
const itemsWithDefaultOptions = items.map((item) => ({
|
||||
...item,
|
||||
@@ -41,5 +49,5 @@ export const prefetchForKindAsync = async <TKind extends WidgetKind>(
|
||||
),
|
||||
}));
|
||||
|
||||
await callback(queryClient, itemsWithDefaultOptions as never[]);
|
||||
await callback(queryClient, itemsWithDefaultOptions);
|
||||
};
|
||||
@@ -1,19 +1,26 @@
|
||||
/**
|
||||
* Query policy used by the root client provider.
|
||||
* Lightweight query defaults for the root client provider.
|
||||
*
|
||||
* Keep this module free of widget component/definition imports. Importing the
|
||||
* full widget registry from a root provider makes every widget part of the
|
||||
* initial client graph, even before a board has rendered one widget.
|
||||
* Keep this list free of widget imports: importing widget definitions here
|
||||
* would put every widget in the initial client bundle.
|
||||
*/
|
||||
export const widgetQueryRefetchIntervals = [
|
||||
{ queryKey: [["docker", "getContainers"]], intervalSeconds: 30 },
|
||||
{ queryKey: [["widget", "dnsHole"]], intervalSeconds: 10 },
|
||||
{ queryKey: [["widget", "downloads", "getJobsAndStatuses"]], intervalSeconds: 10 },
|
||||
{
|
||||
queryKey: [["widget", "downloads", "getJobsAndStatuses"]],
|
||||
intervalSeconds: 10,
|
||||
staleTimeSeconds: 10,
|
||||
},
|
||||
{ queryKey: [["widget", "firewall"]], intervalSeconds: 10 },
|
||||
{ queryKey: [["widget", "healthMonitoring"]], intervalSeconds: 10 },
|
||||
{ queryKey: [["widget", "mediaServer", "getCurrentStreams"]], intervalSeconds: 10 },
|
||||
{ queryKey: [["widget", "tracearr"]], intervalSeconds: 10 },
|
||||
{ queryKey: [["widget", "weather", "atLocation"]], intervalSeconds: 600 },
|
||||
{
|
||||
queryKey: [["widget", "weather", "atLocation"]],
|
||||
intervalSeconds: 600,
|
||||
staleTimeSeconds: 300,
|
||||
},
|
||||
{ queryKey: [["widget", "airQuality", "atLocation"]], intervalSeconds: 900 },
|
||||
{ queryKey: [["widget", "immich", "getAlbum"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "immich", "getServerStats"]], intervalSeconds: null },
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
|
||||
|
||||
import type * as airQuality from "./air-quality";
|
||||
import type * as anchorNote from "./anchor-note";
|
||||
import type * as app from "./app";
|
||||
import type * as archiveTeamWarrior from "./archive-team-warrior";
|
||||
import type * as assistant from "./assistant";
|
||||
import type * as audioStats from "./audio-stats";
|
||||
import type * as bazarr from "./bazarr";
|
||||
import type * as beszelAlerts from "./beszel-alerts";
|
||||
import type * as beszelSystemGrid from "./beszel-system-grid";
|
||||
import type * as beszelSystemStats from "./beszel-system-stats";
|
||||
import type * as beszelSystemTable from "./beszel-system-table";
|
||||
import type * as bookmarks from "./bookmarks";
|
||||
import type * as calendar from "./calendar";
|
||||
import type * as clock from "./clock";
|
||||
import type * as coolify from "./coolify";
|
||||
import type * as countdown from "./countdown";
|
||||
import type * as customApi from "./custom-api";
|
||||
import type * as dnsHoleControls from "./dns-hole/controls";
|
||||
import type * as dnsHoleSummary from "./dns-hole/summary";
|
||||
import type * as dockerContainers from "./docker";
|
||||
import type * as downloads from "./downloads";
|
||||
import type * as firewall from "./firewall";
|
||||
import type * as healthMonitoring from "./health-monitoring";
|
||||
import type * as iframe from "./iframe";
|
||||
import type * as immichAlbumCarousel from "./immich/album-carousel";
|
||||
import type * as immichServerStats from "./immich/server-stats";
|
||||
import type * as indexerManager from "./indexer-manager";
|
||||
import type * as mediaMissing from "./media-missing";
|
||||
import type * as mediaReleases from "./media-releases";
|
||||
import type * as mediaRequestsList from "./media-requests/list";
|
||||
import type * as mediaRequestsStats from "./media-requests/stats";
|
||||
import type * as mediaServer from "./media-server";
|
||||
import type * as mediaTranscoding from "./media-transcoding";
|
||||
import type * as minecraftServerStatus from "./minecraft/server-status";
|
||||
import type * as networkControllerStatus from "./network-controller/network-status";
|
||||
import type * as networkControllerSummary from "./network-controller/summary";
|
||||
import type * as notebook from "./notebook";
|
||||
import type * as notifications from "./notifications";
|
||||
import type * as paperlessNgx from "./paperless-ngx";
|
||||
import type * as patchmon from "./patchmon";
|
||||
import type * as releases from "./releases";
|
||||
import type * as rssFeed from "./rssFeed";
|
||||
import type * as smartHomeEntityState from "./smart-home/entity-state";
|
||||
import type * as smartHomeExecuteAutomation from "./smart-home/execute-automation";
|
||||
import type * as speedtestTracker from "./speedtest-tracker";
|
||||
import type * as stockPrice from "./stocks";
|
||||
import type * as systemDisks from "./system-disks";
|
||||
import type * as systemResources from "./system-resources";
|
||||
import type * as timetable from "./timetable";
|
||||
import type * as timer from "./timer";
|
||||
import type * as tracearr from "./tracearr";
|
||||
import type * as traefik from "./traefik";
|
||||
import type * as umami from "./umami";
|
||||
import type * as ups from "./ups";
|
||||
import type * as uptimeKuma from "./uptime-kuma";
|
||||
import type * as video from "./video";
|
||||
import type * as vpn from "./vpn";
|
||||
import type * as weather from "./weather";
|
||||
import type * as wud from "./wud";
|
||||
|
||||
// Keep these imports explicit so Next.js and Turbopack can discover every widget
|
||||
// module without loading any widget definition or component eagerly.
|
||||
export const widgetModuleLoaders = {
|
||||
clock: () => import("./clock"),
|
||||
airQuality: () => import("./air-quality"),
|
||||
countdown: () => import("./countdown"),
|
||||
timer: () => import("./timer"),
|
||||
app: () => import("./app"),
|
||||
assistant: () => import("./assistant"),
|
||||
archiveTeamWarrior: () => import("./archive-team-warrior"),
|
||||
anchorNote: () => import("./anchor-note"),
|
||||
notebook: () => import("./notebook"),
|
||||
iframe: () => import("./iframe"),
|
||||
video: () => import("./video"),
|
||||
dnsHoleSummary: () => import("./dns-hole/summary"),
|
||||
dnsHoleControls: () => import("./dns-hole/controls"),
|
||||
"smartHome-entityState": () => import("./smart-home/entity-state"),
|
||||
"smartHome-executeAutomation": () => import("./smart-home/execute-automation"),
|
||||
stockPrice: () => import("./stocks"),
|
||||
mediaServer: () => import("./media-server"),
|
||||
calendar: () => import("./calendar"),
|
||||
downloads: () => import("./downloads"),
|
||||
"mediaRequests-requestList": () => import("./media-requests/list"),
|
||||
"mediaRequests-requestStats": () => import("./media-requests/stats"),
|
||||
mediaMissing: () => import("./media-missing"),
|
||||
networkControllerSummary: () => import("./network-controller/summary"),
|
||||
networkControllerStatus: () => import("./network-controller/network-status"),
|
||||
rssFeed: () => import("./rssFeed"),
|
||||
bookmarks: () => import("./bookmarks"),
|
||||
bazarr: () => import("./bazarr"),
|
||||
indexerManager: () => import("./indexer-manager"),
|
||||
healthMonitoring: () => import("./health-monitoring"),
|
||||
mediaTranscoding: () => import("./media-transcoding"),
|
||||
minecraftServerStatus: () => import("./minecraft/server-status"),
|
||||
dockerContainers: () => import("./docker"),
|
||||
releases: () => import("./releases"),
|
||||
firewall: () => import("./firewall"),
|
||||
notifications: () => import("./notifications"),
|
||||
mediaReleases: () => import("./media-releases"),
|
||||
systemResources: () => import("./system-resources"),
|
||||
coolify: () => import("./coolify"),
|
||||
systemDisks: () => import("./system-disks"),
|
||||
timetable: () => import("./timetable"),
|
||||
"immich-serverStats": () => import("./immich/server-stats"),
|
||||
"immich-albumCarousel": () => import("./immich/album-carousel"),
|
||||
paperlessNgx: () => import("./paperless-ngx"),
|
||||
patchmon: () => import("./patchmon"),
|
||||
tracearr: () => import("./tracearr"),
|
||||
speedtestTracker: () => import("./speedtest-tracker"),
|
||||
uptimeKuma: () => import("./uptime-kuma"),
|
||||
audioStats: () => import("./audio-stats"),
|
||||
umami: () => import("./umami"),
|
||||
vpn: () => import("./vpn"),
|
||||
ups: () => import("./ups"),
|
||||
beszelSystemTable: () => import("./beszel-system-table"),
|
||||
beszelSystemGrid: () => import("./beszel-system-grid"),
|
||||
beszelAlerts: () => import("./beszel-alerts"),
|
||||
beszelSystemStats: () => import("./beszel-system-stats"),
|
||||
traefik: () => import("./traefik"),
|
||||
customApi: () => import("./custom-api"),
|
||||
weather: () => import("./weather"),
|
||||
wud: () => import("./wud"),
|
||||
} satisfies { [TKind in WidgetKind]: () => Promise<WidgetImports[TKind]> };
|
||||
|
||||
export type WidgetImports = {
|
||||
clock: typeof clock;
|
||||
airQuality: typeof airQuality;
|
||||
countdown: typeof countdown;
|
||||
timer: typeof timer;
|
||||
app: typeof app;
|
||||
assistant: typeof assistant;
|
||||
archiveTeamWarrior: typeof archiveTeamWarrior;
|
||||
anchorNote: typeof anchorNote;
|
||||
notebook: typeof notebook;
|
||||
iframe: typeof iframe;
|
||||
video: typeof video;
|
||||
dnsHoleSummary: typeof dnsHoleSummary;
|
||||
dnsHoleControls: typeof dnsHoleControls;
|
||||
"smartHome-entityState": typeof smartHomeEntityState;
|
||||
"smartHome-executeAutomation": typeof smartHomeExecuteAutomation;
|
||||
stockPrice: typeof stockPrice;
|
||||
mediaServer: typeof mediaServer;
|
||||
calendar: typeof calendar;
|
||||
downloads: typeof downloads;
|
||||
"mediaRequests-requestList": typeof mediaRequestsList;
|
||||
"mediaRequests-requestStats": typeof mediaRequestsStats;
|
||||
mediaMissing: typeof mediaMissing;
|
||||
networkControllerSummary: typeof networkControllerSummary;
|
||||
networkControllerStatus: typeof networkControllerStatus;
|
||||
rssFeed: typeof rssFeed;
|
||||
bookmarks: typeof bookmarks;
|
||||
bazarr: typeof bazarr;
|
||||
indexerManager: typeof indexerManager;
|
||||
healthMonitoring: typeof healthMonitoring;
|
||||
mediaTranscoding: typeof mediaTranscoding;
|
||||
minecraftServerStatus: typeof minecraftServerStatus;
|
||||
dockerContainers: typeof dockerContainers;
|
||||
releases: typeof releases;
|
||||
firewall: typeof firewall;
|
||||
notifications: typeof notifications;
|
||||
mediaReleases: typeof mediaReleases;
|
||||
systemResources: typeof systemResources;
|
||||
coolify: typeof coolify;
|
||||
systemDisks: typeof systemDisks;
|
||||
timetable: typeof timetable;
|
||||
"immich-serverStats": typeof immichServerStats;
|
||||
"immich-albumCarousel": typeof immichAlbumCarousel;
|
||||
paperlessNgx: typeof paperlessNgx;
|
||||
patchmon: typeof patchmon;
|
||||
tracearr: typeof tracearr;
|
||||
speedtestTracker: typeof speedtestTracker;
|
||||
uptimeKuma: typeof uptimeKuma;
|
||||
audioStats: typeof audioStats;
|
||||
umami: typeof umami;
|
||||
vpn: typeof vpn;
|
||||
ups: typeof ups;
|
||||
beszelSystemTable: typeof beszelSystemTable;
|
||||
beszelSystemGrid: typeof beszelSystemGrid;
|
||||
beszelAlerts: typeof beszelAlerts;
|
||||
beszelSystemStats: typeof beszelSystemStats;
|
||||
traefik: typeof traefik;
|
||||
customApi: typeof customApi;
|
||||
weather: typeof weather;
|
||||
wud: typeof wud;
|
||||
};
|
||||
|
||||
type WidgetComponentLoaders = {
|
||||
[TKind in WidgetKind]: WidgetImports[TKind]["componentLoader"];
|
||||
};
|
||||
|
||||
// Keep component imports literal and separate from widgetModuleLoaders. The
|
||||
// manifest starts both chunk requests together on a cold widget load; resolving
|
||||
// this loader through the widget module would serialize those requests.
|
||||
export const widgetComponentLoaders = {
|
||||
clock: () => import("./clock/component"),
|
||||
airQuality: () => import("./air-quality/component"),
|
||||
countdown: () => import("./countdown/component"),
|
||||
timer: () => import("./timer/component"),
|
||||
app: () => import("./app/component"),
|
||||
assistant: () => import("./assistant/component"),
|
||||
archiveTeamWarrior: () => import("./archive-team-warrior/component"),
|
||||
anchorNote: () => import("./anchor-note/component"),
|
||||
notebook: () => import("./notebook/component"),
|
||||
iframe: () => import("./iframe/component"),
|
||||
video: () => import("./video/component"),
|
||||
dnsHoleSummary: () => import("./dns-hole/summary/component"),
|
||||
dnsHoleControls: () => import("./dns-hole/controls/component"),
|
||||
"smartHome-entityState": () => import("./smart-home/entity-state/component"),
|
||||
"smartHome-executeAutomation": () => import("./smart-home/execute-automation/component"),
|
||||
stockPrice: () => import("./stocks/component"),
|
||||
mediaServer: () => import("./media-server/component"),
|
||||
calendar: () => import("./calendar/component"),
|
||||
downloads: () => import("./downloads/component"),
|
||||
"mediaRequests-requestList": () => import("./media-requests/list/component"),
|
||||
"mediaRequests-requestStats": () => import("./media-requests/stats/component"),
|
||||
mediaMissing: () => import("./media-missing/component"),
|
||||
networkControllerSummary: () => import("./network-controller/summary/component"),
|
||||
networkControllerStatus: () => import("./network-controller/network-status/component"),
|
||||
rssFeed: () => import("./rssFeed/component"),
|
||||
bookmarks: () => import("./bookmarks/component"),
|
||||
bazarr: () => import("./bazarr/component"),
|
||||
indexerManager: () => import("./indexer-manager/component"),
|
||||
healthMonitoring: () => import("./health-monitoring/component"),
|
||||
mediaTranscoding: () => import("./media-transcoding/component"),
|
||||
minecraftServerStatus: () => import("./minecraft/server-status/component"),
|
||||
dockerContainers: () => import("./docker/component"),
|
||||
releases: () => import("./releases/component"),
|
||||
firewall: () => import("./firewall/component"),
|
||||
notifications: () => import("./notifications/component"),
|
||||
mediaReleases: () => import("./media-releases/component"),
|
||||
systemResources: () => import("./system-resources/component"),
|
||||
coolify: () => import("./coolify/component"),
|
||||
systemDisks: () => import("./system-disks/component"),
|
||||
timetable: () => import("./timetable/component"),
|
||||
"immich-serverStats": () => import("./immich/server-stats/component"),
|
||||
"immich-albumCarousel": () => import("./immich/album-carousel/component"),
|
||||
paperlessNgx: () => import("./paperless-ngx/component"),
|
||||
patchmon: () => import("./patchmon/component"),
|
||||
tracearr: () => import("./tracearr/component"),
|
||||
speedtestTracker: () => import("./speedtest-tracker/component"),
|
||||
uptimeKuma: () => import("./uptime-kuma/component"),
|
||||
audioStats: () => import("./audio-stats/component"),
|
||||
umami: () => import("./umami/component"),
|
||||
vpn: () => import("./vpn/component"),
|
||||
ups: () => import("./ups/component"),
|
||||
beszelSystemTable: () => import("./beszel-system-table/component"),
|
||||
beszelSystemGrid: () => import("./beszel-system-grid/component"),
|
||||
beszelAlerts: () => import("./beszel-alerts/component"),
|
||||
beszelSystemStats: () => import("./beszel-system-stats/component"),
|
||||
traefik: () => import("./traefik/component"),
|
||||
customApi: () => import("./custom-api/component"),
|
||||
weather: () => import("./weather/component"),
|
||||
wud: () => import("./wud/component"),
|
||||
} satisfies WidgetComponentLoaders;
|
||||
|
||||
export type WidgetImportKey = keyof WidgetImports;
|
||||
|
||||
export type inferSupportedIntegrations<TKind extends WidgetKind> = (WidgetImports[TKind]["definition"] extends {
|
||||
supportedIntegrations: string[];
|
||||
}
|
||||
? WidgetImports[TKind]["definition"]["supportedIntegrations"]
|
||||
: string[])[number];
|
||||
|
||||
export type inferSupportedIntegrationsStrict<TKind extends WidgetKind> = (WidgetImports[TKind]["definition"] extends {
|
||||
supportedIntegrations: IntegrationKind[];
|
||||
}
|
||||
? WidgetImports[TKind]["definition"]["supportedIntegrations"]
|
||||
: never[])[number];
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconBinaryTree, IconServerOff } from "@tabler/icons-react";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../../definition";
|
||||
import { optionsBuilder } from "../../options";
|
||||
@@ -9,7 +9,7 @@ export const { definition, componentLoader } = createWidgetDefinition("smartHome
|
||||
icon: IconBinaryTree,
|
||||
supportsAdvancedFocus: true,
|
||||
queryKey: [["widget", "smartHome"]],
|
||||
maxIntegrations: 1,
|
||||
...getWidgetIntegrationConfig("smartHome-entityState"),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
entityId: factory.text({
|
||||
@@ -22,7 +22,6 @@ export const { definition, componentLoader } = createWidgetDefinition("smartHome
|
||||
clickable: factory.switch(),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("smartHomeServer"),
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
icon: IconServerOff,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconBinaryTree } from "@tabler/icons-react";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../../definition";
|
||||
import { optionsBuilder } from "../../options";
|
||||
@@ -8,12 +8,11 @@ import { optionsBuilder } from "../../options";
|
||||
export const { definition, componentLoader } = createWidgetDefinition("smartHome-executeAutomation", {
|
||||
icon: IconBinaryTree,
|
||||
supportsAdvancedFocus: true,
|
||||
maxIntegrations: 1,
|
||||
...getWidgetIntegrationConfig("smartHome-executeAutomation"),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
displayName: factory.text(),
|
||||
automationId: factory.text(),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("smartHomeServer"),
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IconServerOff, IconSpeedboat } from "@tabler/icons-react";
|
||||
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
@@ -29,7 +31,7 @@ export const { definition, componentLoader } = createWidgetDefinition("speedtest
|
||||
},
|
||||
);
|
||||
},
|
||||
supportedIntegrations: ["speedtestTracker", "mock"],
|
||||
...getWidgetIntegrationConfig("speedtestTracker"),
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
icon: IconServerOff,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IconServer2 } from "@tabler/icons-react";
|
||||
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
import { createStorageVolumeMultiSelectOptions } from "../storage-volume-options";
|
||||
@@ -9,7 +11,7 @@ export const { definition, componentLoader } = createWidgetDefinition("systemDis
|
||||
icon: IconServer2,
|
||||
queryKey: [["widget", "healthMonitoring"]],
|
||||
refetchInterval: 10,
|
||||
supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "synology", "mock"],
|
||||
...getWidgetIntegrationConfig("systemDisks"),
|
||||
createOptions() {
|
||||
return optionsBuilder.from(
|
||||
(factory) => ({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IconAlignLeft, IconEyeOff, IconGraphFilled, IconListDetails, IconPhoto } from "@tabler/icons-react";
|
||||
|
||||
import { objectEntries } from "@homarr/common";
|
||||
import { invariantTechnicalLabels } from "@homarr/definitions";
|
||||
import { getWidgetIntegrationConfig, invariantTechnicalLabels } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
@@ -24,7 +24,7 @@ export const { definition, componentLoader } = createWidgetDefinition("systemRes
|
||||
supportsAdvancedFocus: true,
|
||||
queryKey: [["widget", "healthMonitoring"]],
|
||||
refetchInterval: 10,
|
||||
supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "glances", "synology", "mock"],
|
||||
...getWidgetIntegrationConfig("systemResources"),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
hasShadow: factory.switch({ defaultValue: true }),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IconActivityHeartbeat, IconServerOff } from "@tabler/icons-react";
|
||||
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
@@ -24,7 +26,7 @@ export const { definition, componentLoader } = createWidgetDefinition("tracearr"
|
||||
}),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: ["tracearr", "mock"],
|
||||
...getWidgetIntegrationConfig("tracearr"),
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
icon: IconServerOff,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IconRoute, IconRouteOff } from "@tabler/icons-react";
|
||||
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
@@ -19,7 +21,7 @@ export const { definition, componentLoader } = createWidgetDefinition("traefik",
|
||||
}),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: ["traefik", "mock"],
|
||||
...getWidgetIntegrationConfig("traefik"),
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
icon: IconRouteOff,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { IconChartBar, IconServerOff } from "@tabler/icons-react";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition, matchesWidgetRuntimeQuery } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
@@ -18,9 +20,7 @@ export const { definition, componentLoader } = createWidgetDefinition("umami", {
|
||||
],
|
||||
queryMatcher: matchesWidgetRuntimeQuery,
|
||||
refetchInterval: null,
|
||||
supportedIntegrations: ["umami", "mock"],
|
||||
integrationsRequired: true,
|
||||
maxIntegrations: 1,
|
||||
...getWidgetIntegrationConfig("umami"),
|
||||
createOptions() {
|
||||
return optionsBuilder.from(
|
||||
(factory) => ({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconBatteryCharging, IconBatteryOff } from "@tabler/icons-react";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
@@ -21,7 +21,7 @@ export const { definition, componentLoader } = createWidgetDefinition("ups", {
|
||||
}),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("ups"),
|
||||
...getWidgetIntegrationConfig("ups"),
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
icon: IconBatteryOff,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IconHeartbeat, IconServerOff } from "@tabler/icons-react";
|
||||
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
@@ -34,7 +36,7 @@ export const { definition, componentLoader } = createWidgetDefinition("uptimeKum
|
||||
}),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: ["uptimeKuma", "mock"],
|
||||
...getWidgetIntegrationConfig("uptimeKuma"),
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
icon: IconServerOff,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconShieldLock } from "@tabler/icons-react";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
@@ -12,5 +12,5 @@ export const { componentLoader, definition } = createWidgetDefinition("vpn", {
|
||||
createOptions() {
|
||||
return optionsBuilder.from(() => ({}));
|
||||
},
|
||||
supportedIntegrations: [...getIntegrationKindsByCategory("vpn"), "mock"],
|
||||
...getWidgetIntegrationConfig("vpn"),
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IconBrandDocker } from "@tabler/icons-react";
|
||||
|
||||
import { getWidgetIntegrationConfig } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
@@ -7,9 +9,7 @@ export const { definition, componentLoader } = createWidgetDefinition("wud", {
|
||||
icon: IconBrandDocker,
|
||||
supportsAdvancedFocus: true,
|
||||
refetchInterval: null,
|
||||
supportedIntegrations: ["wud", "mock"],
|
||||
integrationsRequired: true,
|
||||
maxIntegrations: 1,
|
||||
...getWidgetIntegrationConfig("wud"),
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
showTitle: factory.switch({ defaultValue: true }),
|
||||
|
||||
Reference in new issue
Block a user