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 + + + + + + + ); +}; 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. + + + + + + ); +}; 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 ? ( + + + + ) : ( + + + 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() { -
-
- Homarr Logo +
+
+ Homarr Logo
- - - {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 +
+ + +
+
+ )} ); } -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. - - - - ); -}; - -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"