From 475bfb59ae7ff250fec245a3cbff9a45daf5ec6c Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:51:57 +0200 Subject: [PATCH] style: redesign respository details page (#739) --- .../components/compression-stats-chart.tsx | 138 +++---- app/client/modules/repositories/tabs/info.tsx | 361 ++++++++++-------- 2 files changed, 281 insertions(+), 218 deletions(-) diff --git a/app/client/modules/repositories/components/compression-stats-chart.tsx b/app/client/modules/repositories/components/compression-stats-chart.tsx index 4ada5e42..7523df77 100644 --- a/app/client/modules/repositories/components/compression-stats-chart.tsx +++ b/app/client/modules/repositories/components/compression-stats-chart.tsx @@ -8,7 +8,8 @@ import type { GetRepositoryStatsResponse } from "~/client/api-client/types.gen"; import { ByteSize } from "~/client/components/bytes-size"; import { useRootLoaderData } from "~/client/hooks/use-root-loader-data"; import { Button } from "~/client/components/ui/button"; -import { Card, CardContent, CardTitle } from "~/client/components/ui/card"; +import { Card, CardTitle } from "~/client/components/ui/card"; +import { Separator } from "~/client/components/ui/separator"; import { parseError } from "~/client/lib/errors"; import { cn } from "~/client/lib/utils"; import { toast } from "sonner"; @@ -64,25 +65,25 @@ export function CompressionStatsChart({ repositoryShortId, initialStats }: Props const hasStats = !!stats && (storedSize > 0 || uncompressedSize > 0 || snapshotsCount > 0); + const storedPercent = uncompressedSize > 0 ? (storedSize / uncompressedSize) * 100 : 0; + return ( -
- -
- - Compression Statistics -
- +
+ + + Compression Statistics +

Loading compression statistics...

@@ -92,58 +93,65 @@ export function CompressionStatsChart({ repositoryShortId, initialStats }: Props

Stats will be populated after your first backup. You can also refresh them manually.

- -
-
Stored Size
-
- +
+
+
+
+ Stored + +
+
+
+
+
+
+
+ Uncompressed + +
+
+
+
- -
-
Uncompressed
-
- + +
+
+
Space Saved
+
+ {spaceSavingPercent.toFixed(1)}% + +
+
+
+
Ratio
+
+ + {compressionRatio > 0 ? `${compressionRatio.toFixed(2)}x` : "-"} + +
+
+
+
Snapshots
+
+ + {snapshotsCount.toLocaleString(locale)} + +
+
+
+
Compressed
+
+ + {compressionProgressPercent.toFixed(1)}% + +
- -
-
Space Saved
-
- {spaceSavingPercent.toFixed(1)}% - -
-
- -
-
Ratio
-
- - {compressionRatio > 0 ? `${compressionRatio.toFixed(2)}x` : "-"} - -
-
- -
-
Snapshots
-
- - {snapshotsCount.toLocaleString(locale)} - - - {compressionProgressPercent.toFixed(1)}% compressed - -
-
- +
); } diff --git a/app/client/modules/repositories/tabs/info.tsx b/app/client/modules/repositories/tabs/info.tsx index 12c23bec..8753037d 100644 --- a/app/client/modules/repositories/tabs/info.tsx +++ b/app/client/modules/repositories/tabs/info.tsx @@ -1,9 +1,27 @@ import { useMutation } from "@tanstack/react-query"; import { useState } from "react"; import { toast } from "sonner"; -import { ChevronDown, Pencil, Square, Stethoscope, Trash2, Unlock } from "lucide-react"; +import { + Archive, + ChevronDown, + Clock, + Database, + FolderOpen, + Globe, + HardDrive, + Lock, + Pencil, + Settings, + Shield, + Square, + Stethoscope, + Trash2, + Unlock, +} from "lucide-react"; import { Card, CardContent, CardTitle } from "~/client/components/ui/card"; +import { Badge } from "~/client/components/ui/badge"; import { Button } from "~/client/components/ui/button"; +import { Separator } from "~/client/components/ui/separator"; import { AlertDialog, AlertDialogAction, @@ -29,14 +47,12 @@ import { unlockRepositoryMutation, } from "~/client/api-client/@tanstack/react-query.gen"; import type { RepositoryConfig } from "@zerobyte/core/restic"; -import { TimeAgo } from "~/client/components/time-ago"; import { useTimeFormat } from "~/client/lib/datetime"; import { DoctorReport } from "../components/doctor-report"; import { parseError } from "~/client/lib/errors"; import { useNavigate } from "@tanstack/react-router"; import { CompressionStatsChart } from "../components/compression-stats-chart"; import { cn } from "~/client/lib/utils"; -import { ManagedBadge } from "~/client/components/managed-badge"; type Props = { repository: Repository; @@ -48,8 +64,21 @@ const getEffectiveLocalPath = (repository: Repository): string | null => { return repository.config.path; }; +type ConfigRowProps = { icon: React.ReactNode; label: string; value: string; mono?: boolean; valueClassName?: string }; +function ConfigRow({ icon, label, value, mono, valueClassName }: ConfigRowProps) { + return ( +
+ {icon} + {label} + + {value} + +
+ ); +} + export const RepositoryInfoTabContent = ({ repository, initialStats }: Props) => { - const { formatDateTime } = useTimeFormat(); + const { formatDateTime, formatTimeAgo } = useTimeFormat(); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const navigate = useNavigate(); @@ -104,173 +133,199 @@ export const RepositoryInfoTabContent = ({ repository, initialStats }: Props) => const hasCaCert = Boolean(config.cacert); const hasLastError = Boolean(repository.lastError); const hasInsecureTlsConfig = config.insecureTls !== undefined; - const isTlsValidationDisabled = config.insecureTls === true; return ( <>
-
-
-
-

Repository Settings

- {repository.provisioningId && } -
-
-
- {isDoctorRunning ? ( - - ) : ( - - )} - - - - - - navigate({ to: `/repositories/${repository.shortId}/edit` })}> - - Edit - - - toast.promise(unlockRepo.mutateAsync({ path: { shortId: repository.shortId } }), { - loading: "Unlocking repo", - success: "Repository unlocked successfully", - error: (e) => parseError(e)?.message || "Failed to unlock repository", - }) - } - disabled={unlockRepo.isPending} - > - - Unlock - - - setShowDeleteConfirm(true)} - disabled={deleteRepo.isPending} - > - - Delete - - - -
-
- -
-
- - Overview - -
-
Name
-

{repository.name}

-
-
-
Backend
-

{repository.type}

-
-
-
Management
-

{repository.provisioningId ? "Provisioned" : "Manual"}

-
-
-
Compression Mode
-

{repository.compressionMode || "off"}

-
-
-
Created
-

{formatDateTime(repository.createdAt)}

-
-
-
Status
-

+ +

+
+
+ +
+
+
+

{repository.name}

+ + - {repository.status || "Unknown"} -

+ {repository.status || "Unknown"} +
+ {repository.type} + {repository.provisioningId && Managed}
-
-
Last Checked
- -
- {hasLocalPath && ( -
-
Local Path
-

{effectiveLocalPath}

-
- )} - {hasCaCert && ( -
-
CA Certificate
-

Configured

-
- )} - {hasInsecureTlsConfig && ( -
-
TLS Validation
-

- Disabled - Enabled -

-
- )} - - - - {hasLastError && ( - -
-

Last Error

-

{repository.lastError}

-
-
- )} - -
- +

+ Created {formatDateTime(repository.createdAt)} · Last checked  + {formatTimeAgo(repository.lastChecked)} +

+
+
+
+ {isDoctorRunning ? ( + + ) : ( + + )} + + + + + + navigate({ to: `/repositories/${repository.shortId}/edit` })}> + + Edit + + + toast.promise(unlockRepo.mutateAsync({ path: { shortId: repository.shortId } }), { + loading: "Unlocking repo", + success: "Repository unlocked successfully", + error: (e) => parseError(e)?.message || "Failed to unlock repository", + }) + } + disabled={unlockRepo.isPending} + > + + Unlock + + + setShowDeleteConfirm(true)} + disabled={deleteRepo.isPending} + > + + Delete + + +
+ -
- + {hasLastError && ( + +
+

Last Error

+

{repository.lastError}

+
+
+ )} - - Configuration -
-
-									{JSON.stringify(repository.config, null, 2)}
-								
+
+ + + + Overview + +
+
Name
+

{repository.name}

-
-
+
+
Backend
+

{repository.type}

+
+
+
Management
+

{repository.provisioningId ? "Provisioned" : "Manual"}

+
+
+
Compression Mode
+

{repository.compressionMode || "off"}

+
+
+
Created
+

{formatDateTime(repository.createdAt)}

+
+
+
Last Checked
+

+ + {formatTimeAgo(repository.lastChecked)} +

+
+ {hasLocalPath && ( +
+
Local Path
+

{effectiveLocalPath}

+
+ )} + +
+ + + + + Configuration + +
+ } label="Backend" value={repository.type} /> + {hasLocalPath && ( + } + label="Local Path" + value={effectiveLocalPath!} + mono + /> + )} + } + label="Compression Mode" + value={repository.compressionMode || "off"} + /> + } + label="Management" + value={repository.provisioningId ? "Provisioned" : "Manual"} + /> + {hasCaCert && ( + } + label="CA Certificate" + value="Configured" + valueClassName="text-success" + /> + )} + {hasInsecureTlsConfig && ( + } + label="TLS Validation" + value={config.insecureTls ? "Disabled" : "Enabled"} + valueClassName={config.insecureTls ? "text-red-500" : "text-success"} + /> + )} +
+
+ +