From 1df84cbccd76d186b5b6d10b620587a0e393d07a Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 27 May 2026 15:55:40 +0200 Subject: [PATCH] feat(onboarding): hide 0-widget integrations and add selectable app grid - Filter out integrations with no widgets from onboarding selection - Replace single checkbox app toggle with a compact selectable grid - Each Docker-discovered app is individually toggleable (all on by default) - Track selected apps by container ID instead of a boolean flag --- .../integrations/docker-discovery-panel.tsx | 114 +++++++++++------- .../_steps/integrations/init-integrations.tsx | 24 +++- .../integration/integration-grid-shared.ts | 1 + packages/translation/src/lang/en.json | 2 +- 4 files changed, 92 insertions(+), 49 deletions(-) diff --git a/apps/nextjs/src/app/[locale]/init/_steps/integrations/docker-discovery-panel.tsx b/apps/nextjs/src/app/[locale]/init/_steps/integrations/docker-discovery-panel.tsx index 1756fd12d..38d1b0dc5 100644 --- a/apps/nextjs/src/app/[locale]/init/_steps/integrations/docker-discovery-panel.tsx +++ b/apps/nextjs/src/app/[locale]/init/_steps/integrations/docker-discovery-panel.tsx @@ -4,17 +4,18 @@ import { useEffect, useRef, useState } from "react"; import { Avatar, Badge, - Checkbox, + Card, Collapse, - Divider, Group, Paper, + ScrollArea, + SimpleGrid, Stack, Text, ThemeIcon, UnstyledButton, } from "@mantine/core"; -import { IconBrandDocker, IconChevronDown } from "@tabler/icons-react"; +import { IconBrandDocker, IconCheck, IconChevronDown } from "@tabler/icons-react"; import type { IntegrationKind } from "@homarr/definitions"; import { getIntegrationName } from "@homarr/definitions"; @@ -40,31 +41,36 @@ export interface DiscoveredApp { interface DockerDiscoveryIndicatorProps { integrations: DiscoveredIntegration[]; apps: DiscoveredApp[]; - appsEnabled: boolean; - onToggleAllApps: (checked: boolean) => void; + selectedAppIds: Set; + onToggleApp: (containerId: string) => void; } +const APP_CARD_HEIGHT = 80; + export const DockerDiscoveryIndicator = ({ integrations, apps, - appsEnabled, - onToggleAllApps, + selectedAppIds, + onToggleApp, }: DockerDiscoveryIndicatorProps) => { const hasApps = apps.length > 0; const [expanded, setExpanded] = useState(false); const expandedOnce = useRef(false); useEffect(() => { - if (hasApps && !expandedOnce.current) { + if ((hasApps || integrations.length > 0) && !expandedOnce.current) { expandedOnce.current = true; setExpanded(true); } - }, [hasApps]); + }, [hasApps, integrations.length]); + const t = useScopedI18n("init.step.integrations.docker"); const totalCount = integrations.length + apps.length; if (totalCount === 0) return null; + const selectedAppCount = apps.filter((app) => selectedAppIds.has(app.containerId)).length; + return ( - setExpanded((v) => !v)} w="100%"> + setExpanded((prev) => !prev)} w="100%"> @@ -105,41 +111,63 @@ export const DockerDiscoveryIndicator = ({ - - {integrations.map((integration) => ( - - - - {integration.containerName} - - - {getIntegrationName(integration.kind)} - - - ))} + + {integrations.length > 0 && ( + + {integrations.map((integration) => ( + + + + {integration.containerName} + + + {getIntegrationName(integration.kind)} + + + ))} + + )} {hasApps && ( - <> - {integrations.length > 0 && } - onToggleAllApps(!appsEnabled)} - styles={{ label: { fontSize: "var(--mantine-font-size-xs)" } }} - /> - {appsEnabled && - apps.map((app) => ( - - - {app.containerName.at(0)?.toUpperCase()} - - - {app.containerName} - - - ))} - + + + {t("appSectionLabel", { count: String(selectedAppCount) })} + + + + {apps.map((app) => { + const isSelected = selectedAppIds.has(app.containerId); + return ( + onToggleApp(app.containerId)} + > + + + + {app.containerName} + + {isSelected && } + + + {app.containerName.at(0)?.toUpperCase()} + + + + ); + })} + + + )} diff --git a/apps/nextjs/src/app/[locale]/init/_steps/integrations/init-integrations.tsx b/apps/nextjs/src/app/[locale]/init/_steps/integrations/init-integrations.tsx index f733b7097..59446c0f2 100644 --- a/apps/nextjs/src/app/[locale]/init/_steps/integrations/init-integrations.tsx +++ b/apps/nextjs/src/app/[locale]/init/_steps/integrations/init-integrations.tsx @@ -64,7 +64,7 @@ export const InitIntegrations = () => { const dockerIntegrations = dockerData?.status === "success" ? dockerData.integrations : []; const dockerApps = dockerData?.status === "success" ? dockerData.apps : []; - const [addDockerApps, setAddDockerApps] = useState(true); + const [selectedAppIds, setSelectedAppIds] = useState>(new Set()); const dockerApplied = useRef(false); useEffect(() => { @@ -89,12 +89,25 @@ export const InitIntegrations = () => { setDrafts(newDrafts); setSelectedKinds(Array.from(newKinds)); + setSelectedAppIds(new Set(dockerData.apps.map((app) => app.containerId))); }, [dockerData]); const handleSelectionChange = useCallback((kinds: IntegrationKind[]) => { setSelectedKinds(kinds); }, []); + const handleToggleApp = useCallback((containerId: string) => { + setSelectedAppIds((prev) => { + const next = new Set(prev); + if (next.has(containerId)) { + next.delete(containerId); + } else { + next.add(containerId); + } + return next; + }); + }, []); + const handleContinueToConfig = () => { const newDrafts = new Map(drafts); for (const kind of selectedKinds) { @@ -119,9 +132,10 @@ export const InitIntegrations = () => { const finishSetupAsync = async () => { setPhase("done"); try { - if (addDockerApps && dockerApps.length > 0) { + const appsToCreate = dockerApps.filter((app) => selectedAppIds.has(app.containerId)); + if (appsToCreate.length > 0) { await createApps( - dockerApps.map((app) => ({ + appsToCreate.map((app) => ({ name: app.containerName, href: app.suggestedUrl || null, iconUrl: app.iconUrl, @@ -190,8 +204,8 @@ export const InitIntegrations = () => { diff --git a/apps/nextjs/src/components/integration/integration-grid-shared.ts b/apps/nextjs/src/components/integration/integration-grid-shared.ts index 8a4a244f8..28bfe44ee 100644 --- a/apps/nextjs/src/components/integration/integration-grid-shared.ts +++ b/apps/nextjs/src/components/integration/integration-grid-shared.ts @@ -74,6 +74,7 @@ export const buildSortedIntegrations = ( integrationKinds .filter((kind) => { if (options.onboarding && hiddenFromOnboarding.has(kind)) return false; + if (options.onboarding && widgetsByIntegration[kind].length === 0) return false; if (!options.enableMockIntegration && kind === "mock") return false; return true; }) diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index dd989a925..bf78f068b 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -125,7 +125,7 @@ "detected": "Docker detected {count} services", "detectedBreakdown": "{integrations} integrations ยท {apps} apps", "appBadge": "App", - "appSectionLabel": "Also add {count} apps to dashboard" + "appSectionLabel": "Apps to add to dashboard ({count} selected)" } }, "finish": {