feat: add onboarding with oldmarr import (#1606)

This commit is contained in:
Meier Lukas
2024-12-15 15:40:26 +01:00
committed by GitHub
parent 82ec77d2da
commit 6de74d9525
108 changed files with 6045 additions and 312 deletions

View File

@@ -0,0 +1,27 @@
import type { InferSelectModel } from "@homarr/db";
import type { apps } from "@homarr/db/schema/sqlite";
import type { OldmarrApp } from "@homarr/old-schema";
import type { OldmarrBookmarkDefinition } from "../widgets/definitions/bookmark";
export const mapOldmarrApp = (app: OldmarrApp): InferSelectModel<typeof apps> => {
return {
id: app.id,
name: app.name,
iconUrl: app.appearance.iconUrl,
description: app.behaviour.tooltipDescription ?? null,
href: app.behaviour.externalUrl || app.url,
};
};
export const mapOldmarrBookmarkApp = (
app: OldmarrBookmarkDefinition["options"]["items"][number],
): InferSelectModel<typeof apps> => {
return {
id: app.id,
name: app.name,
iconUrl: app.iconUrl,
description: null,
href: app.href,
};
};

View File

@@ -0,0 +1,27 @@
import type { InferInsertModel } from "@homarr/db";
import { createId } from "@homarr/db";
import type { boards } from "@homarr/db/schema/sqlite";
import type { prepareMultipleImports } from "../prepare/prepare-multiple";
import { mapColor } from "./map-colors";
import { mapColumnCount } from "./map-column-count";
type PreparedBoard = ReturnType<typeof prepareMultipleImports>["preparedBoards"][number];
export const mapBoard = (preparedBoard: PreparedBoard): InferInsertModel<typeof boards> => ({
id: createId(),
name: preparedBoard.name,
backgroundImageAttachment: preparedBoard.config.settings.customization.backgroundImageAttachment,
backgroundImageUrl: preparedBoard.config.settings.customization.backgroundImageUrl,
backgroundImageRepeat: preparedBoard.config.settings.customization.backgroundImageRepeat,
backgroundImageSize: preparedBoard.config.settings.customization.backgroundImageSize,
columnCount: mapColumnCount(preparedBoard.config, preparedBoard.size),
faviconImageUrl: preparedBoard.config.settings.customization.faviconUrl,
isPublic: preparedBoard.config.settings.access.allowGuests,
logoImageUrl: preparedBoard.config.settings.customization.logoImageUrl,
pageTitle: preparedBoard.config.settings.customization.pageTitle,
metaTitle: preparedBoard.config.settings.customization.metaTitle,
opacity: preparedBoard.config.settings.customization.appOpacity,
primaryColor: mapColor(preparedBoard.config.settings.customization.colors.primary, "#fa5252"),
secondaryColor: mapColor(preparedBoard.config.settings.customization.colors.secondary, "#fd7e14"),
});

View File

@@ -1,5 +1,6 @@
import type { OldmarrConfig } from "@homarr/old-schema";
import type { OldmarrImportConfiguration } from "@homarr/validation";
import type { OldmarrImportConfiguration } from "../settings";
export const mapColumnCount = (old: OldmarrConfig, screenSize: OldmarrImportConfiguration["screenSize"]) => {
switch (screenSize) {

View File

@@ -0,0 +1,60 @@
import { decryptSecretWithKey } from "@homarr/common/server";
import { createId } from "@homarr/db";
import type { IntegrationKind } from "@homarr/definitions";
import type { OldmarrIntegrationType } from "@homarr/old-schema";
import type { PreparedIntegration } from "../prepare/prepare-integrations";
export const mapIntegrationType = (type: OldmarrIntegrationType) => {
const kind = mapping[type];
if (!kind) {
throw new Error(`Integration type ${type} is not supported yet`);
}
return kind;
};
const mapping: Record<OldmarrIntegrationType, IntegrationKind | null> = {
adGuardHome: "adGuardHome",
deluge: "deluge",
homeAssistant: "homeAssistant",
jellyfin: "jellyfin",
jellyseerr: "jellyseerr",
lidarr: "lidarr",
nzbGet: "nzbGet",
openmediavault: "openmediavault",
overseerr: "overseerr",
pihole: "piHole",
prowlarr: "prowlarr",
proxmox: null,
qBittorrent: "qBittorrent",
radarr: "radarr",
readarr: "readarr",
sabnzbd: "sabNzbd",
sonarr: "sonarr",
tdarr: null,
transmission: "transmission",
plex: "plex",
};
export const mapAndDecryptIntegrations = (
preparedIntegrations: PreparedIntegration[],
encryptionToken: string | null,
) => {
if (encryptionToken === null) {
return [];
}
const key = Buffer.from(encryptionToken, "hex");
return preparedIntegrations.map(({ type, name, url, properties }) => ({
id: createId(),
name,
url,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
kind: mapIntegrationType(type!),
secrets: properties.map((property) => ({
...property,
value: property.value ? decryptSecretWithKey(property.value as `${string}.${string}`, key) : null,
})),
}));
};

View File

@@ -0,0 +1,89 @@
import SuperJSON from "superjson";
import type { InferInsertModel } from "@homarr/db";
import { createId } from "@homarr/db";
import type { items } from "@homarr/db/schema/sqlite";
import { logger } from "@homarr/log";
import type { BoardSize, OldmarrApp, OldmarrWidget } from "@homarr/old-schema";
import type { WidgetComponentProps } from "../../../widgets/src/definition";
import { mapKind } from "../widgets/definitions";
import { mapOptions } from "../widgets/options";
export const mapApp = (
app: OldmarrApp,
boardSize: BoardSize,
appsMap: Map<string, { id: string }>,
sectionMap: Map<string, { id: string }>,
): InferInsertModel<typeof items> => {
if (app.area.type === "sidebar") throw new Error("Mapping app in sidebar is not supported");
const shapeForSize = app.shape[boardSize];
if (!shapeForSize) {
throw new Error(`Failed to find a shape for appId='${app.id}' screenSize='${boardSize}'`);
}
const sectionId = sectionMap.get(app.area.properties.id)?.id;
if (!sectionId) {
throw new Error(`Failed to find section for app appId='${app.id}' sectionId='${app.area.properties.id}'`);
}
return {
id: createId(),
sectionId,
height: shapeForSize.size.height,
width: shapeForSize.size.width,
xOffset: shapeForSize.location.x,
yOffset: shapeForSize.location.y,
kind: "app",
options: SuperJSON.stringify({
// it's safe to assume that the app exists in the map
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain
appId: appsMap.get(app.id)?.id!,
openInNewTab: app.behaviour.isOpeningNewTab,
pingEnabled: app.network.enabledStatusChecker,
showDescriptionTooltip: app.behaviour.tooltipDescription !== "",
showTitle: app.appearance.appNameStatus === "normal",
} satisfies WidgetComponentProps<"app">["options"]),
};
};
export const mapWidget = (
widget: OldmarrWidget,
boardSize: BoardSize,
appsMap: Map<string, { id: string }>,
sectionMap: Map<string, { id: string }>,
): InferInsertModel<typeof items> | null => {
if (widget.area.type === "sidebar") throw new Error("Mapping widget in sidebar is not supported");
const shapeForSize = widget.shape[boardSize];
if (!shapeForSize) {
throw new Error(`Failed to find a shape for widgetId='${widget.id}' screenSize='${boardSize}'`);
}
const kind = mapKind(widget.type);
if (!kind) {
logger.warn(`Failed to map widget type='${widget.type}'. It's no longer supported`);
return null;
}
const sectionId = sectionMap.get(widget.area.properties.id)?.id;
if (!sectionId) {
throw new Error(
`Failed to find section for widget widgetId='${widget.id}' sectionId='${widget.area.properties.id}'`,
);
}
return {
id: createId(),
sectionId,
height: shapeForSize.size.height,
width: shapeForSize.size.width,
xOffset: shapeForSize.location.x,
yOffset: shapeForSize.location.y,
kind,
options: SuperJSON.stringify(
mapOptions(kind, widget.properties, new Map([...appsMap.entries()].map(([key, value]) => [key, value.id]))),
),
};
};

View File

@@ -0,0 +1,24 @@
import type { InferInsertModel } from "@homarr/db";
import { createId } from "@homarr/db";
import type { sections } from "@homarr/db/schema/sqlite";
import type { OldmarrCategorySection, OldmarrEmptySection } from "@homarr/old-schema";
export const mapCategorySection = (
boardId: string,
category: OldmarrCategorySection,
): InferInsertModel<typeof sections> => ({
id: createId(),
boardId,
kind: "category",
xOffset: 0,
yOffset: category.position,
name: category.name,
});
export const mapEmptySection = (boardId: string, wrapper: OldmarrEmptySection): InferInsertModel<typeof sections> => ({
id: createId(),
boardId,
kind: "empty",
xOffset: 0,
yOffset: wrapper.position,
});

View File

@@ -0,0 +1,35 @@
import { decryptSecretWithKey } from "@homarr/common/server";
import type { InferInsertModel } from "@homarr/db";
import { createId } from "@homarr/db";
import type { users } from "@homarr/db/schema/sqlite";
import type { OldmarrImportUser } from "../user-schema";
export const mapAndDecryptUsers = (importUsers: OldmarrImportUser[], encryptionToken: string | null) => {
if (encryptionToken === null) {
return [];
}
const key = Buffer.from(encryptionToken, "hex");
return importUsers.map(
({
id,
password,
salt,
settings,
...user
}): InferInsertModel<typeof users> & { oldId: string; isAdmin: boolean } => ({
...user,
oldId: id,
id: createId(),
colorScheme: settings?.colorScheme === "environment" ? undefined : settings?.colorScheme,
firstDayOfWeek: settings?.firstDayOfWeek === "sunday" ? 0 : settings?.firstDayOfWeek === "monday" ? 1 : 6,
provider: "credentials",
pingIconsEnabled: settings?.replacePingWithIcons,
isAdmin: user.isAdmin || user.isOwner,
password: decryptSecretWithKey(password, key),
salt: decryptSecretWithKey(salt, key),
}),
);
};