feat: announce Homarr v2 beta
This commit is contained in:
1 parent
da32acb4f0
commit
040d154744
10 files changed
+618
-32
No files matched your search
@@ -0,0 +1,61 @@
|
||||
name: Homarr v2 beta feedback
|
||||
description: Tell us what works, what breaks, or what could be clearer in the Homarr v2 beta
|
||||
title: "feedback(v2): "
|
||||
labels: ["needs triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for testing Homarr v2. It will become the default when it is ready, and every report helps us get there.
|
||||
|
||||
- type: dropdown
|
||||
id: feedback-type
|
||||
attributes:
|
||||
label: Feedback type
|
||||
options:
|
||||
- Bug
|
||||
- Upgrade or migration
|
||||
- Usability or design
|
||||
- Performance
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Homarr v2 version or image tag
|
||||
description: You can find the version in the About page or provide the Docker image tag you tested.
|
||||
placeholder: ghcr.io/homarr-labs/homarr-test:v2
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: feedback
|
||||
attributes:
|
||||
label: What did you find?
|
||||
description: Describe what happened, what you expected, or what you think could be improved.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: For bugs, list the shortest steps that consistently reproduce the problem.
|
||||
|
||||
- type: textarea
|
||||
id: additional-information
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add screenshots, logs, browser details, or anything else that may help. Remove secrets and personal information first.
|
||||
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Before submitting
|
||||
options:
|
||||
- label: I searched the existing issues for the same feedback.
|
||||
required: true
|
||||
- label: I removed passwords, API keys, tokens, and other private information.
|
||||
required: true
|
||||
@@ -11,6 +11,12 @@ tags:
|
||||
import Link from '@docusaurus/Link';
|
||||
import { IconArrowRight } from '@tabler/icons-react';
|
||||
|
||||
:::info Homarr v2 beta is available
|
||||
|
||||
There is no release date yet: Homarr v2 will become the default when it is ready. Read [what is new](https://v2.preview.homarr.dev/blog), try the [hosted preview](https://app-v2.preview.homarr.dev), [join us on Discord](https://discord.com/invite/aCsmEV5RgA), and [share your feedback](https://github.com/homarr-labs/homarr/issues/new?template=v2_beta_feedback.yml). Everyone uses Homarr differently, so every report helps us find what breaks or does not feel right before release.
|
||||
|
||||
:::
|
||||
|
||||
Before you try to install Homarr, please review if your device meets the requirements.
|
||||
Installing on an unsupported device may lead to unstable behaviour and no support will be provided.
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import type { JSX, PropsWithChildren } from "react";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { AppShellMain } from "@mantine/core";
|
||||
import { cookies } from "next/headers";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { getRscUserSettingsAsync } from "@homarr/api/user-server";
|
||||
@@ -8,10 +10,11 @@ import { auth } from "@homarr/auth/next";
|
||||
import { BoardProvider } from "@homarr/boards/context";
|
||||
import { EditModeProvider } from "@homarr/boards/edit-mode";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { v2BetaAnnouncementCookieKey } from "@homarr/definitions";
|
||||
|
||||
import { MainHeader } from "~/components/layout/header";
|
||||
import { MainHeaderContent } from "~/components/layout/header";
|
||||
import { BoardLogoWithTitle } from "~/components/layout/logo/board-logo";
|
||||
import { ClientShell } from "~/components/layout/shell";
|
||||
import { V2BetaDashboardShell } from "~/components/layout/v2-beta-announcement/v2-beta-dashboard-shell";
|
||||
import { BoardTourGate } from "~/components/onboarding/board-tour-gate";
|
||||
import { env } from "~/env";
|
||||
import { getCurrentColorSchemeAsync } from "~/theme/color-scheme";
|
||||
@@ -42,6 +45,7 @@ export const createBoardLayout = <TParams extends Params>({
|
||||
}>) => {
|
||||
const resolvedParams = await params;
|
||||
const sessionPromise = auth();
|
||||
const cookiesPromise = cookies();
|
||||
const initialBoardPromise = getInitialBoard(resolvedParams).then(
|
||||
(board) => ({ status: "fulfilled", board }) as const,
|
||||
(error: unknown) => ({ status: "rejected", error }) as const,
|
||||
@@ -58,11 +62,12 @@ export const createBoardLayout = <TParams extends Params>({
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const [session, initialBoardResult, colorScheme, shouldRunBoardTour] = await Promise.all([
|
||||
const [session, initialBoardResult, colorScheme, shouldRunBoardTour, cookieStore] = await Promise.all([
|
||||
sessionPromise,
|
||||
initialBoardPromise,
|
||||
colorSchemePromise,
|
||||
shouldRunBoardTourPromise,
|
||||
cookiesPromise,
|
||||
]);
|
||||
if (initialBoardResult.status === "rejected") {
|
||||
const { error } = initialBoardResult;
|
||||
@@ -83,6 +88,10 @@ export const createBoardLayout = <TParams extends Params>({
|
||||
throw error;
|
||||
}
|
||||
const initialBoard = initialBoardResult.board;
|
||||
const viewerIdentity = session?.user.id ?? "anonymous";
|
||||
const viewerHash = createHash("sha256").update(viewerIdentity).digest("hex").slice(0, 16);
|
||||
const dismissalCookieName = `${v2BetaAnnouncementCookieKey}.${viewerHash}`;
|
||||
const isAnnouncementDismissed = cookieStore.get(dismissalCookieName)?.value === "dismissed";
|
||||
|
||||
return (
|
||||
<BoardProvider initialBoard={initialBoard}>
|
||||
@@ -91,14 +100,19 @@ export const createBoardLayout = <TParams extends Params>({
|
||||
<BoardMantineProvider defaultColorScheme={colorScheme}>
|
||||
<CustomCss />
|
||||
<BoardTourGate enabled={shouldRunBoardTour}>
|
||||
<ClientShell hasNavigation={false}>
|
||||
<MainHeader
|
||||
logo={<BoardLogoWithTitle size="md" hideTitleOnMobile />}
|
||||
actions={headerActions}
|
||||
hasNavigation={false}
|
||||
/>
|
||||
<AppShellMain>{children}</AppShellMain>
|
||||
</ClientShell>
|
||||
<V2BetaDashboardShell
|
||||
dismissalCookieName={dismissalCookieName}
|
||||
initiallyDismissed={isAnnouncementDismissed}
|
||||
header={
|
||||
<MainHeaderContent
|
||||
logo={<BoardLogoWithTitle size="md" hideTitleOnMobile />}
|
||||
actions={headerActions}
|
||||
hasNavigation={false}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</V2BetaDashboardShell>
|
||||
</BoardTourGate>
|
||||
</BoardMantineProvider>
|
||||
</EditModeProvider>
|
||||
|
||||
@@ -19,25 +19,31 @@ interface Props {
|
||||
export const MainHeader = ({ logo, actions, hasNavigation = true }: Props) => {
|
||||
return (
|
||||
<AppShellHeader maw="100vw" zIndex={201} style={{ overflowX: "hidden" }}>
|
||||
<Group h="100%" gap="xl" px="md" justify="apart" wrap="nowrap">
|
||||
<Group h="100%" align="center" style={{ flex: 1 }} wrap="nowrap">
|
||||
{hasNavigation && <ClientBurger />}
|
||||
<UnstyledButton component={Link} href="/">
|
||||
{logo ?? <HomarrLogoWithTitle size="md" />}
|
||||
</UnstyledButton>
|
||||
</Group>
|
||||
<TourTarget id="board-search">
|
||||
<DesktopSearchInput />
|
||||
</TourTarget>
|
||||
<Group h="100%" align="center" justify="end" style={{ flex: 1 }} wrap="nowrap">
|
||||
{actions}
|
||||
<MobileSearchButton />
|
||||
<TourTarget id="board-user-menu">
|
||||
<UserButton />
|
||||
</TourTarget>
|
||||
</Group>
|
||||
</Group>
|
||||
<LazySpotlight />
|
||||
<MainHeaderContent logo={logo} actions={actions} hasNavigation={hasNavigation} />
|
||||
</AppShellHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export const MainHeaderContent = ({ logo, actions, hasNavigation = true }: Props) => (
|
||||
<>
|
||||
<Group h={60} gap="xl" px="md" justify="apart" wrap="nowrap">
|
||||
<Group h="100%" align="center" style={{ flex: 1 }} wrap="nowrap">
|
||||
{hasNavigation && <ClientBurger />}
|
||||
<UnstyledButton component={Link} href="/">
|
||||
{logo ?? <HomarrLogoWithTitle size="md" />}
|
||||
</UnstyledButton>
|
||||
</Group>
|
||||
<TourTarget id="board-search">
|
||||
<DesktopSearchInput />
|
||||
</TourTarget>
|
||||
<Group h="100%" align="center" justify="end" style={{ flex: 1 }} wrap="nowrap">
|
||||
{actions}
|
||||
<MobileSearchButton />
|
||||
<TourTarget id="board-user-menu">
|
||||
<UserButton />
|
||||
</TourTarget>
|
||||
</Group>
|
||||
</Group>
|
||||
<LazySpotlight />
|
||||
</>
|
||||
);
|
||||
@@ -10,11 +10,13 @@ import { navigationCollapsedAtom } from "./header/burger";
|
||||
interface ClientShellProps {
|
||||
hasHeader?: boolean;
|
||||
hasNavigation?: boolean;
|
||||
headerHeight?: number;
|
||||
}
|
||||
|
||||
export const ClientShell = ({
|
||||
hasHeader = true,
|
||||
hasNavigation = true,
|
||||
headerHeight = 60,
|
||||
children,
|
||||
}: PropsWithChildren<ClientShellProps>) => {
|
||||
const collapsed = useAtomValue(navigationCollapsedAtom);
|
||||
@@ -23,7 +25,7 @@ export const ClientShell = ({
|
||||
return (
|
||||
<AppShell
|
||||
{...backgroundProps}
|
||||
header={hasHeader ? { height: 60 } : undefined}
|
||||
header={hasHeader ? { height: headerHeight } : undefined}
|
||||
navbar={
|
||||
hasNavigation
|
||||
? {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export const v2BetaBlogUrl = "https://v2.preview.homarr.dev/blog";
|
||||
export const v2BetaPreviewUrl = "https://app-v2.preview.homarr.dev";
|
||||
export const v2BetaFeedbackUrl = "https://github.com/homarr-labs/homarr/issues/new?template=v2_beta_feedback.yml";
|
||||
export const v2BetaDiscussionUrl = "https://github.com/homarr-labs/homarr/issues/6600";
|
||||
export const discordInviteUrl = "https://discord.com/invite/aCsmEV5RgA";
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
.banner {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
height: 52px;
|
||||
overflow: hidden;
|
||||
color: var(--mantine-color-white);
|
||||
background: var(--mantine-color-red-9);
|
||||
border-bottom: 1px solid rgb(255 255 255 / 18%);
|
||||
}
|
||||
|
||||
.banner::after {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: -1.75rem;
|
||||
right: 12%;
|
||||
color: rgb(255 255 255 / 9%);
|
||||
font-size: 5.5rem;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
content: "v2";
|
||||
transform: rotate(5deg);
|
||||
}
|
||||
|
||||
.copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
opacity: 0.86;
|
||||
}
|
||||
|
||||
.modalHero {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
overflow: hidden;
|
||||
color: var(--mantine-color-white);
|
||||
background: var(--mantine-color-red-9);
|
||||
}
|
||||
|
||||
.modalHero::after {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: -3.5rem;
|
||||
right: 1.5rem;
|
||||
color: rgb(255 255 255 / 8%);
|
||||
font-size: 12rem;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
content: "v2";
|
||||
transform: rotate(5deg);
|
||||
}
|
||||
|
||||
.featureCard {
|
||||
height: 100%;
|
||||
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
|
||||
}
|
||||
|
||||
.qolCard {
|
||||
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
|
||||
}
|
||||
|
||||
.diffViewer {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.diffHeader {
|
||||
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-7));
|
||||
border-bottom: 1px solid var(--mantine-color-default-border);
|
||||
}
|
||||
|
||||
.diffCode {
|
||||
margin: 0;
|
||||
padding-block: var(--mantine-spacing-sm);
|
||||
overflow-x: auto;
|
||||
color: light-dark(var(--mantine-color-dark-8), var(--mantine-color-gray-2));
|
||||
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-9));
|
||||
font-family: var(--mantine-font-family-monospace);
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.diffLine_context,
|
||||
.diffLine_added,
|
||||
.diffLine_removed {
|
||||
display: block;
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
padding-inline: var(--mantine-spacing-md);
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.diffLine_added {
|
||||
color: light-dark(var(--mantine-color-green-9), var(--mantine-color-green-3));
|
||||
background: light-dark(var(--mantine-color-green-0), rgb(47 158 68 / 15%));
|
||||
}
|
||||
|
||||
.diffLine_removed {
|
||||
color: light-dark(var(--mantine-color-red-9), var(--mantine-color-red-3));
|
||||
background: light-dark(var(--mantine-color-red-0), rgb(250 82 82 / 15%));
|
||||
}
|
||||
|
||||
@media (max-width: $mantine-breakpoint-xs) {
|
||||
.banner::after {
|
||||
right: 35%;
|
||||
}
|
||||
|
||||
.modalHero::after {
|
||||
right: -2rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import {
|
||||
ActionIcon,
|
||||
Alert,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
CopyButton,
|
||||
Group,
|
||||
Paper,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconArrowsMove,
|
||||
IconBrandCss3,
|
||||
IconBrandDiscord,
|
||||
IconBrandGithub,
|
||||
IconCheck,
|
||||
IconCopy,
|
||||
IconExternalLink,
|
||||
IconHeartHandshake,
|
||||
IconRobot,
|
||||
IconRocket,
|
||||
IconSparkles,
|
||||
IconTerminal2,
|
||||
IconWand,
|
||||
IconX,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import { setClientCookie } from "@homarr/common";
|
||||
import { createModal, useModalAction } from "@homarr/modals";
|
||||
|
||||
import { discordInviteUrl, v2BetaBlogUrl, v2BetaDiscussionUrl, v2BetaFeedbackUrl, v2BetaPreviewUrl } from "./constants";
|
||||
import classes from "./v2-beta-announcement.module.css";
|
||||
|
||||
const diffLineClassNames = {
|
||||
added: classes.diffLine_added,
|
||||
context: classes.diffLine_context,
|
||||
removed: classes.diffLine_removed,
|
||||
} as const;
|
||||
|
||||
const composeDiffLines = [
|
||||
{ kind: "context", value: " services:" },
|
||||
{ kind: "context", value: " homarr:" },
|
||||
{ kind: "added", value: "+ image: ghcr.io/homarr-labs/homarr-test:v2" },
|
||||
{ kind: "removed", value: "- image: ghcr.io/homarr-labs/homarr:latest" },
|
||||
{ kind: "context", value: " ports:" },
|
||||
{ kind: "context", value: ' - "7575:7575"' },
|
||||
{ kind: "context", value: " volumes:" },
|
||||
{ kind: "context", value: " - homarr-v2:/appdata" },
|
||||
{ kind: "context", value: " environment:" },
|
||||
{ kind: "context", value: " SECRET_ENCRYPTION_KEY: <SECRET_KEY>" },
|
||||
{ kind: "context", value: " WORKSHOP_API_URL: https://v2.preview.homarr.dev/ # Only during the beta" },
|
||||
{ kind: "context", value: "" },
|
||||
{ kind: "context", value: " volumes:" },
|
||||
{ kind: "context", value: " homarr-v2:" },
|
||||
] as const;
|
||||
|
||||
const v2ComposeFile = `services:
|
||||
homarr:
|
||||
image: ghcr.io/homarr-labs/homarr-test:v2
|
||||
ports:
|
||||
- "7575:7575"
|
||||
volumes:
|
||||
- homarr-v2:/appdata
|
||||
environment:
|
||||
SECRET_ENCRYPTION_KEY: <SECRET_KEY>
|
||||
WORKSHOP_API_URL: https://v2.preview.homarr.dev/ # Only during the beta
|
||||
|
||||
volumes:
|
||||
homarr-v2:
|
||||
`;
|
||||
|
||||
interface V2BetaAnnouncementProps {
|
||||
dismissalCookieName: string;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
export const V2BetaAnnouncement = ({ dismissalCookieName, onDismiss }: V2BetaAnnouncementProps) => {
|
||||
const { openModal } = useModalAction(V2BetaAnnouncementModal);
|
||||
|
||||
const dismiss = () => {
|
||||
setClientCookie(dismissalCookieName, "dismissed", {
|
||||
maxAge: 60 * 60 * 24 * 365,
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
secure: window.location.protocol === "https:",
|
||||
});
|
||||
onDismiss();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box component="aside" className={classes.banner} aria-label="Homarr v2 beta announcement">
|
||||
<Group h="100%" justify="space-between" gap="sm" wrap="nowrap" px="md">
|
||||
<Group gap="sm" wrap="nowrap" className={classes.copy}>
|
||||
<ThemeIcon variant="white" color="red" radius="xl" size="md">
|
||||
<IconSparkles size={18} />
|
||||
</ThemeIcon>
|
||||
<Box className={classes.copy}>
|
||||
<Text fw={750} size="sm" truncate>
|
||||
Homarr v2 beta is here
|
||||
</Text>
|
||||
<Text className={classes.subtitle} visibleFrom="sm" size="xs" truncate>
|
||||
Test it with us and help make v2 ready for everyone.
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<Button variant="white" color="red" size="compact-sm" onClick={() => openModal(undefined)}>
|
||||
Explore v2
|
||||
</Button>
|
||||
<ActionIcon variant="subtle" color="white" onClick={dismiss} aria-label="Dismiss the v2 beta announcement">
|
||||
<IconX size={18} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Group>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const V2BetaAnnouncementModal = createModal<void>(() => {
|
||||
const { openModal: openInstallModal } = useModalAction(V2BetaInstallModal);
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Paper className={classes.modalHero} p={{ base: "lg", sm: "xl" }} radius="md">
|
||||
<Stack gap="sm" maw={760}>
|
||||
<Badge variant="white" color="red" size="lg" w="fit-content">
|
||||
Public beta
|
||||
</Badge>
|
||||
<Title order={2}>Help us get Homarr v2 ready</Title>
|
||||
<Text>
|
||||
There is no release date yet. Homarr v2 will become the default when it is ready—and we need you to test it
|
||||
and share feedback before we can be confident enough to make that switch.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||
<FeatureCard icon={<IconBrandCss3 size={21} />} title="Homarr Workshop">
|
||||
Share and discover Custom Widgets and CSS made by the Homarr community.
|
||||
</FeatureCard>
|
||||
<FeatureCard icon={<IconArrowsMove size={21} />} title="Complete drag-and-drop rewrite">
|
||||
Move, resize, multi-select, and arrange your dashboard with a board editor rebuilt from the ground up.
|
||||
</FeatureCard>
|
||||
<FeatureCard icon={<IconWand size={21} />} title="Custom Widgets v2">
|
||||
We think you can make almost any widget with live previews, API requests, options, actions, and validation.
|
||||
</FeatureCard>
|
||||
<FeatureCard icon={<IconRobot size={21} />} title="Homarr Assistant">
|
||||
Ask questions, find what you need, and manage Homarr through permission-aware tools.
|
||||
</FeatureCard>
|
||||
</SimpleGrid>
|
||||
|
||||
<Paper withBorder p="md" radius="md" className={classes.qolCard}>
|
||||
<Stack gap="sm">
|
||||
<Text fw={750}>And so much more</Text>
|
||||
<Text c="dimmed" size="sm">
|
||||
Hundreds of quality-of-life improvements, including:
|
||||
</Text>
|
||||
<Group gap="xs">
|
||||
<Badge variant="light">Brand-new onboarding</Badge>
|
||||
<Badge variant="light">Visual board switcher</Badge>
|
||||
<Badge variant="light">Keyboard shortcuts</Badge>
|
||||
<Badge variant="light">Instance branding</Badge>
|
||||
<Badge variant="light">Custom header buttons</Badge>
|
||||
<Badge variant="light">Custom header order</Badge>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Alert variant="light" color="red" icon={<IconHeartHandshake size={22} />} title="Your testing is essential">
|
||||
Everyone uses Homarr in a different way. We’re counting on you to tell us anything that breaks or doesn’t feel
|
||||
right with this new version. Every report matters.
|
||||
</Alert>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="sm">
|
||||
<Button
|
||||
component="a"
|
||||
href={v2BetaPreviewUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
size="md"
|
||||
leftSection={<IconRocket size={18} />}
|
||||
>
|
||||
Try the hosted preview
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
size="md"
|
||||
leftSection={<IconTerminal2 size={18} />}
|
||||
onClick={() => openInstallModal(undefined)}
|
||||
>
|
||||
Test v2 on your server
|
||||
</Button>
|
||||
<Button
|
||||
component="a"
|
||||
href={v2BetaBlogUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="default"
|
||||
size="md"
|
||||
rightSection={<IconExternalLink size={16} />}
|
||||
>
|
||||
See everything new
|
||||
</Button>
|
||||
<Button
|
||||
component="a"
|
||||
href={discordInviteUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="default"
|
||||
size="md"
|
||||
leftSection={<IconBrandDiscord size={18} />}
|
||||
>
|
||||
Join us on Discord
|
||||
</Button>
|
||||
</SimpleGrid>
|
||||
|
||||
<Button
|
||||
component="a"
|
||||
href={v2BetaFeedbackUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="light"
|
||||
color="red"
|
||||
size="md"
|
||||
leftSection={<IconBrandGithub size={18} />}
|
||||
>
|
||||
Report anything that breaks or feels wrong
|
||||
</Button>
|
||||
|
||||
<Group justify="space-between" align="center">
|
||||
<Text c="dimmed" size="xs" maw={720}>
|
||||
Please use a separate volume for the beta and keep a backup of your current Homarr data.
|
||||
</Text>
|
||||
<Button
|
||||
component="a"
|
||||
href={v2BetaDiscussionUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="subtle"
|
||||
size="compact-sm"
|
||||
rightSection={<IconExternalLink size={15} />}
|
||||
>
|
||||
Follow v2 development
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}).withOptions({
|
||||
defaultTitle: "Homarr v2 beta",
|
||||
size: 1000,
|
||||
centered: true,
|
||||
});
|
||||
|
||||
const V2BetaInstallModal = createModal<void>(() => (
|
||||
<Stack gap="lg">
|
||||
<Alert color="yellow" title="Keep your current Homarr data safe">
|
||||
Run the beta with the separate <code>homarr-v2</code> volume shown below. Keep a backup, and do not point this
|
||||
prerelease image at the only copy of your production data.
|
||||
</Alert>
|
||||
|
||||
<Stack gap="xs">
|
||||
<Title order={3}>Update your Docker Compose file</Title>
|
||||
<Text c="dimmed" size="sm">
|
||||
Replace the current image and add the beta-only Workshop URL. The complete example below can be copied as a
|
||||
starting point.
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Paper withBorder radius="md" className={classes.diffViewer}>
|
||||
<Group justify="space-between" px="md" py="sm" className={classes.diffHeader}>
|
||||
<Group gap="xs">
|
||||
<IconTerminal2 size={17} />
|
||||
<Text fw={700} size="sm">
|
||||
docker-compose.yml
|
||||
</Text>
|
||||
</Group>
|
||||
<CopyButton value={v2ComposeFile} timeout={2000}>
|
||||
{({ copied, copy }) => (
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="compact-sm"
|
||||
leftSection={copied ? <IconCheck size={15} /> : <IconCopy size={15} />}
|
||||
onClick={copy}
|
||||
>
|
||||
{copied ? "Copied" : "Copy file"}
|
||||
</Button>
|
||||
)}
|
||||
</CopyButton>
|
||||
</Group>
|
||||
<Box component="pre" className={classes.diffCode} aria-label="Docker Compose changes for Homarr v2 beta">
|
||||
{composeDiffLines.map((line, index) => (
|
||||
<Box component="span" className={diffLineClassNames[line.kind]} key={`${line.kind}-${index}`}>
|
||||
{line.value || " "}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
Replace <code><SECRET_KEY></code> with your existing encryption key. The Workshop URL is only required while
|
||||
the beta uses the preview Workshop.
|
||||
</Text>
|
||||
</Stack>
|
||||
)).withOptions({
|
||||
defaultTitle: "Test Homarr v2 on your server",
|
||||
size: 900,
|
||||
centered: true,
|
||||
});
|
||||
|
||||
interface FeatureCardProps {
|
||||
icon: ReactNode;
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const FeatureCard = ({ icon, title, children }: FeatureCardProps) => (
|
||||
<Paper className={classes.featureCard} withBorder p="md" radius="md">
|
||||
<Group gap="sm" align="flex-start" wrap="nowrap">
|
||||
<ThemeIcon variant="light" color="red" radius="md" size="lg">
|
||||
{icon}
|
||||
</ThemeIcon>
|
||||
<Box>
|
||||
<Text fw={750} size="sm">
|
||||
{title}
|
||||
</Text>
|
||||
<Text c="dimmed" size="sm">
|
||||
{children}
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
</Paper>
|
||||
);
|
||||
@@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import type { PropsWithChildren, ReactNode } from "react";
|
||||
import { useState } from "react";
|
||||
import { AppShellHeader, AppShellMain } from "@mantine/core";
|
||||
|
||||
import { ClientShell } from "../shell";
|
||||
import { V2BetaAnnouncement } from "./v2-beta-announcement";
|
||||
|
||||
interface V2BetaDashboardShellProps {
|
||||
dismissalCookieName: string;
|
||||
initiallyDismissed: boolean;
|
||||
header: ReactNode;
|
||||
}
|
||||
|
||||
export const V2BetaDashboardShell = ({
|
||||
dismissalCookieName,
|
||||
initiallyDismissed,
|
||||
header,
|
||||
children,
|
||||
}: PropsWithChildren<V2BetaDashboardShellProps>) => {
|
||||
const [isAnnouncementVisible, setIsAnnouncementVisible] = useState(!initiallyDismissed);
|
||||
const headerHeight = isAnnouncementVisible ? 112 : 60;
|
||||
|
||||
return (
|
||||
<ClientShell hasNavigation={false} headerHeight={headerHeight}>
|
||||
<AppShellHeader maw="100vw" zIndex={201} style={{ overflowX: "hidden" }}>
|
||||
{isAnnouncementVisible && (
|
||||
<V2BetaAnnouncement
|
||||
dismissalCookieName={dismissalCookieName}
|
||||
onDismiss={() => setIsAnnouncementVisible(false)}
|
||||
/>
|
||||
)}
|
||||
{header}
|
||||
</AppShellHeader>
|
||||
<AppShellMain>{children}</AppShellMain>
|
||||
</ClientShell>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +1,3 @@
|
||||
export const colorSchemeCookieKey = "homarr.color-scheme";
|
||||
export const localeCookieKey = "homarr.locale";
|
||||
export const v2BetaAnnouncementCookieKey = "homarr.v2-beta-announcement";
|
||||
Reference in new issue
Block a user