Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f18401c72e | ||
|
|
dade477b32 | ||
|
|
79f88dc3b8 | ||
|
|
40e53976e5 | ||
|
|
211fd648be | ||
|
|
26702fe078 |
No files matched your search
@@ -10,6 +10,7 @@ README.md
|
|||||||
dev
|
dev
|
||||||
.build
|
.build
|
||||||
e2e
|
e2e
|
||||||
|
apps/nextjs/.next
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
@@ -1,70 +1,27 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useRef } from "react";
|
import { Box, Stack } from "@mantine/core";
|
||||||
import { Box, LoadingOverlay, Stack } from "@mantine/core";
|
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
|
||||||
import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
|
import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
|
||||||
|
|
||||||
import { BoardCategorySection } from "~/components/board/sections/category-section";
|
import { BoardCategorySection } from "~/components/board/sections/category-section";
|
||||||
import { BoardEmptySection } from "~/components/board/sections/empty-section";
|
import { BoardEmptySection } from "~/components/board/sections/empty-section";
|
||||||
import { BoardBackgroundVideo } from "~/components/layout/background";
|
import { BoardBackgroundVideo } from "~/components/layout/background";
|
||||||
import { fullHeightWithoutHeaderAndFooter } from "~/constants";
|
|
||||||
import { useIsBoardReady } from "./_ready-context";
|
|
||||||
|
|
||||||
let boardName: string | null = null;
|
|
||||||
|
|
||||||
export const updateBoardName = (name: string | null) => {
|
|
||||||
boardName = name;
|
|
||||||
};
|
|
||||||
|
|
||||||
type UpdateCallback = (prev: RouterOutputs["board"]["getHomeBoard"]) => RouterOutputs["board"]["getHomeBoard"];
|
|
||||||
|
|
||||||
export const useUpdateBoard = () => {
|
|
||||||
const utils = clientApi.useUtils();
|
|
||||||
|
|
||||||
const updateBoard = useCallback(
|
|
||||||
(updaterWithoutUndefined: UpdateCallback) => {
|
|
||||||
if (!boardName) {
|
|
||||||
throw new Error("Board name is not set");
|
|
||||||
}
|
|
||||||
utils.board.getBoardByName.setData({ name: boardName }, (previous) =>
|
|
||||||
previous ? updaterWithoutUndefined(previous) : previous,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[utils],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
updateBoard,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ClientBoard = () => {
|
export const ClientBoard = () => {
|
||||||
const board = useRequiredBoard();
|
const board = useRequiredBoard();
|
||||||
const currentLayoutId = useCurrentLayout();
|
const currentLayoutId = useCurrentLayout();
|
||||||
const isReady = useIsBoardReady();
|
|
||||||
|
|
||||||
const fullWidthSortedSections = board.sections
|
const fullWidthSortedSections = board.sections
|
||||||
.filter((section) => section.kind === "empty" || section.kind === "category")
|
.filter((section) => section.kind === "empty" || section.kind === "category")
|
||||||
.sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset);
|
.sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset);
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box h="100%" pos="relative">
|
<Box h="100%" pos="relative">
|
||||||
<BoardBackgroundVideo />
|
<BoardBackgroundVideo />
|
||||||
<LoadingOverlay
|
<Stack h="100%">
|
||||||
visible={!isReady}
|
|
||||||
transitionProps={{ duration: 500 }}
|
|
||||||
loaderProps={{ size: "lg" }}
|
|
||||||
h={fullHeightWithoutHeaderAndFooter}
|
|
||||||
/>
|
|
||||||
<Stack ref={ref} h="100%" style={{ visibility: isReady ? "visible" : "hidden" }}>
|
|
||||||
{fullWidthSortedSections.map((section) =>
|
{fullWidthSortedSections.map((section) =>
|
||||||
section.kind === "empty" ? (
|
section.kind === "empty" ? (
|
||||||
// Unique keys per layout to always reinitialize the gridstack
|
|
||||||
<BoardEmptySection key={`${currentLayoutId}-${section.id}`} section={section} />
|
<BoardEmptySection key={`${currentLayoutId}-${section.id}`} section={section} />
|
||||||
) : (
|
) : (
|
||||||
<BoardCategorySection key={`${currentLayoutId}-${section.id}`} section={section} />
|
<BoardCategorySection key={`${currentLayoutId}-${section.id}`} section={section} />
|
||||||
|
|||||||
@@ -1,42 +1,19 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
import { createContext, useCallback, useContext, useEffect, useState } from "react";
|
import { createContext, useCallback, useContext } from "react";
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
|
||||||
import { useRequiredBoard } from "@homarr/boards/context";
|
|
||||||
|
|
||||||
const BoardReadyContext = createContext<{
|
const BoardReadyContext = createContext<{
|
||||||
isReady: boolean;
|
|
||||||
markAsReady: (id: string) => void;
|
markAsReady: (id: string) => void;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
export const BoardReadyProvider = ({ children }: PropsWithChildren) => {
|
export const BoardReadyProvider = ({ children }: PropsWithChildren) => {
|
||||||
const pathname = usePathname();
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
const utils = clientApi.useUtils();
|
const markAsReady = useCallback((_id: string) => {}, []);
|
||||||
const board = useRequiredBoard();
|
|
||||||
const [readySections, setReadySections] = useState<string[]>([]);
|
|
||||||
|
|
||||||
// Reset sections required for ready state
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
setReadySections([]);
|
|
||||||
};
|
|
||||||
}, [pathname, utils]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setReadySections((previous) => previous.filter((id) => board.sections.some((section) => section.id === id)));
|
|
||||||
}, [board.sections.length, setReadySections]);
|
|
||||||
|
|
||||||
const markAsReady = useCallback((id: string) => {
|
|
||||||
setReadySections((previous) => (previous.includes(id) ? previous : [...previous, id]));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BoardReadyContext.Provider
|
<BoardReadyContext.Provider
|
||||||
value={{
|
value={{
|
||||||
isReady: board.sections.length === readySections.length,
|
|
||||||
markAsReady,
|
markAsReady,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -54,13 +31,3 @@ export const useMarkSectionAsReady = () => {
|
|||||||
|
|
||||||
return context.markAsReady;
|
return context.markAsReady;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useIsBoardReady = () => {
|
|
||||||
const context = useContext(BoardReadyContext);
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("BoardReadyProvider is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.isReady;
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createId, objectKeys } from "@homarr/common";
|
import { createId, objectKeys } from "@homarr/common";
|
||||||
import {
|
import {
|
||||||
createDocumentationLink,
|
createDocumentationLink,
|
||||||
|
credentialsAdminGroup,
|
||||||
everyoneGroup,
|
everyoneGroup,
|
||||||
getIntegrationDefaultUrl,
|
getIntegrationDefaultUrl,
|
||||||
getIntegrationName,
|
getIntegrationName,
|
||||||
@@ -11,12 +12,24 @@ import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server
|
|||||||
|
|
||||||
import type { Database } from "..";
|
import type { Database } from "..";
|
||||||
import { eq } from "..";
|
import { eq } from "..";
|
||||||
|
import { getMaxGroupPositionAsync } from "../queries";
|
||||||
import {
|
import {
|
||||||
getServerSettingByKeyAsync,
|
getServerSettingByKeyAsync,
|
||||||
insertServerSettingByKeyAsync,
|
insertServerSettingByKeyAsync,
|
||||||
updateServerSettingByKeyAsync,
|
updateServerSettingByKeyAsync,
|
||||||
} from "../queries/server-setting";
|
} from "../queries/server-setting";
|
||||||
import { groups, integrations, onboarding, searchEngines } from "../schema";
|
import {
|
||||||
|
boards,
|
||||||
|
groupMembers,
|
||||||
|
groupPermissions,
|
||||||
|
groups,
|
||||||
|
integrations,
|
||||||
|
layouts,
|
||||||
|
onboarding,
|
||||||
|
searchEngines,
|
||||||
|
sections,
|
||||||
|
users,
|
||||||
|
} from "../schema";
|
||||||
import type { Integration } from "../schema";
|
import type { Integration } from "../schema";
|
||||||
|
|
||||||
export const seedDataAsync = async (db: Database) => {
|
export const seedDataAsync = async (db: Database) => {
|
||||||
@@ -25,6 +38,10 @@ export const seedDataAsync = async (db: Database) => {
|
|||||||
await seedServerSettingsAsync(db);
|
await seedServerSettingsAsync(db);
|
||||||
await seedDefaultSearchEnginesAsync(db);
|
await seedDefaultSearchEnginesAsync(db);
|
||||||
await seedDefaultIntegrationsAsync(db);
|
await seedDefaultIntegrationsAsync(db);
|
||||||
|
|
||||||
|
if (process.env.DEMO_MODE === "true") {
|
||||||
|
await seedDemoUserAsync(db);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const seedEveryoneGroupAsync = async (db: Database) => {
|
const seedEveryoneGroupAsync = async (db: Database) => {
|
||||||
@@ -191,3 +208,72 @@ const seedDefaultIntegrationsAsync = async (db: Database) => {
|
|||||||
|
|
||||||
console.log(`Created ${createdCount} default integrations through seeding process`);
|
console.log(`Created ${createdCount} default integrations through seeding process`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const seedDemoUserAsync = async (db: Database) => {
|
||||||
|
const existingUser = await db.query.users.findFirst({
|
||||||
|
where: eq(users.name, "demo"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
console.log("Skipping seeding of demo user as it already exists");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = createId();
|
||||||
|
|
||||||
|
await db.insert(users).values({
|
||||||
|
id: userId,
|
||||||
|
name: "demo",
|
||||||
|
email: "demo@example.com",
|
||||||
|
password: "$2b$10$c5AaQb0oj32/bui5Z55PyOb7pY.iEkXYqgdD8MjS9LjSjLMeOkZu2",
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxPosition = await getMaxGroupPositionAsync(db);
|
||||||
|
const groupId = createId();
|
||||||
|
await db.insert(groups).values({
|
||||||
|
id: groupId,
|
||||||
|
name: credentialsAdminGroup,
|
||||||
|
ownerId: userId,
|
||||||
|
position: maxPosition + 1,
|
||||||
|
});
|
||||||
|
await db.insert(groupPermissions).values({
|
||||||
|
groupId,
|
||||||
|
permission: "admin",
|
||||||
|
});
|
||||||
|
await db.insert(groupMembers).values({
|
||||||
|
groupId,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.update(onboarding).set({
|
||||||
|
step: "finish",
|
||||||
|
previousStep: "settings",
|
||||||
|
});
|
||||||
|
|
||||||
|
const boardId = createId();
|
||||||
|
await db.insert(boards).values({
|
||||||
|
id: boardId,
|
||||||
|
name: "default",
|
||||||
|
isPublic: false,
|
||||||
|
creatorId: userId,
|
||||||
|
});
|
||||||
|
await db.insert(sections).values({
|
||||||
|
id: createId(),
|
||||||
|
kind: "empty",
|
||||||
|
xOffset: 0,
|
||||||
|
yOffset: 0,
|
||||||
|
boardId,
|
||||||
|
});
|
||||||
|
await db.insert(layouts).values({
|
||||||
|
id: createId(),
|
||||||
|
name: "Base",
|
||||||
|
columnCount: 12,
|
||||||
|
breakpoint: 0,
|
||||||
|
boardId,
|
||||||
|
});
|
||||||
|
await db.update(users).set({ homeBoardId: boardId }).where(eq(users.id, userId));
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"Your instance has enabled demo mode. Homarr therefore created example data for your demo. You can disable demo mode anytime by setting the env 'DEMO_MODE' to 'false' or unset it.",
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in new issue
Block a user