-
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add onboarding with oldmarr import (#1606)
- Loading branch information
1 parent
82ec77d
commit 6de74d9
Showing
108 changed files
with
6,045 additions
and
312 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
"use client"; | ||
|
||
import { Button } from "@mantine/core"; | ||
|
||
import { clientApi } from "@homarr/api/client"; | ||
import { revalidatePathActionAsync } from "@homarr/common/client"; | ||
import { useI18n } from "@homarr/translation/client"; | ||
|
||
export const BackToStart = () => { | ||
const t = useI18n(); | ||
const { mutateAsync, isPending } = clientApi.onboard.previousStep.useMutation(); | ||
|
||
const handleBackToStartAsync = async () => { | ||
await mutateAsync(); | ||
await revalidatePathActionAsync("/init"); | ||
}; | ||
|
||
return ( | ||
<Button loading={isPending} variant="subtle" color="gray" fullWidth onClick={handleBackToStartAsync}> | ||
{t("init.backToStart")} | ||
</Button> | ||
); | ||
}; |
87 changes: 87 additions & 0 deletions
87
apps/nextjs/src/app/[locale]/init/_steps/finish/init-finish.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import Link from "next/link"; | ||
import type { MantineColor } from "@mantine/core"; | ||
import { Button, Card, Stack, Text } from "@mantine/core"; | ||
import { IconBook2, IconCategoryPlus, IconLayoutDashboard, IconMailForward } from "@tabler/icons-react"; | ||
|
||
import { isProviderEnabled } from "@homarr/auth/server"; | ||
import { getMantineColor } from "@homarr/common"; | ||
import { db } from "@homarr/db"; | ||
import { createDocumentationLink } from "@homarr/definitions"; | ||
import { getScopedI18n } from "@homarr/translation/server"; | ||
import type { TablerIcon } from "@homarr/ui"; | ||
|
||
export const InitFinish = async () => { | ||
const firstBoard = await db.query.boards.findFirst({ columns: { name: true } }); | ||
const tFinish = await getScopedI18n("init.step.finish"); | ||
|
||
return ( | ||
<Card w={64 * 6} maw="90vw" withBorder> | ||
<Stack> | ||
<Text>{tFinish("description")}</Text> | ||
|
||
{firstBoard ? ( | ||
<InternalLinkButton | ||
href={`/auth/login?callbackUrl=/boards/${firstBoard.name}`} | ||
iconProps={{ icon: IconLayoutDashboard, color: "blue" }} | ||
> | ||
{tFinish("action.goToBoard", { name: firstBoard.name })} | ||
</InternalLinkButton> | ||
) : ( | ||
<InternalLinkButton | ||
href="/auth/login?callbackUrl=/manage/boards" | ||
iconProps={{ icon: IconCategoryPlus, color: "blue" }} | ||
> | ||
{tFinish("action.createBoard")} | ||
</InternalLinkButton> | ||
)} | ||
|
||
{isProviderEnabled("credentials") && ( | ||
<InternalLinkButton | ||
href="/auth/login?callbackUrl=/manage/users/invites" | ||
iconProps={{ icon: IconMailForward, color: "pink" }} | ||
> | ||
{tFinish("action.inviteUser")} | ||
</InternalLinkButton> | ||
)} | ||
|
||
<ExternalLinkButton | ||
href={createDocumentationLink("/docs/getting-started/after-the-installation")} | ||
iconProps={{ icon: IconBook2, color: "yellow" }} | ||
> | ||
{tFinish("action.docs")} | ||
</ExternalLinkButton> | ||
</Stack> | ||
</Card> | ||
); | ||
}; | ||
|
||
interface LinkButtonProps { | ||
href: string; | ||
children: string; | ||
iconProps: IconProps; | ||
} | ||
|
||
interface IconProps { | ||
icon: TablerIcon; | ||
color: MantineColor; | ||
} | ||
|
||
const Icon = ({ icon: IcomComponent, color }: IconProps) => { | ||
return <IcomComponent color={getMantineColor(color, 6)} size={16} stroke={1.5} />; | ||
}; | ||
|
||
const InternalLinkButton = ({ href, children, iconProps }: LinkButtonProps) => { | ||
return ( | ||
<Button variant="default" component={Link} href={href} leftSection={<Icon {...iconProps} />}> | ||
{children} | ||
</Button> | ||
); | ||
}; | ||
|
||
const ExternalLinkButton = ({ href, children, iconProps }: LinkButtonProps) => { | ||
return ( | ||
<Button variant="default" component="a" href={href} leftSection={<Icon {...iconProps} />}> | ||
{children} | ||
</Button> | ||
); | ||
}; |
52 changes: 52 additions & 0 deletions
52
apps/nextjs/src/app/[locale]/init/_steps/group/init-group.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
"use client"; | ||
|
||
import { Button, Card, Stack, TextInput } from "@mantine/core"; | ||
import { IconArrowRight } from "@tabler/icons-react"; | ||
|
||
import { clientApi } from "@homarr/api/client"; | ||
import { revalidatePathActionAsync } from "@homarr/common/client"; | ||
import { useZodForm } from "@homarr/form"; | ||
import { useI18n } from "@homarr/translation/client"; | ||
import type { z } from "@homarr/validation"; | ||
import { validation } from "@homarr/validation"; | ||
|
||
export const InitGroup = () => { | ||
const t = useI18n(); | ||
const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation(); | ||
const form = useZodForm(validation.group.create, { | ||
initialValues: { | ||
name: "", | ||
}, | ||
}); | ||
|
||
const handleSubmitAsync = async (values: z.infer<typeof validation.group.create>) => { | ||
await mutateAsync(values, { | ||
async onSuccess() { | ||
await revalidatePathActionAsync("/init"); | ||
}, | ||
onError(error) { | ||
if (error.data?.code === "CONFLICT") { | ||
form.setErrors({ name: t("common.zod.errors.custom.groupNameTaken") }); | ||
} | ||
}, | ||
}); | ||
}; | ||
|
||
return ( | ||
<Card w={64 * 6} maw="90vw" withBorder> | ||
<form onSubmit={form.onSubmit(handleSubmitAsync)}> | ||
<Stack> | ||
<TextInput | ||
label={t("init.step.group.form.name.label")} | ||
description={t("init.step.group.form.name.description")} | ||
withAsterisk | ||
{...form.getInputProps("name")} | ||
/> | ||
<Button type="submit" loading={form.submitting} rightSection={<IconArrowRight size={16} stroke={1.5} />}> | ||
{t("common.action.continue")} | ||
</Button> | ||
</Stack> | ||
</form> | ||
</Card> | ||
); | ||
}; |
41 changes: 41 additions & 0 deletions
41
apps/nextjs/src/app/[locale]/init/_steps/import/file-info-card.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { ActionIcon, Button, Card, Group, Text } from "@mantine/core"; | ||
import type { FileWithPath } from "@mantine/dropzone"; | ||
import { IconPencil } from "@tabler/icons-react"; | ||
|
||
import { humanFileSize } from "@homarr/common"; | ||
import { useScopedI18n } from "@homarr/translation/client"; | ||
|
||
interface FileInfoCardProps { | ||
file: FileWithPath; | ||
onRemove: () => void; | ||
} | ||
|
||
export const FileInfoCard = ({ file, onRemove }: FileInfoCardProps) => { | ||
const tFileInfo = useScopedI18n("init.step.import.fileInfo"); | ||
return ( | ||
<Card w={64 * 12 + 8} maw="90vw"> | ||
<Group justify="space-between" align="center" wrap="nowrap"> | ||
<Group> | ||
<Text fw={500} lineClamp={1} style={{ wordBreak: "break-all" }}> | ||
{file.name} | ||
</Text> | ||
<Text visibleFrom="md" c="gray.6" size="sm"> | ||
{humanFileSize(file.size)} | ||
</Text> | ||
</Group> | ||
<Button | ||
variant="subtle" | ||
color="gray" | ||
rightSection={<IconPencil size={16} stroke={1.5} />} | ||
onClick={onRemove} | ||
visibleFrom="md" | ||
> | ||
{tFileInfo("action.change")} | ||
</Button> | ||
<ActionIcon size="sm" variant="subtle" color="gray" hiddenFrom="md" onClick={onRemove}> | ||
<IconPencil size={16} stroke={1.5} /> | ||
</ActionIcon> | ||
</Group> | ||
</Card> | ||
); | ||
}; |
54 changes: 54 additions & 0 deletions
54
apps/nextjs/src/app/[locale]/init/_steps/import/import-dropzone.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { Group, rem, Text } from "@mantine/core"; | ||
import type { FileWithPath } from "@mantine/dropzone"; | ||
import { Dropzone, MIME_TYPES } from "@mantine/dropzone"; | ||
import { IconFileZip, IconUpload, IconX } from "@tabler/icons-react"; | ||
|
||
import "@mantine/dropzone/styles.css"; | ||
|
||
import { useScopedI18n } from "@homarr/translation/client"; | ||
|
||
interface ImportDropZoneProps { | ||
loading: boolean; | ||
updateFile: (file: FileWithPath) => void; | ||
} | ||
|
||
export const ImportDropZone = ({ loading, updateFile }: ImportDropZoneProps) => { | ||
const tDropzone = useScopedI18n("init.step.import.dropzone"); | ||
return ( | ||
<Dropzone | ||
onDrop={(files) => { | ||
const firstFile = files[0]; | ||
if (!firstFile) return; | ||
|
||
updateFile(firstFile); | ||
}} | ||
acceptColor="blue.6" | ||
rejectColor="red.6" | ||
accept={[MIME_TYPES.zip]} | ||
loading={loading} | ||
multiple={false} | ||
maxSize={1024 * 1024 * 1024 * 64} // 64 MB | ||
> | ||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: "none" }}> | ||
<Dropzone.Accept> | ||
<IconUpload style={{ width: rem(52), height: rem(52), color: "var(--mantine-color-blue-6)" }} stroke={1.5} /> | ||
</Dropzone.Accept> | ||
<Dropzone.Reject> | ||
<IconX style={{ width: rem(52), height: rem(52), color: "var(--mantine-color-red-6)" }} stroke={1.5} /> | ||
</Dropzone.Reject> | ||
<Dropzone.Idle> | ||
<IconFileZip style={{ width: rem(52), height: rem(52), color: "var(--mantine-color-dimmed)" }} stroke={1.5} /> | ||
</Dropzone.Idle> | ||
|
||
<div> | ||
<Text size="xl" inline> | ||
{tDropzone("title")} | ||
</Text> | ||
<Text size="sm" c="dimmed" inline mt={7}> | ||
{tDropzone("description")} | ||
</Text> | ||
</div> | ||
</Group> | ||
</Dropzone> | ||
); | ||
}; |
53 changes: 53 additions & 0 deletions
53
apps/nextjs/src/app/[locale]/init/_steps/import/init-import.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
"use client"; | ||
|
||
import { startTransition, useState } from "react"; | ||
import { Card, Stack } from "@mantine/core"; | ||
import type { FileWithPath } from "@mantine/dropzone"; | ||
|
||
import type { RouterOutputs } from "@homarr/api"; | ||
import { clientApi } from "@homarr/api/client"; | ||
import { InitialOldmarrImport } from "@homarr/old-import/components"; | ||
|
||
import { FileInfoCard } from "./file-info-card"; | ||
import { ImportDropZone } from "./import-dropzone"; | ||
|
||
export const InitImport = () => { | ||
const [file, setFile] = useState<FileWithPath | null>(null); | ||
const { isPending, mutate } = clientApi.import.analyseInitialOldmarrImport.useMutation(); | ||
const [analyseResult, setAnalyseResult] = useState<RouterOutputs["import"]["analyseInitialOldmarrImport"] | null>( | ||
null, | ||
); | ||
|
||
if (!file) { | ||
return ( | ||
<Card w={64 * 12 + 8} maw="90vw" withBorder> | ||
<ImportDropZone | ||
loading={isPending} | ||
updateFile={(file) => { | ||
const formData = new FormData(); | ||
formData.append("file", file); | ||
|
||
mutate(formData, { | ||
onSuccess: (result) => { | ||
startTransition(() => { | ||
setAnalyseResult(result); | ||
setFile(file); | ||
}); | ||
}, | ||
onError: (error) => { | ||
console.error(error); | ||
}, | ||
}); | ||
}} | ||
/> | ||
</Card> | ||
); | ||
} | ||
|
||
return ( | ||
<Stack mb="sm"> | ||
<FileInfoCard file={file} onRemove={() => setFile(null)} /> | ||
{analyseResult !== null && <InitialOldmarrImport file={file} analyseResult={analyseResult} />} | ||
</Stack> | ||
); | ||
}; |
Oops, something went wrong.