feat(widgets): consolidate integration metadata + shared response caching (#6691)

This commit is contained in:
Thomas authored and GitHub committed 2026-08-30 18:38:07 +02:00
1 parent 7d1d9062ed
commit 8d8dec5749
206 files changed
+3893 -1946

No files matched your search

@@ -8,7 +8,7 @@ describe("buildSortedIntegrations", () => {
it("keeps every real integration available during onboarding", () => {
const kinds = buildSortedIntegrations({ onboarding: true }).map((integration) => integration.kind);
expect(kinds).toHaveLength(integrationKinds.length - 1);
expect(kinds).toHaveLength(integrationKinds.filter((kind) => kind !== "mock").length);
expect(kinds).not.toContain("mock");
});
@@ -1,9 +1,8 @@
import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
import {
featuredIntegrations,
getIntegrationName,
getIntegrationOnboardingMetadata,
getWidgetKindsForIntegration,
hiddenFromOnboarding,
integrationDefs,
integrationKinds,
} from "@homarr/definitions";
@@ -56,7 +55,7 @@ export const buildSortedIntegrations = (
integrationKinds
.filter((kind) => {
if (options.allowedKinds && !options.allowedKinds.includes(kind)) return false;
if (options.onboarding && hiddenFromOnboarding.has(kind)) return false;
if (options.onboarding && getIntegrationOnboardingMetadata(kind).hidden) return false;
if (!options.enableMockIntegration && kind === "mock") return false;
return true;
})
@@ -67,11 +66,11 @@ export const buildSortedIntegrations = (
widgets: getWidgetKindsForIntegration(kind),
}))
.toSorted((left, right) => {
const leftIndex = featuredIntegrations.indexOf(left.kind);
const rightIndex = featuredIntegrations.indexOf(right.kind);
if (leftIndex !== -1 && rightIndex !== -1) return leftIndex - rightIndex;
if (leftIndex !== -1) return -1;
if (rightIndex !== -1) return 1;
const leftFeaturedOrder = getIntegrationOnboardingMetadata(left.kind).featuredOrder;
const rightFeaturedOrder = getIntegrationOnboardingMetadata(right.kind).featuredOrder;
if (leftFeaturedOrder !== null && rightFeaturedOrder !== null) return leftFeaturedOrder - rightFeaturedOrder;
if (leftFeaturedOrder !== null) return -1;
if (rightFeaturedOrder !== null) return 1;
return right.widgets.length - left.widgets.length || left.name.localeCompare(right.name);
});
@@ -2,45 +2,7 @@
import { useMemo, useState } from "react";
import { Badge, Center, Group, Input, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core";
import {
IconAd,
IconApps,
IconBinaryTree,
IconBookmark,
IconBrandDocker,
IconBrandMinecraft,
IconBrowser,
IconBuildingBank,
IconBusStop,
IconCalendar,
IconChartBar,
IconClock,
IconClockPlay,
IconCloud,
IconDeviceCctv,
IconDeviceGamepad,
IconDownload,
IconGraphFilled,
IconHeartRateMonitor,
IconHourglass,
IconMessage,
IconMovie,
IconNotes,
IconPuzzle,
IconReportSearch,
IconRobot,
IconRocket,
IconRss,
IconSearch,
IconServer2,
IconTicket,
IconTopologyFull,
IconTransform,
IconVideo,
IconWall,
IconWind,
IconZoomQuestion,
} from "@tabler/icons-react";
import { IconSearch } from "@tabler/icons-react";
import { getWidgetName } from "@homarr/definitions";
import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
@@ -49,6 +11,7 @@ import { useI18n } from "@homarr/translation/client";
import { IntegrationAvatar } from "../components/integration-avatar";
import { SelectableCard } from "../components/selectable-card";
import { selectGridCols } from "../select-grid-layout";
import { widgetCatalogIcons } from "../widget-icons";
import { buildSortedIntegrations, filterIntegrations } from "./integration-grid-shared";
import type { IntegrationGridItem } from "./integration-grid-shared";
@@ -155,48 +118,6 @@ const IntegrationCard = ({
);
};
const widgetIconsMap: Partial<Record<WidgetKind, typeof IconPuzzle>> = {
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,
};
const IntegrationTiedWidgets = ({ widgets, limit }: { widgets: WidgetKind[]; limit?: number }) => {
const t = useI18n();
if (widgets.length === 0) {
@@ -213,7 +134,7 @@ const IntegrationTiedWidgets = ({ widgets, limit }: { widgets: WidgetKind[]; lim
return (
<Group gap={4} wrap="wrap">
{items.map((widgetKind) => {
const WidgetIcon = widgetIconsMap[widgetKind] ?? IconPuzzle;
const WidgetIcon = widgetCatalogIcons[widgetKind];
return (
<Badge
key={widgetKind}