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 83320184c..1756fd12d 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
@@ -14,7 +14,7 @@ import {
ThemeIcon,
UnstyledButton,
} from "@mantine/core";
-import { IconChevronDown, IconBrandDocker } from "@tabler/icons-react";
+import { IconBrandDocker, IconChevronDown } from "@tabler/icons-react";
import type { IntegrationKind } from "@homarr/definitions";
import { getIntegrationName } from "@homarr/definitions";
@@ -128,21 +128,17 @@ export const DockerDiscoveryIndicator = ({
onChange={() => onToggleAllApps(!appsEnabled)}
styles={{ label: { fontSize: "var(--mantine-font-size-xs)" } }}
/>
- {appsEnabled && apps.map((app) => (
-
-
- {app.containerName.at(0)?.toUpperCase()}
-
-
- {app.containerName}
-
-
- ))}
+ {appsEnabled &&
+ apps.map((app) => (
+
+
+ {app.containerName.at(0)?.toUpperCase()}
+
+
+ {app.containerName}
+
+
+ ))}
>
)}
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 f6302e5ab..f733b7097 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
@@ -21,15 +21,14 @@ import { useDisclosure } from "@mantine/hooks";
import { IconArrowRight, IconSettings } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
-import type { IntegrationKind } from "@homarr/definitions";
+import type { IntegrationKind, UrlTemplateMode } from "@homarr/definitions";
import { buildIntegrationUrl, getIntegrationName } from "@homarr/definitions";
-import type { UrlTemplateMode } from "@homarr/definitions";
+import { showErrorNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import { IntegrationAvatar } from "@homarr/ui";
import { NewIntegrationForm } from "~/app/[locale]/manage/integrations/new/_integration-new-form";
import { IntegrationMultiSelectGrid } from "~/components/integration/integration-multi-select-grid";
-
import { DockerDiscoveryIndicator } from "./docker-discovery-panel";
type Phase = "select" | "configure" | "done";
@@ -69,7 +68,7 @@ export const InitIntegrations = () => {
const dockerApplied = useRef(false);
useEffect(() => {
- if (!dockerData || dockerData.status !== "success" || dockerApplied.current) return;
+ if (dockerData?.status !== "success" || dockerApplied.current) return;
dockerApplied.current = true;
const newDrafts = new Map(drafts);
@@ -132,6 +131,10 @@ export const InitIntegrations = () => {
await setupIntegrations();
router.refresh();
} catch {
+ showErrorNotification({
+ title: t("configure.error.title"),
+ message: t("configure.error.message"),
+ });
setPhase("select");
}
};
@@ -192,7 +195,9 @@ export const InitIntegrations = () => {
/>
- {t("urlTemplate.label")}
+
+ {t("urlTemplate.label")}
+
{
-
+
-
+
@@ -298,7 +303,11 @@ const ConfigurePhase = ({
-
+
{t("configure.helpers.description")}
@@ -322,7 +331,9 @@ const ConfigurePhase = ({
{baseHost && (
- {t("urlTemplate.preview", { example: buildIntegrationUrl(currentKind, baseHost, urlMode, draft?.dockerPort ?? undefined) })}
+ {t("urlTemplate.preview", {
+ example: buildIntegrationUrl(currentKind, baseHost, urlMode, draft?.dockerPort ?? undefined),
+ })}
)}
diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-form.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-form.tsx
index ae4985782..2f67da11c 100644
--- a/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-form.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-form.tsx
@@ -63,17 +63,6 @@ const formSchema = integrationCreateSchema.omit({ kind: true, app: true }).and(
}),
);
-const onboardingFormSchema = z
- .object({
- name: z.string().nonempty().max(127),
- url: z.string().nonempty(),
- secrets: z.array(z.object({ kind: z.string(), value: z.string() })),
- attemptSearchEngineCreation: z.boolean(),
- hasApp: z.boolean(),
- appHref: appHrefSchema,
- appId: z.string().nullable(),
- });
-
export const NewIntegrationForm = ({
kind,
initialUrl,
@@ -93,7 +82,7 @@ export const NewIntegrationForm = ({
if (hasUrlSecret) {
url = "http://localhost";
}
- const form = useZodForm(isOnboarding ? onboardingFormSchema : formSchema, {
+ const form = useZodForm(formSchema, {
initialValues: {
name: initialName ?? getIntegrationName(kind),
url,
diff --git a/apps/nextjs/src/components/integration/integration-grid-shared.ts b/apps/nextjs/src/components/integration/integration-grid-shared.ts
index 2147f766b..8a4a244f8 100644
--- a/apps/nextjs/src/components/integration/integration-grid-shared.ts
+++ b/apps/nextjs/src/components/integration/integration-grid-shared.ts
@@ -68,7 +68,9 @@ export interface IntegrationGridItem {
widgets: WidgetKind[];
}
-export const buildSortedIntegrations = (options: { enableMockIntegration?: boolean; onboarding?: boolean } = {}): IntegrationGridItem[] =>
+export const buildSortedIntegrations = (
+ options: { enableMockIntegration?: boolean; onboarding?: boolean } = {},
+): IntegrationGridItem[] =>
integrationKinds
.filter((kind) => {
if (options.onboarding && hiddenFromOnboarding.has(kind)) return false;
diff --git a/apps/nextjs/src/components/integration/integration-multi-select-grid.tsx b/apps/nextjs/src/components/integration/integration-multi-select-grid.tsx
index 352e414cb..22d9138d7 100644
--- a/apps/nextjs/src/components/integration/integration-multi-select-grid.tsx
+++ b/apps/nextjs/src/components/integration/integration-multi-select-grid.tsx
@@ -34,18 +34,21 @@ export const IntegrationMultiSelectGrid = ({
const toggleKind = useCallback(
(kind: IntegrationKind) => {
- const next = new Set(selectedKinds);
- if (next.has(kind)) {
- next.delete(kind);
+ const current = new Set(selectedKindsArray);
+ if (current.has(kind)) {
+ current.delete(kind);
} else {
- next.add(kind);
+ current.add(kind);
}
- onSelectionChange(Array.from(next));
+ onSelectionChange(Array.from(current));
},
- [selectedKinds, onSelectionChange],
+ [selectedKindsArray, onSelectionChange],
);
- const integrations = useMemo(() => buildSortedIntegrations({ enableMockIntegration, onboarding }), [enableMockIntegration, onboarding]);
+ const integrations = useMemo(
+ () => buildSortedIntegrations({ enableMockIntegration, onboarding }),
+ [enableMockIntegration, onboarding],
+ );
const filtered = useMemo(() => filterIntegrations(integrations, search), [integrations, search]);
return (
diff --git a/packages/api/src/router/onboard/onboard-router.ts b/packages/api/src/router/onboard/onboard-router.ts
index 6ab2bf5f8..e88062542 100644
--- a/packages/api/src/router/onboard/onboard-router.ts
+++ b/packages/api/src/router/onboard/onboard-router.ts
@@ -1,11 +1,10 @@
import { TRPCError } from "@trpc/server";
-import superjson from "superjson";
import { z } from "zod/v4";
import { createId } from "@homarr/common";
import { encryptSecret } from "@homarr/common/server";
-import type { Database } from "@homarr/db";
import { eq, like, or } from "@homarr/db";
+import { placeAllWidgetsAsync } from "@homarr/db/queries";
import {
apps,
groups,
@@ -18,19 +17,15 @@ import {
onboarding,
} from "@homarr/db/schema";
import {
- defaultBookmarkApps,
- defaultWidgetConfigs,
- emptySuperJSON,
everyoneGroup,
extractContainerImageName,
getIconUrl,
- getWidgetKindsForIntegration,
integrationKinds,
integrationSecretKinds,
matchIntegrationKindFromContainer,
onboardingSteps,
} from "@homarr/definitions";
-import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
+import type { IntegrationKind } from "@homarr/definitions";
import { zodEnumFromArray } from "@homarr/validation/enums";
import { createTRPCRouter, onboardingProcedure, publicProcedure } from "../../trpc";
@@ -57,67 +52,6 @@ const buildSuggestedUrl = (ports: PortInfo[] | undefined, host: string): Suggest
return { url: "", publishedPort: null };
};
-const widgetConfigMap = new Map(defaultWidgetConfigs.map((config) => [config.kind, config]));
-
-const getWidgetConfig = (kind: WidgetKind) => widgetConfigMap.get(kind);
-
-interface PlaceWidgetContext {
- db: Database;
- boardId: string;
- sectionId: string;
- layoutId: string;
- columnCount: number;
- xOffset: number;
- yOffset: number;
- rowMaxHeight: number;
-}
-
-const placeWidgetAsync = async (
- ctx: PlaceWidgetContext,
- kind: WidgetKind,
- linkedIntegrationIds: string[],
- options?: string,
- size?: { width: number; height: number },
-) => {
- const itemWidth = size?.width ?? 2;
- const itemHeight = size?.height ?? 2;
-
- if (ctx.xOffset + itemWidth > ctx.columnCount) {
- ctx.xOffset = 0;
- ctx.yOffset += ctx.rowMaxHeight;
- ctx.rowMaxHeight = 0;
- }
-
- const itemId = createId();
- await ctx.db.insert(items).values({
- id: itemId,
- boardId: ctx.boardId,
- kind,
- options: options ?? emptySuperJSON,
- advancedOptions: emptySuperJSON,
- });
-
- await ctx.db.insert(itemLayouts).values({
- itemId,
- sectionId: ctx.sectionId,
- layoutId: ctx.layoutId,
- xOffset: ctx.xOffset,
- yOffset: ctx.yOffset,
- width: itemWidth,
- height: itemHeight,
- });
-
- for (const integrationId of linkedIntegrationIds) {
- await ctx.db.insert(integrationItems).values({
- itemId,
- integrationId,
- });
- }
-
- ctx.rowMaxHeight = Math.max(ctx.rowMaxHeight, itemHeight);
- ctx.xOffset += itemWidth;
-};
-
export const onboardRouter = createTRPCRouter({
currentStep: publicProcedure.query(async ({ ctx }) => {
return await getOnboardingOrFallbackAsync(ctx.db);
@@ -211,7 +145,14 @@ export const onboardRouter = createTRPCRouter({
}),
discoverDockerServices: onboardingProcedure.requiresStep("integrations").query(async ({ ctx }) => {
const emptyResult = {
- integrations: [] as { containerId: string; containerName: string; kind: IntegrationKind; suggestedUrl: string; publishedPort: number | null; iconUrl: string | null }[],
+ integrations: [] as {
+ containerId: string;
+ containerName: string;
+ kind: IntegrationKind;
+ suggestedUrl: string;
+ publishedPort: number | null;
+ iconUrl: string | null;
+ }[],
apps: [] as { containerId: string; containerName: string; suggestedUrl: string; iconUrl: string | null }[],
};
@@ -227,9 +168,7 @@ export const onboardRouter = createTRPCRouter({
const results = await Promise.allSettled(
dockerInstances.map(async ({ instance, host }) => {
const containerList = await instance.listContainers({ all: true });
- return containerList
- .filter((c) => !(dockerLabels.hide in c.Labels))
- .map((c) => ({ ...c, host }));
+ return containerList.filter((c) => !(dockerLabels.hide in c.Labels)).map((c) => ({ ...c, host }));
}),
);
@@ -240,9 +179,7 @@ export const onboardRouter = createTRPCRouter({
}
const likeQueries = containers.map((c) => like(icons.name, `%${extractContainerImageName(c.Image)}%`));
- const dbIcons = likeQueries.length > 0
- ? await ctx.db.query.icons.findMany({ where: or(...likeQueries) })
- : [];
+ const dbIcons = likeQueries.length > 0 ? await ctx.db.query.icons.findMany({ where: or(...likeQueries) }) : [];
const seenKinds = new Set();
const discoveredIntegrations = emptyResult.integrations;
@@ -346,68 +283,12 @@ export const onboardRouter = createTRPCRouter({
await db.delete(items).where(eq(items.boardId, board.id));
}
- const widgetCtx: PlaceWidgetContext = {
+ await placeAllWidgetsAsync(
db,
- boardId: board.id,
- sectionId: section.id,
- layoutId: layout.id,
- columnCount: layout.columnCount,
- xOffset: 0,
- yOffset: 0,
- rowMaxHeight: 0,
- };
-
- const placedWidgets = new Set();
-
- for (const integration of allIntegrations) {
- for (const widgetKind of getWidgetKindsForIntegration(integration.kind)) {
- if (placedWidgets.has(widgetKind)) continue;
- const config = getWidgetConfig(widgetKind);
- if (config?.skip) continue;
- placedWidgets.add(widgetKind);
-
- const matchingIds = allIntegrations
- .filter((row) => getWidgetKindsForIntegration(row.kind).includes(widgetKind))
- .map((row) => row.id);
-
- const options = config?.options ? superjson.stringify(config.options) : undefined;
- await placeWidgetAsync(
- widgetCtx,
- widgetKind,
- matchingIds,
- options,
- config && {
- width: config.width,
- height: config.height,
- },
- );
- }
- }
-
- for (const app of allApps) {
- const appOptions = superjson.stringify({ appId: app.id, openInNewTab: true, showTitle: true });
- await placeWidgetAsync(widgetCtx, "app", [], appOptions, { width: 1, height: 1 });
- }
-
- const bookmarkAppNames = new Set(defaultBookmarkApps.map((bookmark) => bookmark.name));
- const bookmarkAppIds = allApps.filter((app) => bookmarkAppNames.has(app.name)).map((app) => app.id);
-
- for (const config of defaultWidgetConfigs) {
- if (config.skip) continue;
- if (placedWidgets.has(config.kind)) continue;
- placedWidgets.add(config.kind);
-
- let options = config.options ? { ...config.options } : undefined;
-
- if (config.kind === "bookmarks" && bookmarkAppIds.length > 0) {
- options = { ...options, items: bookmarkAppIds };
- }
-
- await placeWidgetAsync(widgetCtx, config.kind, [], options ? superjson.stringify(options) : undefined, {
- width: config.width,
- height: config.height,
- });
- }
+ { boardId: board.id, sectionId: section.id, layoutId: layout.id, columnCount: layout.columnCount },
+ allIntegrations,
+ allApps,
+ );
}
await nextOnboardingStepAsync(db, undefined);
diff --git a/packages/db/migrations/seed.ts b/packages/db/migrations/seed.ts
index ceb269a3c..ecbb923c8 100644
--- a/packages/db/migrations/seed.ts
+++ b/packages/db/migrations/seed.ts
@@ -1,25 +1,19 @@
-import superjson from "superjson";
-
import { createId, objectKeys } from "@homarr/common";
import {
createDocumentationLink,
credentialsAdminGroup,
defaultBookmarkApps,
- defaultWidgetConfigs,
- emptySuperJSON,
everyoneGroup,
getIntegrationDefaultUrl,
getIntegrationName,
- getWidgetKindsForIntegration,
integrationDefs,
integrationKinds,
} from "@homarr/definitions";
-import type { WidgetKind } from "@homarr/definitions";
import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server-settings";
import type { Database } from "..";
import { eq } from "..";
-import { getMaxGroupPositionAsync } from "../queries";
+import { getMaxGroupPositionAsync, placeAllWidgetsAsync } from "../queries";
import {
getServerSettingByKeyAsync,
insertServerSettingByKeyAsync,
@@ -31,9 +25,7 @@ import {
groupMembers,
groupPermissions,
groups,
- integrationItems,
integrations,
- itemLayouts,
items,
layouts,
onboarding,
@@ -265,7 +257,7 @@ const seedDefaultBoardAsync = async (db: Database) => {
await db.insert(layouts).values({
id: createId(),
name: "Base",
- columnCount: 10,
+ columnCount: 12,
breakpoint: 0,
boardId,
});
@@ -351,103 +343,12 @@ const seedBoardWidgetsAsync = async (db: Database) => {
const allIntegrations = await db.query.integrations.findMany();
const allApps = await db.query.apps.findMany();
- const ctx = {
- xOffset: 0,
- yOffset: 0,
- rowMaxHeight: 0,
- columnCount: layout.columnCount,
- };
+ const count = await placeAllWidgetsAsync(
+ db,
+ { boardId: board.id, sectionId: section.id, layoutId: layout.id, columnCount: layout.columnCount },
+ allIntegrations,
+ allApps,
+ );
- const placedWidgets = new Set();
- const widgetConfigMap = new Map(defaultWidgetConfigs.map((cfg) => [cfg.kind, cfg]));
-
- const placeWidgetAsync = async (
- kind: WidgetKind,
- linkedIntegrationIds: string[],
- options?: string,
- size?: { width: number; height: number },
- ) => {
- const width = size?.width ?? 2;
- const height = size?.height ?? 2;
-
- if (ctx.xOffset + width > ctx.columnCount) {
- ctx.xOffset = 0;
- ctx.yOffset += ctx.rowMaxHeight;
- ctx.rowMaxHeight = 0;
- }
-
- const itemId = createId();
- await db.insert(items).values({
- id: itemId,
- boardId: board.id,
- kind,
- options: options ?? emptySuperJSON,
- advancedOptions: emptySuperJSON,
- });
-
- await db.insert(itemLayouts).values({
- itemId,
- sectionId: section.id,
- layoutId: layout.id,
- xOffset: ctx.xOffset,
- yOffset: ctx.yOffset,
- width,
- height,
- });
-
- for (const integrationId of linkedIntegrationIds) {
- await db.insert(integrationItems).values({ itemId, integrationId });
- }
-
- ctx.rowMaxHeight = Math.max(ctx.rowMaxHeight, height);
- ctx.xOffset += width;
- };
-
- for (const integration of allIntegrations) {
- for (const widgetKind of getWidgetKindsForIntegration(integration.kind)) {
- if (placedWidgets.has(widgetKind)) continue;
- const config = widgetConfigMap.get(widgetKind);
- if (config?.skip) continue;
- placedWidgets.add(widgetKind);
-
- const matchingIds = allIntegrations
- .filter((row) => getWidgetKindsForIntegration(row.kind).includes(widgetKind))
- .map((row) => row.id);
-
- const options = config?.options ? superjson.stringify(config.options) : undefined;
- await placeWidgetAsync(
- widgetKind,
- matchingIds,
- options,
- config && { width: config.width, height: config.height },
- );
- }
- }
-
- for (const app of allApps) {
- await placeWidgetAsync("app", [], superjson.stringify({ appId: app.id, openInNewTab: true, showTitle: true }), {
- width: 1,
- height: 1,
- });
- }
-
- const bookmarkAppNames = new Set(defaultBookmarkApps.map((bookmark) => bookmark.name));
- const bookmarkAppIds = allApps.filter((app) => bookmarkAppNames.has(app.name)).map((app) => app.id);
-
- for (const config of defaultWidgetConfigs) {
- if (config.skip || placedWidgets.has(config.kind)) continue;
- placedWidgets.add(config.kind);
-
- let options = config.options ? { ...config.options } : undefined;
- if (config.kind === "bookmarks" && bookmarkAppIds.length > 0) {
- options = { ...options, items: bookmarkAppIds };
- }
-
- await placeWidgetAsync(config.kind, [], options ? superjson.stringify(options) : undefined, {
- width: config.width,
- height: config.height,
- });
- }
-
- console.log(`Placed ${placedWidgets.size + allApps.length} widgets on board`);
+ console.log(`Placed ${count} widgets on board`);
};
diff --git a/packages/db/queries/index.ts b/packages/db/queries/index.ts
index 13e90d6eb..368d7eaa2 100644
--- a/packages/db/queries/index.ts
+++ b/packages/db/queries/index.ts
@@ -1,3 +1,4 @@
export * from "./item";
export * from "./server-setting";
export * from "./group";
+export * from "./widget-placement";
diff --git a/packages/db/queries/widget-placement.ts b/packages/db/queries/widget-placement.ts
new file mode 100644
index 000000000..1d0b529f9
--- /dev/null
+++ b/packages/db/queries/widget-placement.ts
@@ -0,0 +1,148 @@
+import superjson from "superjson";
+
+import { createId } from "@homarr/common";
+import {
+ defaultBookmarkApps,
+ defaultWidgetConfigs,
+ emptySuperJSON,
+ getWidgetKindsForIntegration,
+} from "@homarr/definitions";
+import type { WidgetKind } from "@homarr/definitions";
+
+import type { Database } from "..";
+import { integrationItems, itemLayouts, items } from "../schema";
+import type { Integration } from "../schema";
+
+const widgetConfigMap = new Map(defaultWidgetConfigs.map((config) => [config.kind, config]));
+
+interface BoardTarget {
+ boardId: string;
+ sectionId: string;
+ layoutId: string;
+ columnCount: number;
+}
+
+interface PlacementContext extends BoardTarget {
+ xOffset: number;
+ yOffset: number;
+ rowMaxHeight: number;
+}
+
+const placeWidgetAsync = async (
+ db: Database,
+ ctx: PlacementContext,
+ kind: WidgetKind,
+ linkedIntegrationIds: string[],
+ options?: string,
+ size?: { width: number; height: number },
+) => {
+ const itemWidth = size?.width ?? 2;
+ const itemHeight = size?.height ?? 2;
+
+ if (ctx.xOffset + itemWidth > ctx.columnCount) {
+ ctx.xOffset = 0;
+ ctx.yOffset += ctx.rowMaxHeight;
+ ctx.rowMaxHeight = 0;
+ }
+
+ const itemId = createId();
+ await db.insert(items).values({
+ id: itemId,
+ boardId: ctx.boardId,
+ kind,
+ options: options ?? emptySuperJSON,
+ advancedOptions: emptySuperJSON,
+ });
+
+ await db.insert(itemLayouts).values({
+ itemId,
+ sectionId: ctx.sectionId,
+ layoutId: ctx.layoutId,
+ xOffset: ctx.xOffset,
+ yOffset: ctx.yOffset,
+ width: itemWidth,
+ height: itemHeight,
+ });
+
+ for (const integrationId of linkedIntegrationIds) {
+ await db.insert(integrationItems).values({ itemId, integrationId });
+ }
+
+ ctx.rowMaxHeight = Math.max(ctx.rowMaxHeight, itemHeight);
+ ctx.xOffset += itemWidth;
+};
+
+interface App {
+ id: string;
+ name: string;
+}
+
+export const placeAllWidgetsAsync = async (
+ db: Database,
+ target: BoardTarget,
+ allIntegrations: Integration[],
+ allApps: App[],
+) => {
+ const ctx: PlacementContext = {
+ ...target,
+ xOffset: 0,
+ yOffset: 0,
+ rowMaxHeight: 0,
+ };
+
+ const placedWidgets = new Set();
+
+ for (const integration of allIntegrations) {
+ for (const widgetKind of getWidgetKindsForIntegration(integration.kind)) {
+ if (placedWidgets.has(widgetKind)) continue;
+ const config = widgetConfigMap.get(widgetKind);
+ if (config?.skip) continue;
+ placedWidgets.add(widgetKind);
+
+ const matchingIds = allIntegrations
+ .filter((row) => getWidgetKindsForIntegration(row.kind).includes(widgetKind))
+ .map((row) => row.id);
+
+ const options = config?.options ? superjson.stringify(config.options) : undefined;
+ await placeWidgetAsync(
+ db,
+ ctx,
+ widgetKind,
+ matchingIds,
+ options,
+ config && { width: config.width, height: config.height },
+ );
+ }
+ }
+
+ for (const app of allApps) {
+ await placeWidgetAsync(
+ db,
+ ctx,
+ "app",
+ [],
+ superjson.stringify({ appId: app.id, openInNewTab: true, showTitle: true }),
+ { width: 1, height: 1 },
+ );
+ }
+
+ const bookmarkAppNames = new Set(defaultBookmarkApps.map((bookmark) => bookmark.name));
+ const bookmarkAppIds = allApps.filter((app) => bookmarkAppNames.has(app.name)).map((app) => app.id);
+
+ for (const config of defaultWidgetConfigs) {
+ if (config.skip || placedWidgets.has(config.kind)) continue;
+ placedWidgets.add(config.kind);
+
+ let options = config.options ? { ...config.options } : undefined;
+ if (config.kind === "bookmarks" && bookmarkAppIds.length > 0) {
+ options = { ...options, items: bookmarkAppIds };
+ }
+
+ await placeWidgetAsync(db, ctx, config.kind, [], options ? superjson.stringify(options) : undefined, {
+ width: config.width,
+ height: config.height,
+ });
+ }
+
+ return placedWidgets.size + allApps.length;
+};
diff --git a/packages/definitions/src/docker-integration-match.ts b/packages/definitions/src/docker-integration-match.ts
index d3aebde09..c85715808 100644
--- a/packages/definitions/src/docker-integration-match.ts
+++ b/packages/definitions/src/docker-integration-match.ts
@@ -3,8 +3,7 @@ import { objectKeys } from "@homarr/common";
import { integrationDefs } from "./integration";
import type { IntegrationKind } from "./integration";
-export const extractContainerImageName = (image: string): string =>
- image.split("/").at(-1)?.split(":").at(0) ?? "";
+export const extractContainerImageName = (image: string): string => image.split("/").at(-1)?.split(":").at(0) ?? "";
const extractIconSlug = (iconUrl: string): string => {
const filename = iconUrl.split("/").pop() ?? "";
diff --git a/packages/definitions/src/integration-url-template.ts b/packages/definitions/src/integration-url-template.ts
index fe2c8abfd..20aca11b3 100644
--- a/packages/definitions/src/integration-url-template.ts
+++ b/packages/definitions/src/integration-url-template.ts
@@ -4,8 +4,7 @@ import { getIntegrationDefaultPort } from "./integration";
export type UrlTemplateMode = "hostPort" | "subdomain";
-const getSlugForKind = (kind: IntegrationKind): string =>
- integrationIconSlugs[kind] ?? kind.toLowerCase();
+const getSlugForKind = (kind: IntegrationKind): string => integrationIconSlugs[kind];
export const buildIntegrationUrl = (
kind: IntegrationKind,
diff --git a/packages/definitions/src/integration.ts b/packages/definitions/src/integration.ts
index 09d572566..30431c7d6 100644
--- a/packages/definitions/src/integration.ts
+++ b/packages/definitions/src/integration.ts
@@ -473,7 +473,7 @@ export const getIntegrationDefaultPort = (kind: IntegrationKind): number | undef
export const getIntegrationApiKeyUrl = (integrationUrl: string, kind: IntegrationKind): string | null => {
const definition = integrationDefs[kind];
- if (!("apiKeySettingsPath" in definition) || !definition.apiKeySettingsPath) return null;
+ if (!("apiKeySettingsPath" in definition)) return null;
const base = integrationUrl.replace(/\/+$/, "");
if (!base) return null;
return `${base}${definition.apiKeySettingsPath}`;
diff --git a/packages/definitions/src/test/api-key-url.spec.ts b/packages/definitions/src/test/api-key-url.spec.ts
index 4d26080e8..21b7e0fe7 100644
--- a/packages/definitions/src/test/api-key-url.spec.ts
+++ b/packages/definitions/src/test/api-key-url.spec.ts
@@ -24,8 +24,6 @@ describe("getIntegrationApiKeyUrl", () => {
});
it("strips trailing slash from base URL", () => {
- expect(getIntegrationApiKeyUrl("http://sonarr.local/", "sonarr")).toBe(
- "http://sonarr.local/settings/general",
- );
+ expect(getIntegrationApiKeyUrl("http://sonarr.local/", "sonarr")).toBe("http://sonarr.local/settings/general");
});
});
diff --git a/packages/translation/src/lang/en-gb.json b/packages/translation/src/lang/en-gb.json
index 491ce4480..ae6870d90 100644
--- a/packages/translation/src/lang/en-gb.json
+++ b/packages/translation/src/lang/en-gb.json
@@ -90,40 +90,6 @@
"title": "Settings",
"subtitle": "Configure server settings"
},
- "integrations": {
- "title": "Integrations",
- "subtitle": "Select the services you use.",
- "description": "Choose the integrations you'd like to connect. We'll set up your dashboard with matching widgets automatically.",
- "action": {
- "continue": "Continue",
- "skip": "Skip for now"
- },
- "configure": {
- "progress": "{current} of {total}",
- "skip": "Skip this integration",
- "finishing": "Setting up your dashboard...",
- "helpers": {
- "tooltip": "URL helpers",
- "description": "These helpers try to guess the URL for your services. If it doesn't work, just type the address manually."
- }
- },
- "urlTemplate": {
- "label": "Base URL",
- "placeholder": "homelab.local or 192.168.1.1",
- "hint": "Enter your base domain or IP to auto-fill integration URLs",
- "preview": "e.g. {example}",
- "mode": {
- "subdomain": "Subdomain",
- "hostPort": "Host:Port"
- }
- },
- "docker": {
- "detected": "Docker detected {count} services",
- "detectedBreakdown": "{integrations} integrations ยท {apps} apps",
- "appBadge": "App",
- "appSectionLabel": "Also add {count} apps to dashboard"
- }
- },
"finish": {
"title": "Finish set-up",
"subtitle": "Congratulations, you are ready to go!",
@@ -764,9 +730,6 @@
},
"appHref": {
"placeholder": "Custom application URL"
- },
- "apiKeySettings": {
- "label": "Get API key"
}
},
"action": {
diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json
index cecd64e8c..dd989a925 100644
--- a/packages/translation/src/lang/en.json
+++ b/packages/translation/src/lang/en.json
@@ -102,6 +102,10 @@
"progress": "{current} of {total}",
"skip": "Skip this integration",
"finishing": "Setting up your dashboard...",
+ "error": {
+ "title": "Setup failed",
+ "message": "Something went wrong while setting up your dashboard. Please try again."
+ },
"helpers": {
"tooltip": "URL helpers",
"description": "These helpers try to guess the URL for your services. If it doesn't work, just type the address manually."