fix: react hydration issues by using the same locale during ssr and hydration
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
This commit is contained in:
parent
ca38f7ca69
commit
63e12868b1
12 changed files with 65 additions and 18 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import { Card, CardContent } from "~/client/components/ui/card";
|
import { Card, CardContent } from "~/client/components/ui/card";
|
||||||
import { ByteSize } from "~/client/components/bytes-size";
|
import { ByteSize } from "~/client/components/bytes-size";
|
||||||
|
import { useRootLoaderData } from "~/client/hooks/use-root-loader-data";
|
||||||
import type { ResticSnapshotSummaryDto } from "@zerobyte/core/restic";
|
import type { ResticSnapshotSummaryDto } from "@zerobyte/core/restic";
|
||||||
import { formatDuration } from "~/utils/utils";
|
import { formatDuration } from "~/utils/utils";
|
||||||
|
|
||||||
|
|
@ -7,8 +8,6 @@ type Props = {
|
||||||
summary?: ResticSnapshotSummaryDto | null;
|
summary?: ResticSnapshotSummaryDto | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatCount = (value: number) => value.toLocaleString();
|
|
||||||
|
|
||||||
const getDurationLabel = (start: string, end: string) => {
|
const getDurationLabel = (start: string, end: string) => {
|
||||||
const startMs = new Date(start).getTime();
|
const startMs = new Date(start).getTime();
|
||||||
const endMs = new Date(end).getTime();
|
const endMs = new Date(end).getTime();
|
||||||
|
|
@ -17,8 +16,11 @@ const getDurationLabel = (start: string, end: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BackupSummaryCard = ({ summary }: Props) => {
|
export const BackupSummaryCard = ({ summary }: Props) => {
|
||||||
|
const { locale } = useRootLoaderData();
|
||||||
|
|
||||||
if (!summary) return null;
|
if (!summary) return null;
|
||||||
|
|
||||||
|
const formatCount = (value: number) => value.toLocaleString(locale);
|
||||||
const durationLabel = getDurationLabel(summary.backup_start, summary.backup_end);
|
const durationLabel = getDurationLabel(summary.backup_start, summary.backup_end);
|
||||||
|
|
||||||
const topStats = [
|
const topStats = [
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { formatBytes } from "~/utils/format-bytes";
|
import { useFormatBytes } from "~/client/hooks/use-format-bytes";
|
||||||
|
|
||||||
type ByteSizeProps = {
|
type ByteSizeProps = {
|
||||||
bytes: number;
|
bytes: number;
|
||||||
|
|
@ -14,6 +14,7 @@ type ByteSizeProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ByteSize(props: ByteSizeProps) {
|
export function ByteSize(props: ByteSizeProps) {
|
||||||
|
const formatBytes = useFormatBytes();
|
||||||
const {
|
const {
|
||||||
bytes,
|
bytes,
|
||||||
base = 1000,
|
base = 1000,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { ByteSize } from "~/client/components/bytes-size";
|
import { ByteSize } from "~/client/components/bytes-size";
|
||||||
|
import { useFormatBytes } from "~/client/hooks/use-format-bytes";
|
||||||
|
import { useRootLoaderData } from "~/client/hooks/use-root-loader-data";
|
||||||
import { Card } from "~/client/components/ui/card";
|
import { Card } from "~/client/components/ui/card";
|
||||||
import { Progress } from "~/client/components/ui/progress";
|
import { Progress } from "~/client/components/ui/progress";
|
||||||
import { type RestoreProgressEvent, useServerEvents } from "~/client/hooks/use-server-events";
|
import { type RestoreProgressEvent, useServerEvents } from "~/client/hooks/use-server-events";
|
||||||
import { formatBytes } from "~/utils/format-bytes";
|
|
||||||
import { formatDuration } from "~/utils/utils";
|
import { formatDuration } from "~/utils/utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -12,6 +13,8 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RestoreProgress = ({ repositoryId, snapshotId }: Props) => {
|
export const RestoreProgress = ({ repositoryId, snapshotId }: Props) => {
|
||||||
|
const formatBytes = useFormatBytes();
|
||||||
|
const { locale } = useRootLoaderData();
|
||||||
const { addEventListener } = useServerEvents();
|
const { addEventListener } = useServerEvents();
|
||||||
const [progress, setProgress] = useState<RestoreProgressEvent | null>(null);
|
const [progress, setProgress] = useState<RestoreProgressEvent | null>(null);
|
||||||
|
|
||||||
|
|
@ -71,9 +74,9 @@ export const RestoreProgress = ({ repositoryId, snapshotId }: Props) => {
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs uppercase text-muted-foreground">Files</p>
|
<p className="text-xs uppercase text-muted-foreground">Files</p>
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{progress.files_restored.toLocaleString()}
|
{progress.files_restored.toLocaleString(locale)}
|
||||||
/
|
/
|
||||||
{progress.total_files.toLocaleString()}
|
{progress.total_files.toLocaleString(locale)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as RechartsPrimitive from "recharts";
|
import * as RechartsPrimitive from "recharts";
|
||||||
|
|
||||||
|
import { useRootLoaderData } from "~/client/hooks/use-root-loader-data";
|
||||||
import { cn } from "~/client/lib/utils";
|
import { cn } from "~/client/lib/utils";
|
||||||
|
|
||||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||||
|
|
@ -116,6 +117,7 @@ function ChartTooltipContent({
|
||||||
labelKey?: string;
|
labelKey?: string;
|
||||||
}) {
|
}) {
|
||||||
const { config } = useChart();
|
const { config } = useChart();
|
||||||
|
const { locale } = useRootLoaderData();
|
||||||
|
|
||||||
const tooltipLabel = React.useMemo(() => {
|
const tooltipLabel = React.useMemo(() => {
|
||||||
if (hideLabel || !payload?.length) {
|
if (hideLabel || !payload?.length) {
|
||||||
|
|
@ -205,7 +207,7 @@ function ChartTooltipContent({
|
||||||
</div>
|
</div>
|
||||||
{item.value && (
|
{item.value && (
|
||||||
<span className="text-foreground font-mono font-medium tabular-nums">
|
<span className="text-foreground font-mono font-medium tabular-nums">
|
||||||
{item.value.toLocaleString()}
|
{item.value.toLocaleString(locale)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -565,10 +565,16 @@ function SidebarMenuSkeleton({
|
||||||
}: React.ComponentProps<"div"> & {
|
}: React.ComponentProps<"div"> & {
|
||||||
showIcon?: boolean;
|
showIcon?: boolean;
|
||||||
}) {
|
}) {
|
||||||
// Random width between 50 to 90%.
|
const skeletonId = React.useId();
|
||||||
const width = React.useMemo(() => {
|
const width = React.useMemo(() => {
|
||||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
let hash = 0;
|
||||||
}, []);
|
|
||||||
|
for (const char of skeletonId) {
|
||||||
|
hash = (hash * 31 + char.charCodeAt(0)) % 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${hash + 50}%`;
|
||||||
|
}, [skeletonId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
12
app/client/hooks/use-format-bytes.ts
Normal file
12
app/client/hooks/use-format-bytes.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { useRootLoaderData } from "~/client/hooks/use-root-loader-data";
|
||||||
|
import { formatBytes, type FormatBytesOptions } from "~/utils/format-bytes";
|
||||||
|
|
||||||
|
export const useFormatBytes = () => {
|
||||||
|
const { locale } = useRootLoaderData();
|
||||||
|
|
||||||
|
return (bytes: number, options?: FormatBytesOptions) =>
|
||||||
|
formatBytes(bytes, {
|
||||||
|
...options,
|
||||||
|
locale: options?.locale ?? locale,
|
||||||
|
});
|
||||||
|
};
|
||||||
3
app/client/hooks/use-root-loader-data.ts
Normal file
3
app/client/hooks/use-root-loader-data.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { Route as RootRoute } from "~/routes/__root";
|
||||||
|
|
||||||
|
export const useRootLoaderData = () => RootRoute.useLoaderData();
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { formatDistanceToNow, isValid } from "date-fns";
|
import { formatDistanceToNow, isValid } from "date-fns";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Route as RootRoute } from "~/routes/__root";
|
import { useRootLoaderData } from "~/client/hooks/use-root-loader-data";
|
||||||
|
|
||||||
export type DateInput = Date | string | number | null | undefined;
|
export type DateInput = Date | string | number | null | undefined;
|
||||||
|
|
||||||
|
|
@ -201,7 +201,7 @@ export function formatTimeAgo(date: DateInput, now = Date.now()): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTimeFormat() {
|
export function useTimeFormat() {
|
||||||
const { locale, timeZone, dateFormat, timeFormat, now } = RootRoute.useLoaderData();
|
const { locale, timeZone, dateFormat, timeFormat, now } = useRootLoaderData();
|
||||||
const [currentNow, setCurrentNow] = useState(now);
|
const [currentNow, setCurrentNow] = useState(now);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { ByteSize } from "~/client/components/bytes-size";
|
import { ByteSize } from "~/client/components/bytes-size";
|
||||||
|
import { useFormatBytes } from "~/client/hooks/use-format-bytes";
|
||||||
|
import { useRootLoaderData } from "~/client/hooks/use-root-loader-data";
|
||||||
import { Card } from "~/client/components/ui/card";
|
import { Card } from "~/client/components/ui/card";
|
||||||
import { Progress } from "~/client/components/ui/progress";
|
import { Progress } from "~/client/components/ui/progress";
|
||||||
import { getBackupProgressOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
import { getBackupProgressOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import type { GetBackupProgressResponse } from "~/client/api-client/types.gen";
|
import type { GetBackupProgressResponse } from "~/client/api-client/types.gen";
|
||||||
import { formatDuration } from "~/utils/utils";
|
import { formatDuration } from "~/utils/utils";
|
||||||
import { formatBytes } from "~/utils/format-bytes";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
scheduleShortId: string;
|
scheduleShortId: string;
|
||||||
|
|
@ -13,6 +14,8 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BackupProgressCard = ({ scheduleShortId, initialProgress }: Props) => {
|
export const BackupProgressCard = ({ scheduleShortId, initialProgress }: Props) => {
|
||||||
|
const formatBytes = useFormatBytes();
|
||||||
|
const { locale } = useRootLoaderData();
|
||||||
const { data: progress } = useQuery({
|
const { data: progress } = useQuery({
|
||||||
...getBackupProgressOptions({ path: { shortId: scheduleShortId } }),
|
...getBackupProgressOptions({ path: { shortId: scheduleShortId } }),
|
||||||
initialData: initialProgress,
|
initialData: initialProgress,
|
||||||
|
|
@ -52,7 +55,7 @@ export const BackupProgressCard = ({ scheduleShortId, initialProgress }: Props)
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{progress ? (
|
{progress ? (
|
||||||
<>
|
<>
|
||||||
{files_done.toLocaleString()} / {total_files.toLocaleString()}
|
{files_done.toLocaleString(locale)} / {total_files.toLocaleString(locale)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
"—"
|
"—"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import type { GetRepositoryStatsResponse } from "~/client/api-client/types.gen";
|
import type { GetRepositoryStatsResponse } from "~/client/api-client/types.gen";
|
||||||
import { ByteSize } from "~/client/components/bytes-size";
|
import { ByteSize } from "~/client/components/bytes-size";
|
||||||
|
import { useRootLoaderData } from "~/client/hooks/use-root-loader-data";
|
||||||
import { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
import { Card, CardContent, CardTitle } from "~/client/components/ui/card";
|
import { Card, CardContent, CardTitle } from "~/client/components/ui/card";
|
||||||
import { parseError } from "~/client/lib/errors";
|
import { parseError } from "~/client/lib/errors";
|
||||||
|
|
@ -26,6 +27,7 @@ const toSafeNumber = (value: number | undefined) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CompressionStatsChart({ repositoryShortId, initialStats }: Props) {
|
export function CompressionStatsChart({ repositoryShortId, initialStats }: Props) {
|
||||||
|
const { locale } = useRootLoaderData();
|
||||||
const refreshStats = useMutation({
|
const refreshStats = useMutation({
|
||||||
...refreshRepositoryStatsMutation(),
|
...refreshRepositoryStatsMutation(),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|
@ -133,7 +135,9 @@ export function CompressionStatsChart({ repositoryShortId, initialStats }: Props
|
||||||
<div className="flex flex-col gap-1.5 lg:col-span-2">
|
<div className="flex flex-col gap-1.5 lg:col-span-2">
|
||||||
<div className="text-sm font-medium text-muted-foreground">Snapshots</div>
|
<div className="text-sm font-medium text-muted-foreground">Snapshots</div>
|
||||||
<div className="flex items-baseline gap-2">
|
<div className="flex items-baseline gap-2">
|
||||||
<span className="text-xl font-semibold text-foreground font-mono">{snapshotsCount.toLocaleString()}</span>
|
<span className="text-xl font-semibold text-foreground font-mono">
|
||||||
|
{snapshotsCount.toLocaleString(locale)}
|
||||||
|
</span>
|
||||||
<span className="text-xs text-muted-foreground font-mono">
|
<span className="text-xs text-muted-foreground font-mono">
|
||||||
{compressionProgressPercent.toFixed(1)}% compressed
|
{compressionProgressPercent.toFixed(1)}% compressed
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { Input } from "~/client/components/ui/input";
|
||||||
import { Label } from "~/client/components/ui/label";
|
import { Label } from "~/client/components/ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
|
||||||
|
import { useRootLoaderData } from "~/client/hooks/use-root-loader-data";
|
||||||
import { authClient } from "~/client/lib/auth-client";
|
import { authClient } from "~/client/lib/auth-client";
|
||||||
import {
|
import {
|
||||||
DATE_FORMATS,
|
DATE_FORMATS,
|
||||||
|
|
@ -29,7 +30,6 @@ import {
|
||||||
} from "~/client/lib/datetime";
|
} from "~/client/lib/datetime";
|
||||||
import { logger } from "~/client/lib/logger";
|
import { logger } from "~/client/lib/logger";
|
||||||
import { type AppContext } from "~/context";
|
import { type AppContext } from "~/context";
|
||||||
import { Route as RootRoute } from "~/routes/__root";
|
|
||||||
import { TwoFactorSection } from "../components/two-factor-section";
|
import { TwoFactorSection } from "../components/two-factor-section";
|
||||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
import { SsoSettingsSection } from "~/client/modules/sso/components/sso-settings-section";
|
import { SsoSettingsSection } from "~/client/modules/sso/components/sso-settings-section";
|
||||||
|
|
@ -50,7 +50,7 @@ export function SettingsPage({ appContext, initialMembers, initialSsoSettings, i
|
||||||
const [downloadDialogOpen, setDownloadDialogOpen] = useState(false);
|
const [downloadDialogOpen, setDownloadDialogOpen] = useState(false);
|
||||||
const [downloadPassword, setDownloadPassword] = useState("");
|
const [downloadPassword, setDownloadPassword] = useState("");
|
||||||
const [isChangingPassword, setIsChangingPassword] = useState(false);
|
const [isChangingPassword, setIsChangingPassword] = useState(false);
|
||||||
const { locale, dateFormat, timeFormat } = RootRoute.useLoaderData();
|
const { locale, dateFormat, timeFormat } = useRootLoaderData();
|
||||||
|
|
||||||
const { tab } = useSearch({ from: "/(dashboard)/settings/" });
|
const { tab } = useSearch({ from: "/(dashboard)/settings/" });
|
||||||
const activeTab = tab || "account";
|
const activeTab = tab || "account";
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,20 @@
|
||||||
import "./setup.ts";
|
import "./setup.ts";
|
||||||
import { GlobalRegistrator } from "@happy-dom/global-registrator";
|
import { GlobalRegistrator } from "@happy-dom/global-registrator";
|
||||||
import { afterAll, afterEach, beforeAll } from "bun:test";
|
import { afterAll, afterEach, beforeAll, mock } from "bun:test";
|
||||||
import { client } from "~/client/api-client/client.gen";
|
import { client } from "~/client/api-client/client.gen";
|
||||||
import { server } from "~/test/msw/server";
|
import { server } from "~/test/msw/server";
|
||||||
|
|
||||||
|
void mock.module("~/client/hooks/use-root-loader-data", () => ({
|
||||||
|
useRootLoaderData: () => ({
|
||||||
|
theme: "dark",
|
||||||
|
locale: "en-US",
|
||||||
|
timeZone: "UTC",
|
||||||
|
dateFormat: "MM/DD/YYYY",
|
||||||
|
timeFormat: "12h",
|
||||||
|
now: Date.now(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
GlobalRegistrator.register({ url: "http://localhost:3000" });
|
GlobalRegistrator.register({ url: "http://localhost:3000" });
|
||||||
|
|
||||||
client.setConfig({
|
client.setConfig({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue