feat(onboarding): enhance integration setup with Docker discovery and URL templating (#5607)
This commit is contained in:
1 parent
0a6467979a
commit
dd7a4908a3
68 files changed
+2922
-434
No files matched your search
@@ -4,6 +4,7 @@ import { createId, objectKeys } from "@homarr/common";
|
||||
import {
|
||||
createDocumentationLink,
|
||||
credentialsAdminGroup,
|
||||
defaultBookmarkApps,
|
||||
everyoneGroup,
|
||||
getIntegrationDefaultUrl,
|
||||
getIntegrationName,
|
||||
@@ -15,7 +16,7 @@ import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server
|
||||
|
||||
import type { Database } from "..";
|
||||
import { eq } from "..";
|
||||
import { getMaxGroupPositionAsync } from "../queries";
|
||||
import { getMaxGroupPositionAsync, placeAllWidgetsAsync } from "../queries";
|
||||
import {
|
||||
getServerSettingByKeyAsync,
|
||||
insertServerSettingByKeyAsync,
|
||||
@@ -46,6 +47,9 @@ export const seedDataAsync = async (db: Database) => {
|
||||
await seedServerSettingsAsync(db);
|
||||
await seedDefaultSearchEnginesAsync(db);
|
||||
await seedDefaultIntegrationsAsync(db);
|
||||
await seedDefaultAppsAsync(db);
|
||||
await seedDefaultBoardAsync(db);
|
||||
await seedBoardWidgetsAsync(db);
|
||||
|
||||
if (["1", "yes", "t", "true"].includes((process.env.DEMO_MODE ?? "").toLowerCase())) {
|
||||
await seedDemoUserAsync(db);
|
||||
@@ -217,6 +221,64 @@ const seedDefaultIntegrationsAsync = async (db: Database) => {
|
||||
console.log(`Created ${createdCount} default integrations through seeding process`);
|
||||
};
|
||||
|
||||
const seedDefaultAppsAsync = async (db: Database) => {
|
||||
const existingApps = await db.$count(apps);
|
||||
if (existingApps > 0) {
|
||||
console.log("Skipping seeding of default apps as some already exist");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const app of defaultBookmarkApps) {
|
||||
await db.insert(apps).values({
|
||||
id: createId(),
|
||||
name: app.name,
|
||||
iconUrl: app.iconUrl,
|
||||
href: app.href,
|
||||
});
|
||||
}
|
||||
console.log(`Created ${defaultBookmarkApps.length} default apps through seeding process`);
|
||||
};
|
||||
|
||||
const seedDefaultBoardAsync = async (db: Database) => {
|
||||
const existingBoard = await db.query.boards.findFirst();
|
||||
|
||||
if (existingBoard) {
|
||||
console.log("Skipping seeding of default board as one already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
const boardId = createId();
|
||||
await db.insert(boards).values({
|
||||
id: boardId,
|
||||
name: "dashboard",
|
||||
isPublic: false,
|
||||
});
|
||||
await db.insert(sections).values({
|
||||
id: createId(),
|
||||
kind: "empty",
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
boardId,
|
||||
});
|
||||
await db.insert(layouts).values({
|
||||
id: createId(),
|
||||
name: "Base",
|
||||
columnCount: 10,
|
||||
breakpoint: 0,
|
||||
boardId,
|
||||
});
|
||||
|
||||
const everyoneGroupRow = await db.query.groups.findFirst({
|
||||
where: eq(groups.name, everyoneGroup),
|
||||
});
|
||||
if (everyoneGroupRow) {
|
||||
await db.update(groups).set({ homeBoardId: boardId }).where(eq(groups.id, everyoneGroupRow.id));
|
||||
console.log("Set default board as home board for everyone group");
|
||||
}
|
||||
|
||||
console.log("Created default board 'dashboard' through seed");
|
||||
};
|
||||
|
||||
interface DemoWidget {
|
||||
kind: WidgetKind;
|
||||
width: number;
|
||||
@@ -475,3 +537,32 @@ const seedDemoUserAsync = async (db: Database) => {
|
||||
"Demo mode enabled: created demo user, mock integration, and sample board with widgets. Disable by setting DEMO_MODE=false.",
|
||||
);
|
||||
};
|
||||
|
||||
const seedBoardWidgetsAsync = async (db: Database) => {
|
||||
const existingItems = await db.$count(items);
|
||||
if (existingItems > 0) {
|
||||
console.log("Skipping seeding of board widgets as some already exist");
|
||||
return;
|
||||
}
|
||||
|
||||
const board = await db.query.boards.findFirst({
|
||||
with: { sections: true, layouts: true },
|
||||
});
|
||||
if (!board) return;
|
||||
|
||||
const section = board.sections.find((sec) => sec.kind === "empty");
|
||||
const layout = board.layouts[0];
|
||||
if (!section || !layout) return;
|
||||
|
||||
const allIntegrations = await db.query.integrations.findMany();
|
||||
const allApps = await db.query.apps.findMany();
|
||||
|
||||
const count = await placeAllWidgetsAsync(
|
||||
db,
|
||||
{ boardId: board.id, sectionId: section.id, layoutId: layout.id, columnCount: layout.columnCount },
|
||||
allIntegrations,
|
||||
allApps,
|
||||
);
|
||||
|
||||
console.log(`Placed ${count} widgets on board`);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./item";
|
||||
export * from "./server-setting";
|
||||
export * from "./group";
|
||||
export * from "./widget-placement";
|
||||
@@ -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<WidgetKind>();
|
||||
|
||||
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;
|
||||
};
|
||||
Reference in new issue
Block a user