diff --git a/.env.example b/.env.example
index 37c6640d7..444562c2d 100644
--- a/.env.example
+++ b/.env.example
@@ -11,7 +11,7 @@
# Prisma
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
-DATABASE_URL="file:./db.sqlite"
+DATABASE_URL="file:../database/db.sqlite"
# Next Auth
# You can generate a new secret on the command line with:
diff --git a/.gitignore b/.gitignore
index 4f96eb93c..9472a3696 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,4 +58,5 @@ public/locales/*
!public/locales/en
#database
-prisma/db.sqlite
\ No newline at end of file
+prisma/db.sqlite
+database/*.sqlite
\ No newline at end of file
diff --git a/package.json b/package.json
index 4db04bb10..089a5013b 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
"@mantine/modals": "^6.0.0",
"@mantine/next": "^6.0.0",
"@mantine/notifications": "^6.0.0",
+ "@mantine/prism": "^6.0.19",
"@mantine/tiptap": "^6.0.17",
"@next-auth/prisma-adapter": "^1.0.5",
"@nivo/core": "^0.83.0",
diff --git a/public/imgs/app-icons/truenas.svg b/public/imgs/app-icons/truenas.svg
new file mode 100644
index 000000000..c3d96ff70
--- /dev/null
+++ b/public/imgs/app-icons/truenas.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/imgs/app-icons/unraid-alt.svg b/public/imgs/app-icons/unraid-alt.svg
new file mode 100644
index 000000000..7d695dadc
--- /dev/null
+++ b/public/imgs/app-icons/unraid-alt.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/Onboarding/common-wrapper.tsx b/src/components/Onboarding/common-wrapper.tsx
new file mode 100644
index 000000000..09df0005f
--- /dev/null
+++ b/src/components/Onboarding/common-wrapper.tsx
@@ -0,0 +1,10 @@
+import { Card } from '@mantine/core';
+import { ReactNode } from 'react';
+
+export const OnboardingStepWrapper = ({ children }: { children: ReactNode }) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/components/Onboarding/onboarding-steps.tsx b/src/components/Onboarding/onboarding-steps.tsx
new file mode 100644
index 000000000..036f4fb31
--- /dev/null
+++ b/src/components/Onboarding/onboarding-steps.tsx
@@ -0,0 +1,46 @@
+import { Stack, Stepper } from '@mantine/core';
+import { useState } from 'react';
+
+import { StepCreateAccount } from './step-create-account';
+import { StepDockerImport } from './step-docker-import';
+import { StepDocumentation } from './step-documentation';
+import { StepOnboardingFinished } from './step-onboarding-finished';
+import { StepUpdatePathMappings } from './step-update-path-mappings';
+
+export const OnboardingSteps = ({ isUpdate }: { isUpdate: boolean }) => {
+ const [currentStep, setCurrentStep] = useState(0);
+ const nextStep = () => setCurrentStep((current) => (current < 4 ? current + 1 : current));
+ const prevStep = () => setCurrentStep((current) => (current > 0 ? current - 1 : current));
+
+ return (
+
+
+ {isUpdate && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/Onboarding/step-create-account.tsx b/src/components/Onboarding/step-create-account.tsx
new file mode 100644
index 000000000..de7a8b3d8
--- /dev/null
+++ b/src/components/Onboarding/step-create-account.tsx
@@ -0,0 +1,98 @@
+import { Button, Group, PasswordInput, Stack, TextInput, Title } from '@mantine/core';
+import { useForm } from '@mantine/form';
+import { IconArrowLeft, IconArrowRight } from '@tabler/icons-react';
+import { signIn } from 'next-auth/react';
+import { useState } from 'react';
+import { z } from 'zod';
+import { api } from '~/utils/api';
+import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
+import { signUpFormSchema } from '~/validations/user';
+
+import { OnboardingStepWrapper } from './common-wrapper';
+
+export const StepCreateAccount = ({
+ previous,
+ next,
+}: {
+ previous: () => void;
+ next: () => void;
+}) => {
+ const [isSigninIn, setIsSigninIn] = useState(false);
+ const { mutateAsync } = api.user.createOwnerAccount.useMutation();
+ const { i18nZodResolver } = useI18nZodResolver();
+
+ const form = useForm>({
+ validate: i18nZodResolver(signUpFormSchema),
+ validateInputOnBlur: true,
+ });
+ const handleSubmit = (values: z.infer) => {
+ setIsSigninIn(true);
+ void mutateAsync(values, {
+ onSuccess: () => {
+ signIn('credentials', {
+ redirect: false,
+ name: values.username,
+ password: values.password,
+ callbackUrl: '/',
+ }).then((response) => {
+ if (!response?.ok) {
+ setIsSigninIn(false);
+ return;
+ }
+ next();
+ });
+ },
+ });
+ };
+
+ return (
+
+
+ Create your administrator account
+
+
+
+ );
+};
diff --git a/src/components/Onboarding/step-docker-import.tsx b/src/components/Onboarding/step-docker-import.tsx
new file mode 100644
index 000000000..5d65e7466
--- /dev/null
+++ b/src/components/Onboarding/step-docker-import.tsx
@@ -0,0 +1,20 @@
+import { Button, Stack, Title } from '@mantine/core';
+import { IconArrowRight } from '@tabler/icons-react';
+
+import { OnboardingStepWrapper } from './common-wrapper';
+
+export const StepDockerImport = ({ next }: { next: () => void }) => {
+ return (
+
+
+ Automatic container import
+
+
+
+ } fullWidth>
+ Next
+
+
+
+ );
+};
diff --git a/src/components/Onboarding/step-documentation.tsx b/src/components/Onboarding/step-documentation.tsx
new file mode 100644
index 000000000..6b2a32dd2
--- /dev/null
+++ b/src/components/Onboarding/step-documentation.tsx
@@ -0,0 +1,31 @@
+import { Button, Divider, Stack, Text, Title } from '@mantine/core';
+import { IconArrowRight, IconExternalLink } from '@tabler/icons-react';
+
+import { OnboardingStepWrapper } from './common-wrapper';
+
+export const StepDocumentation = ({ next }: { next: () => void }) => {
+ return (
+
+
+ Documentation
+
+
+
+ We highly encourage you to read the documentation, before you continue.
+ }
+ variant="default"
+ >
+ Open documentation
+
+
+ } fullWidth>
+ Finish
+
+
+
+ );
+};
diff --git a/src/components/Onboarding/step-onboarding-finished.tsx b/src/components/Onboarding/step-onboarding-finished.tsx
new file mode 100644
index 000000000..22f260459
--- /dev/null
+++ b/src/components/Onboarding/step-onboarding-finished.tsx
@@ -0,0 +1,58 @@
+import { NavLink, Stack, Text, Title, createStyles } from '@mantine/core';
+import {
+ IconChevronRight,
+ IconDashboard,
+ IconFileText,
+ IconManualGearbox,
+} from '@tabler/icons-react';
+import Image from 'next/image';
+
+import { OnboardingStepWrapper } from './common-wrapper';
+
+export const StepOnboardingFinished = () => {
+ const { classes } = useStyles();
+ return (
+
+
+
+
+ Congratulations, you've set Homarr up!
+
+ Awesome! What do you want to do next?
+
+
+ }
+ className={classes.link}
+ icon={ }
+ label="Go to your board"
+ variant="light"
+ active
+ />
+ }
+ className={classes.link}
+ icon={ }
+ label="Go to the management dashboard"
+ variant="light"
+ active
+ />
+ }
+ className={classes.link}
+ icon={ }
+ label="Check out the documentation"
+ variant="light"
+ active
+ />
+
+
+
+ );
+};
+
+const useStyles = createStyles((theme) => ({
+ link: {
+ borderRadius: '0.4rem',
+ },
+}));
diff --git a/src/components/Onboarding/step-update-path-mappings.tsx b/src/components/Onboarding/step-update-path-mappings.tsx
new file mode 100644
index 000000000..02c32762b
--- /dev/null
+++ b/src/components/Onboarding/step-update-path-mappings.tsx
@@ -0,0 +1,207 @@
+import { Box, Button, Code, Group, List, Space, Tabs, TabsValue, Text, Title } from '@mantine/core';
+import { Prism } from '@mantine/prism';
+import {
+ IconArrowRight,
+ IconBrandDebian,
+ IconBrandDocker,
+ IconInfoSquareRounded,
+} from '@tabler/icons-react';
+import Image from 'next/image';
+import { useState } from 'react';
+
+import { OnboardingStepWrapper } from './common-wrapper';
+
+const dockerRunCommand = `docker run \\
+--name homarr \\
+--restart unless-stopped \\
+-p 7575:7575 \\
+-v your-path/homarr/configs:/app/data/configs \\
+-v your-path/homarr/data:/app/database \\
+-v your-path/homarr/icons:/app/public/icons \\
+-d ghcr.io/ajnart/homarr:latest`;
+
+const dockerComposeCommand = `version: '3'
+#---------------------------------------------------------------------#
+# Homarr - A simple, yet powerful dashboard for your server. #
+#---------------------------------------------------------------------#
+services:
+ homarr:
+ container_name: homarr
+ image: ghcr.io/ajnart/homarr:latest
+ restart: unless-stopped
+ volumes:
+ - ./homarr/configs:/app/data/configs
+ - ./homarr/data:/app/database
+ - ./homarr/icons:/app/public/icons
+ ports:
+ - '7575:7575'`;
+
+const added = { color: 'green', label: '+' };
+
+export const StepUpdatePathMappings = ({ next }: { next: () => void }) => {
+ const [selectedTab, setSelectedTab] = useState("standard_docker");
+ return (
+
+
+ Update path mappings
+
+
+ Homarr has updated the location of the saved data. We detected, that your instance might
+ need an update to function as expected. It is recommended, that you take a backup of your
+ .json configuration file on the file system and copy it, in case something goes wrong.
+
+
+
+ setSelectedTab(tab)} mt="xs">
+
+ }>
+ Docker
+
+ }>
+ Docker Compose
+
+ }>
+ Standalone Linux / Windows
+
+ }
+ >
+ Unraid
+
+ }>
+ Others
+
+
+
+
+
+
+
+ Back up your configuration . In case you didn't mount your configuration
+ correctly, you could risk loosing your dashboard. To back up,
+ go on your file system and copy the directory, containing your
+ default.json to your local machine.
+
+
+
+
+ Before you continue, check that you still have the command, that you set up Homarr
+ with. Otherwise, your configuration might not be loaded correctly or icons are
+ missing.
+
+
+
+
+ Run docker rm homarr, where homarr indicates the name of
+ your container
+
+
+
+
+ Run docker run ... again, that you used to create the Homarr container.
+ Note, that you need to add a new line:
+
+
+ {dockerRunCommand}
+
+
+ Refresh this page and click on "continue"
+
+
+
+
+
+
+
+ Back up your configuration . In case you didn't mount your configuration
+ correctly, you could risk loosing your dashboard. To back up,
+ go on your file system and copy the directory, containing your
+ default.json to your local machine.
+
+
+
+
+ Navigate to the directory, where the docker-compose.yml for Homarr is
+ located.
+
+
+
+
+ Run docker compose down
+
+
+
+
+ Edit docker-compose.yml using text editor. Use Notepad or VSC on GUI
+ based systems. Use nano or vim on terminal systems.
+
+
+ {dockerComposeCommand}
+
+
+ Run docker compose up.
+ Refresh this page and click on "continue"
+
+
+
+
+
+ You're lucky. For installation without Docker on Windows and Linux, there are no
+ additional steps required. However, be advised that your backups should start to include
+ the files located at /database too, if you run automatic backups.
+
+
+
+
+
+ Click on your Homarr application and click "Edit"
+
+ Scroll down and click on the link "Add another path, port, variable or device"
+
+
+ After the new modal has opened, make sure that "Path" has been selected at the top
+
+
+ In the container path, enter /app/database
+
+
+ In the host path, enter a new path on your host system. Choose a similar path, but the
+ innermost directory should be different, than your existing mounting points (eg.{' '}
+ /mnt/user/appdata/homarr/data)
+
+ Click "Apply" and wait for the container to be restarted.
+ Refresh this page and click on "continue"
+
+
+
+
+
+ We are sadly not able to include upgrade guides for all kind of systems. If your system
+ was not listed, you should mount this new mounting point in your container:
+
+ /app/database
+
+
+
+ {selectedTab ? (
+
+ }
+ color="green"
+ fullWidth
+ >
+ Continue
+
+
+ ) : (
+
+
+ Please select your installation method
+
+
+ )}
+
+ );
+};
diff --git a/src/pages/onboard.tsx b/src/pages/onboard.tsx
index aa776bc2e..c93ceb93b 100644
--- a/src/pages/onboard.tsx
+++ b/src/pages/onboard.tsx
@@ -1,48 +1,24 @@
-import {
- Box,
- Button,
- Card,
- Center,
- Flex,
- Grid,
- Group,
- Image,
- PasswordInput,
- Stack,
- Text,
- TextInput,
- Title,
- UnstyledButton,
- createStyles,
- useMantineTheme,
-} from '@mantine/core';
-import { useForm } from '@mantine/form';
-import { useMediaQuery } from '@mantine/hooks';
-import { IconLayoutDashboard, IconUserCog } from '@tabler/icons-react';
-import { IconArrowRight, IconBook2, IconUserPlus } from '@tabler/icons-react';
-import { GetServerSideProps } from 'next';
-import { signIn } from 'next-auth/react';
+import { Box, Button, Center, Image, Stack, Text, Title, useMantineTheme } from '@mantine/core';
+import { useDisclosure } from '@mantine/hooks';
+import { IconArrowRight } from '@tabler/icons-react';
+import fs from 'fs';
+import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
import Head from 'next/head';
-import Link from 'next/link';
-import { ReactNode, useMemo, useState } from 'react';
-import { z } from 'zod';
+import { OnboardingSteps } from '~/components/Onboarding/onboarding-steps';
import { prisma } from '~/server/db';
+import { getConfig } from '~/tools/config/getConfig';
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
-import { api } from '~/utils/api';
-import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
-import { signUpFormSchema } from '~/validations/user';
-const getStepContents = () => [FirstStepContent, SecondStepContent, ThirdStepContent] as const;
-
-export default function OnboardPage() {
- const { fn, colors, breakpoints, colorScheme } = useMantineTheme();
- const [currentStep, setStep] = useState(0);
- const next = () => setStep((prev) => prev + 1);
- const isSmallerThanMd = useMediaQuery(`(max-width: ${breakpoints.sm})`);
- const stepContents = useMemo(() => getStepContents(), []);
- const CurrentStepComponent = useMemo(() => stepContents[currentStep], [currentStep]);
+export default function OnboardPage({
+ configSchemaVersions,
+}: InferGetServerSidePropsType) {
+ const { fn, colors, colorScheme } = useMantineTheme();
const background = colorScheme === 'dark' ? 'dark.6' : 'gray.1';
+ const [onboardingSteps, { open: showOnboardingSteps }] = useDisclosure(false);
+
+ const isUpgradeFromSchemaOne = configSchemaVersions.includes(1);
+
return (
<>
@@ -50,193 +26,45 @@ export default function OnboardPage() {
-
-
-
+
+
+
-
-
- {stepContents.map((_, index) => (
-
- ))}
-
-
-
+
+ {onboardingSteps ? (
+
+ ) : (
+
+
+
+ Welcome to Homarr!
+
+
+ Your favorite dashboard has received a big upgrade.
+
+ We'll help you update within the next few steps
+
+
+ }
+ variant="default"
+ >
+ Start update process
+
+
+
+ )}
>
);
}
-type StepProps = {
- isCurrent: boolean;
- isMobile: boolean;
- isDark: boolean;
-};
-const Step = ({ isCurrent, isMobile, isDark }: StepProps) => {
- return (
-
- );
-};
-
-type StepContentComponent = (props: { isMobile: boolean; next: () => void }) => ReactNode;
-
-const FirstStepContent: StepContentComponent = ({ isMobile, next }) => {
- return (
- <>
-
- Hi there!
- Welcome to Homarr! 👋
-
-
- Before you can use Homarr, you need to configure a few things.
-
-
- Start configuration
-
- >
- );
-};
-
-const SecondStepContent: StepContentComponent = ({ isMobile, next }) => {
- const [isSigninIn, setIsSigninIn] = useState(false);
- const { mutateAsync } = api.user.createOwnerAccount.useMutation();
- const { i18nZodResolver } = useI18nZodResolver();
-
- const form = useForm>({
- validate: i18nZodResolver(signUpFormSchema),
- validateInputOnBlur: true,
- });
- const handleSubmit = (values: z.infer) => {
- setIsSigninIn(true);
- void mutateAsync(values, {
- onSuccess: () => {
- signIn('credentials', {
- redirect: false,
- name: values.username,
- password: values.password,
- callbackUrl: '/',
- }).then((response) => {
- if (!response?.ok) {
- setIsSigninIn(false);
- return;
- }
- next();
- });
- },
- });
- };
-
- return (
- <>
- Configure your credentials
-
- >
- );
-};
-
-const firstActions = [
- {
- icon: IconBook2,
- label: 'Read the documentation',
- href: 'https://homarr.dev/docs/introduction/after-the-installation',
- },
- {
- icon: IconUserPlus,
- label: 'Invite an user',
- href: '/manage/users/invites',
- },
- {
- icon: IconLayoutDashboard,
- label: 'Setup your board',
- href: '/board',
- },
- {
- icon: IconUserCog,
- label: 'Configure your profile',
- href: '/user/preferences',
- },
-];
-
-const ThirdStepContent: StepContentComponent = ({ isMobile, next }) => {
- const { breakpoints } = useMantineTheme();
- const { classes } = useStyles();
-
- return (
- <>
- Get started! 🚀
-
- {firstActions.map((action) => (
-
-
-
-
-
-
- {action.label}
-
-
-
-
-
-
-
- ))}
-
- >
- );
-};
-
-const useStyles = createStyles((theme) => ({
- button: {
- '&:hover': {
- backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
- },
- },
-}));
-
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const userCount = await prisma.user.count();
if (userCount >= 1) {
@@ -245,11 +73,16 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
};
}
+ const files = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json'));
+ const configs = files.map((file) => getConfig(file));
+ const configSchemaVersions = configs.map((config) => config.schemaVersion);
+
const translations = await getServerSideTranslations([], ctx.locale, ctx.req, ctx.res);
return {
props: {
...translations,
+ configSchemaVersions: configSchemaVersions,
},
};
};
diff --git a/yarn.lock b/yarn.lock
index 7e6e74b8d..d1baa73ab 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1086,6 +1086,21 @@ __metadata:
languageName: node
linkType: hard
+"@mantine/prism@npm:^6.0.19":
+ version: 6.0.19
+ resolution: "@mantine/prism@npm:6.0.19"
+ dependencies:
+ "@mantine/utils": 6.0.19
+ prism-react-renderer: ^1.2.1
+ peerDependencies:
+ "@mantine/core": 6.0.19
+ "@mantine/hooks": 6.0.19
+ react: ">=16.8.0"
+ react-dom: ">=16.8.0"
+ checksum: ae806b6341a0a34831ffd04cd5254e0d37ae3946cb3a0d3fd774b2a86feca09815616a580a581af3338cd7f3574a4861fc79680501b207d8596787e4ddc5b447
+ languageName: node
+ linkType: hard
+
"@mantine/ssr@npm:6.0.19":
version: 6.0.19
resolution: "@mantine/ssr@npm:6.0.19"
@@ -6179,6 +6194,7 @@ __metadata:
"@mantine/modals": ^6.0.0
"@mantine/next": ^6.0.0
"@mantine/notifications": ^6.0.0
+ "@mantine/prism": ^6.0.19
"@mantine/tiptap": ^6.0.17
"@next-auth/prisma-adapter": ^1.0.5
"@next/bundle-analyzer": ^13.0.0
@@ -8460,6 +8476,15 @@ __metadata:
languageName: node
linkType: hard
+"prism-react-renderer@npm:^1.2.1":
+ version: 1.3.5
+ resolution: "prism-react-renderer@npm:1.3.5"
+ peerDependencies:
+ react: ">=0.14.9"
+ checksum: c18806dcbc4c0b4fd6fd15bd06b4f7c0a6da98d93af235c3e970854994eb9b59e23315abb6cfc29e69da26d36709a47e25da85ab27fed81b6812f0a52caf6dfa
+ languageName: node
+ linkType: hard
+
"prisma@npm:^5.0.0":
version: 5.1.1
resolution: "prisma@npm:5.1.1"