feat: announce Homarr v2 beta (#6793)
* feat: announce Homarr v2 beta * chore: refine v2 beta announcement * feat: expand v2 beta guidance * chore: refine v2 beta resources * chore: simplify v2 beta modal * chore: improve v2 beta feedback links * fix: keep v2 modal above header * chore: sharpen v2 beta message * fix: clear header for v2 modal
This commit is contained in:
1 parent
da32acb4f0
commit
32a51c1e60
13 files changed
+799
-97
No files matched your search
@@ -1,17 +1,21 @@
|
||||
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";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { BoardProvider } from "@homarr/boards/context";
|
||||
import { EditModeProvider } from "@homarr/boards/edit-mode";
|
||||
import { dbEnv } from "@homarr/core/infrastructure/db/env";
|
||||
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 +46,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 +63,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 +89,11 @@ 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";
|
||||
const canExportBackup = dbEnv.DRIVER === "better-sqlite3" && Boolean(session?.user.permissions.includes("admin"));
|
||||
|
||||
return (
|
||||
<BoardProvider initialBoard={initialBoard}>
|
||||
@@ -91,14 +102,20 @@ 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
|
||||
canExportBackup={canExportBackup}
|
||||
dismissalCookieName={dismissalCookieName}
|
||||
initiallyDismissed={isAnnouncementDismissed}
|
||||
header={
|
||||
<MainHeaderContent
|
||||
logo={<BoardLogoWithTitle size="md" hideTitleOnMobile />}
|
||||
actions={headerActions}
|
||||
hasNavigation={false}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</V2BetaDashboardShell>
|
||||
</BoardTourGate>
|
||||
</BoardMantineProvider>
|
||||
</EditModeProvider>
|
||||
|
||||
+4
-65
@@ -1,49 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Alert, Button, Card, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import { IconAlertTriangle, IconDatabaseExport, IconDownload } from "@tabler/icons-react";
|
||||
import { Card, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import { IconDatabaseExport } from "@tabler/icons-react";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { DangerousActionConfirmation } from "~/components/backup/dangerous-action-confirmation";
|
||||
import { BackupExportButton } from "~/components/backup";
|
||||
|
||||
export const BackupExportCard = () => {
|
||||
const t = useScopedI18n("management.page.tool.backup.export");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
|
||||
const handleExport = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch("/api/backup/export");
|
||||
if (!response.ok) {
|
||||
const errorMessage = await response.json().then(
|
||||
(data) => data.error,
|
||||
() => `Server returned ${response.status}`,
|
||||
);
|
||||
throw new Error(errorMessage ?? t("error"));
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const filename =
|
||||
response.headers.get("Content-Disposition")?.match(/filename="(.+)"/)?.[1] ?? "homarr-backup.zip";
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
setShowConfirm(false);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t("error"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card withBorder>
|
||||
@@ -55,33 +20,7 @@ export const BackupExportCard = () => {
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("description")}
|
||||
</Text>
|
||||
{error && (
|
||||
<Alert color="red" icon={<IconAlertTriangle size={16} />}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{showConfirm ? (
|
||||
<DangerousActionConfirmation
|
||||
title={t("confirm.title")}
|
||||
warningTitle={t("confirm.warningTitle")}
|
||||
warningBody={t("confirm.warningBody")}
|
||||
typePrompt={t("confirm.typePrompt", { phrase: "I understand" })}
|
||||
submitLabel={t("confirm.submit")}
|
||||
submitIcon={<IconDownload size={16} />}
|
||||
cancelLabel={t("confirm.cancel")}
|
||||
onConfirm={handleExport}
|
||||
onCancel={() => setShowConfirm(false)}
|
||||
disabled={loading}
|
||||
color="orange"
|
||||
/>
|
||||
) : (
|
||||
<Group>
|
||||
<Button leftSection={<IconDownload size={16} />} loading={loading} onClick={() => setShowConfirm(true)}>
|
||||
{t("button")}
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
<BackupExportButton />
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user