diff --git a/app/client/modules/repositories/components/compression-stats-chart.tsx b/app/client/modules/repositories/components/compression-stats-chart.tsx index 7523df77..0921b939 100644 --- a/app/client/modules/repositories/components/compression-stats-chart.tsx +++ b/app/client/modules/repositories/components/compression-stats-chart.tsx @@ -60,7 +60,6 @@ export function CompressionStatsChart({ repositoryShortId, initialStats }: Props const rawCompressionProgress = toSafeNumber(stats?.compression_progress); const compressionProgressPercent = Math.min(100, Math.max(0, rawCompressionProgress)); - const spaceSavingPercent = toSafeNumber(stats?.compression_space_saving); const snapshotsCount = Math.round(toSafeNumber(stats?.snapshots_count)); const hasStats = !!stats && (storedSize > 0 || uncompressedSize > 0 || snapshotsCount > 0); @@ -94,61 +93,53 @@ export function CompressionStatsChart({ repositoryShortId, initialStats }: Props Stats will be populated after your first backup. You can also refresh them manually.

-
-
-
- Stored - +
+
+ + of + data across {snapshotsCount} snapshots + +
+
+
+ On disk
-
-
+
+ = 80 })}>Freed by compression
-
-
- Uncompressed - -
-
-
-
+
+ + + freed +
-
-
-
Space Saved
-
- {spaceSavingPercent.toFixed(1)}% - -
+
+
+ Ratio + + {compressionRatio > 0 ? `${compressionRatio.toFixed(2)}x` : "-"} +
-
-
Ratio
-
- - {compressionRatio > 0 ? `${compressionRatio.toFixed(2)}x` : "-"} - -
+ +
+ Snapshots + {snapshotsCount.toLocaleString(locale)}
-
-
Snapshots
-
- - {snapshotsCount.toLocaleString(locale)} - -
-
-
-
Compressed
-
- - {compressionProgressPercent.toFixed(1)}% - -
+ +
+ Compressed + {compressionProgressPercent.toFixed(0)}%
diff --git a/app/client/modules/repositories/routes/repository-details.tsx b/app/client/modules/repositories/routes/repository-details.tsx index b34087b2..14b80360 100644 --- a/app/client/modules/repositories/routes/repository-details.tsx +++ b/app/client/modules/repositories/routes/repository-details.tsx @@ -1,12 +1,43 @@ -import { useSuspenseQuery } from "@tanstack/react-query"; -import { Suspense } from "react"; -import { getRepositoryOptions } from "~/client/api-client/@tanstack/react-query.gen"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs"; -import { RepositoryInfoTabContent } from "../tabs/info"; -import { RepositorySnapshotsTabContent } from "../tabs/snapshots"; +import { useMutation, useSuspenseQuery } from "@tanstack/react-query"; +import { Suspense, useState } from "react"; import { useNavigate, useSearch } from "@tanstack/react-router"; +import { toast } from "sonner"; +import { ChevronDown, Database, Pencil, Square, Stethoscope, Trash2, Unlock } from "lucide-react"; +import { + cancelDoctorMutation, + deleteRepositoryMutation, + getRepositoryOptions, + startDoctorMutation, + unlockRepositoryMutation, +} from "~/client/api-client/@tanstack/react-query.gen"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogHeader, + AlertDialogTitle, +} from "~/client/components/ui/alert-dialog"; +import { Badge } from "~/client/components/ui/badge"; +import { Button } from "~/client/components/ui/button"; +import { Card } from "~/client/components/ui/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "~/client/components/ui/dropdown-menu"; +import { Separator } from "~/client/components/ui/separator"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs"; +import { formatDateTime, formatTimeAgo } from "~/client/lib/datetime"; +import { parseError } from "~/client/lib/errors"; +import { cn } from "~/client/lib/utils"; import type { BackupSchedule, Snapshot } from "~/client/lib/types"; import type { GetRepositoryStatsResponse } from "~/client/api-client/types.gen"; +import { RepositoryInfoTabContent } from "../tabs/info"; +import { RepositorySnapshotsTabContent } from "../tabs/snapshots"; export default function RepositoryDetailsPage({ repositoryId, @@ -23,30 +54,209 @@ export default function RepositoryDetailsPage({ const { tab } = useSearch({ from: "/(dashboard)/repositories/$repositoryId/" }); const activeTab = tab || "info"; - const { data } = useSuspenseQuery({ + const { data: repository } = useSuspenseQuery({ ...getRepositoryOptions({ path: { shortId: repositoryId } }), }); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + + const deleteRepo = useMutation({ + ...deleteRepositoryMutation(), + onSuccess: () => { + toast.success("Repository deleted successfully"); + void navigate({ to: "/repositories" }); + }, + onError: (error) => { + toast.error("Failed to delete repository", { + description: parseError(error)?.message, + }); + }, + }); + + const startDoctor = useMutation({ + ...startDoctorMutation(), + onError: (error) => { + toast.error("Failed to start doctor", { + description: parseError(error)?.message, + }); + }, + }); + + const cancelDoctor = useMutation({ + ...cancelDoctorMutation(), + onSuccess: () => { + toast.info("Doctor operation cancelled"); + }, + onError: (error) => { + toast.error("Failed to cancel doctor", { + description: parseError(error)?.message, + }); + }, + }); + + const unlockRepo = useMutation({ + ...unlockRepositoryMutation(), + }); + + const handleConfirmDelete = () => { + setShowDeleteConfirm(false); + deleteRepo.mutate({ path: { shortId: repository.shortId } }); + }; + + const isDoctorRunning = repository.status === "doctor"; + return ( <> - navigate({ to: ".", search: (s) => ({ ...s, tab: value }) })}> - - Configuration - Snapshots - - - - - - - - - - +
+ +
+
+
+ +
+
+
+

{repository.name}

+ + + + {repository.status || "Unknown"} + + {repository.type} + {repository.provisioningId && Managed} +
+

+ 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) => + toast.error("Failed to unlock repository", { + description: parseError(e)?.message, + }), + }) + } + disabled={unlockRepo.isPending} + > + + Unlock + + + setShowDeleteConfirm(true)} + disabled={deleteRepo.isPending} + > + + Delete + + + +
+
+
+ + {repository.lastError && ( + +
+

Last Error

+

{repository.lastError}

+
+
+ )} + + navigate({ to: ".", search: () => ({ tab: value }) })}> + + Configuration + Snapshots + + + + + + + + + + +
+ + + + + Delete repository? + + Are you sure you want to delete the repository {repository.name}? This will not remove + the actual data from the backend storage, only the repository configuration will be deleted. +
+
+ All backup schedules associated with this repository will also be removed. +
+
+
+ Cancel + + + Delete repository + +
+
+
); } diff --git a/app/client/modules/repositories/tabs/info.tsx b/app/client/modules/repositories/tabs/info.tsx index 8753037d..02a0af9e 100644 --- a/app/client/modules/repositories/tabs/info.tsx +++ b/app/client/modules/repositories/tabs/info.tsx @@ -1,56 +1,10 @@ -import { useMutation } from "@tanstack/react-query"; -import { useState } from "react"; -import { toast } from "sonner"; -import { - Archive, - ChevronDown, - Clock, - Database, - FolderOpen, - Globe, - HardDrive, - Lock, - Pencil, - Settings, - Shield, - Square, - Stethoscope, - Trash2, - Unlock, -} from "lucide-react"; +import { Archive, Clock, FolderOpen, HardDrive, Lock, Settings, Shield } 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, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogHeader, - AlertDialogTitle, -} from "~/client/components/ui/alert-dialog"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "~/client/components/ui/dropdown-menu"; import type { Repository } from "~/client/lib/types"; import type { GetRepositoryStatsResponse } from "~/client/api-client/types.gen"; -import { - cancelDoctorMutation, - deleteRepositoryMutation, - startDoctorMutation, - unlockRepositoryMutation, -} from "~/client/api-client/@tanstack/react-query.gen"; +import { formatDateTime, formatTimeAgo } from "~/client/lib/datetime"; import type { RepositoryConfig } from "@zerobyte/core/restic"; -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"; @@ -78,281 +32,93 @@ function ConfigRow({ icon, label, value, mono, valueClassName }: ConfigRowProps) } export const RepositoryInfoTabContent = ({ repository, initialStats }: Props) => { - const { formatDateTime, formatTimeAgo } = useTimeFormat(); - const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); - const navigate = useNavigate(); - const effectiveLocalPath = getEffectiveLocalPath(repository); - const deleteRepo = useMutation({ - ...deleteRepositoryMutation(), - onSuccess: () => { - toast.success("Repository deleted successfully"); - void navigate({ to: "/repositories" }); - }, - onError: (error) => { - toast.error("Failed to delete repository", { - description: parseError(error)?.message, - }); - }, - }); - - const startDoctor = useMutation({ - ...startDoctorMutation(), - onError: (error) => { - toast.error("Failed to start doctor", { - description: parseError(error)?.message, - }); - }, - }); - - const cancelDoctor = useMutation({ - ...cancelDoctorMutation(), - onSuccess: () => { - toast.info("Doctor operation cancelled"); - }, - onError: (error) => { - toast.error("Failed to cancel doctor", { - description: parseError(error)?.message, - }); - }, - }); - - const unlockRepo = useMutation({ - ...unlockRepositoryMutation(), - }); - - const handleConfirmDelete = () => { - setShowDeleteConfirm(false); - deleteRepo.mutate({ path: { shortId: repository.shortId } }); - }; - const config = repository.config as RepositoryConfig; - const isDoctorRunning = repository.status === "doctor"; const hasLocalPath = Boolean(effectiveLocalPath); const hasCaCert = Boolean(config.cacert); - const hasLastError = Boolean(repository.lastError); const hasInsecureTlsConfig = config.insecureTls !== undefined; return ( - <> -
- -
-
-
- -
-
-
-

{repository.name}

- - - - {repository.status || "Unknown"} - - {repository.type} - {repository.provisioningId && Managed} -
-

- 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}

-
-
- )} - -
- - - - 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} /> + 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 && ( - } - label="Local Path" - value={effectiveLocalPath!} - mono - /> +
+
Local Path
+

{effectiveLocalPath}

+
)} - } - 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"} - /> - )} -
+
- -
- - - - Delete repository? - - Are you sure you want to delete the repository {repository.name}? This will not remove - the actual data from the backend storage, only the repository configuration will be deleted. -
-
- All backup schedules associated with this repository will also be removed. -
-
-
- Cancel - - - Delete repository - -
-
-
- + + + + Configuration + +
+ } label="Backend" value={repository.type} /> + {hasLocalPath && ( + } label="Local Path" value={effectiveLocalPath!} mono /> + )} + } + label="Compression Mode" + value={repository.compressionMode || "off"} + /> + {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"} + /> + )} +
+
+ + +
); }; diff --git a/app/client/modules/volumes/routes/volume-details.tsx b/app/client/modules/volumes/routes/volume-details.tsx index 63cd2585..68fd18a8 100644 --- a/app/client/modules/volumes/routes/volume-details.tsx +++ b/app/client/modules/volumes/routes/volume-details.tsx @@ -1,16 +1,49 @@ -import { useSuspenseQuery } from "@tanstack/react-query"; -import { useSearch } from "@tanstack/react-router"; - +import { useMutation, useSuspenseQuery } from "@tanstack/react-query"; +import { useNavigate, useSearch } from "@tanstack/react-router"; +import { useState } from "react"; +import { toast } from "sonner"; +import { Activity, ChevronDown, HardDrive, HeartIcon, Pencil, Plug, Trash2, Unplug } from "lucide-react"; +import { + deleteVolumeMutation, + getVolumeOptions, + healthCheckVolumeMutation, + mountVolumeMutation, + unmountVolumeMutation, + updateVolumeMutation, +} from "~/client/api-client/@tanstack/react-query.gen"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "~/client/components/ui/alert-dialog"; +import { Badge } from "~/client/components/ui/badge"; +import { Button } from "~/client/components/ui/button"; +import { Card } from "~/client/components/ui/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "~/client/components/ui/dropdown-menu"; +import { Separator } from "~/client/components/ui/separator"; +import { Switch } from "~/client/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs"; +import { ManagedBadge } from "~/client/components/managed-badge"; +import { formatDateTime, formatTimeAgo } from "~/client/lib/datetime"; +import { parseError } from "~/client/lib/errors"; +import { cn } from "~/client/lib/utils"; import { VolumeInfoTabContent } from "../tabs/info"; import { FilesTabContent } from "../tabs/files"; -import { getVolumeOptions } from "~/client/api-client/@tanstack/react-query.gen"; -import { useNavigate } from "@tanstack/react-router"; export function VolumeDetails({ volumeId }: { volumeId: string }) { const navigate = useNavigate(); const searchParams = useSearch({ from: "/(dashboard)/volumes/$volumeId/" }); - const activeTab = searchParams.tab || "info"; const { data } = useSuspenseQuery({ @@ -19,20 +52,248 @@ export function VolumeDetails({ volumeId }: { volumeId: string }) { const { volume, statfs } = data; + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + + const mountVol = useMutation({ + ...mountVolumeMutation(), + }); + + const unmountVol = useMutation({ + ...unmountVolumeMutation(), + }); + + const deleteVol = useMutation({ + ...deleteVolumeMutation(), + onSuccess: () => { + toast.success("Volume deleted successfully"); + void navigate({ to: "/volumes" }); + }, + onError: (error) => { + toast.error("Failed to delete volume", { + description: parseError(error)?.message, + }); + }, + }); + + const healthcheck = useMutation({ + ...healthCheckVolumeMutation(), + onSuccess: (d) => { + if (d.error) { + toast.error("Health check failed", { description: d.error }); + return; + } + toast.success("Health check completed", { description: "The volume is healthy." }); + }, + onError: (error) => { + toast.error("Health check failed", { description: error.message }); + }, + }); + + const toggleAutoRemount = useMutation({ + ...updateVolumeMutation(), + onSuccess: (d) => { + toast.success("Volume updated", { + description: `Auto remount is now ${d.autoRemount ? "enabled" : "paused"}.`, + }); + }, + onError: (error) => { + toast.error("Update failed", { description: error.message }); + }, + }); + + const handleConfirmDelete = () => { + setShowDeleteConfirm(false); + deleteVol.mutate({ path: { shortId: volume.shortId } }); + }; + + const isMounted = volume.status === "mounted"; + const isError = volume.status === "error"; + return ( <> - navigate({ to: ".", search: () => ({ tab: value }) })}> - - Configuration - Files - - - - - - - - +
+ +
+
+
+ +
+
+
+

{volume.name}

+ + + + {volume.status} + + {volume.type} + {volume.provisioningId && } +
+

Created {formatDateTime(volume.createdAt)}

+
+
+
+ {isMounted ? ( + + ) : ( + + )} + + + + + + navigate({ to: `/volumes/${volume.shortId}/edit` })}> + + Edit + + + setShowDeleteConfirm(true)} + disabled={deleteVol.isPending} + > + + Delete + + + +
+
+
+ + +
+
+
+ + Health + {isError ? ( + + Error + + ) : isMounted ? ( + + Healthy + + ) : ( + + Unmounted + + )} +
+ + Checked {formatTimeAgo(volume.lastHealthCheck)} + +
+ Auto-remount + + toggleAutoRemount.mutate({ + path: { shortId: volume.shortId }, + body: { autoRemount: !volume.autoRemount }, + }) + } + disabled={toggleAutoRemount.isPending} + /> +
+
+ +
+
+ + {volume.lastError && ( + +
+

Last Error

+

{volume.lastError}

+
+
+ )} + + navigate({ to: ".", search: () => ({ tab: value }) })}> + + Configuration + Files + + + + + + + + +
+ + + + + Delete volume? + + Are you sure you want to delete the volume {volume.name}? This action cannot be undone. +
+
+ All backup schedules associated with this volume will also be removed. +
+
+ + Cancel + + + Delete volume + + +
+
); } diff --git a/app/client/modules/volumes/tabs/info.tsx b/app/client/modules/volumes/tabs/info.tsx index 3b48d123..7ce10814 100644 --- a/app/client/modules/volumes/tabs/info.tsx +++ b/app/client/modules/volumes/tabs/info.tsx @@ -1,196 +1,201 @@ -import { useMutation } from "@tanstack/react-query"; -import { useState } from "react"; -import { toast } from "sonner"; -import { ChevronDown, Pencil, Plug, Trash2, Unplug } from "lucide-react"; -import { CreateVolumeForm } from "~/client/modules/volumes/components/create-volume-form"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "~/client/components/ui/alert-dialog"; -import { Button } from "~/client/components/ui/button"; -import { Card } from "~/client/components/ui/card"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "~/client/components/ui/dropdown-menu"; +import { useMemo } from "react"; +import { FolderOpen, HardDrive, Settings, Unplug } from "lucide-react"; +import { Label, Pie, PieChart } from "recharts"; +import { ByteSize } from "~/client/components/bytes-size"; +import { Card, CardTitle } from "~/client/components/ui/card"; +import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "~/client/components/ui/chart"; import type { StatFs, Volume } from "~/client/lib/types"; -import { HealthchecksCard } from "../components/healthchecks-card"; -import { StorageChart } from "../components/storage-chart"; -import { - deleteVolumeMutation, - mountVolumeMutation, - unmountVolumeMutation, -} from "~/client/api-client/@tanstack/react-query.gen"; -import { useNavigate } from "@tanstack/react-router"; -import { parseError } from "~/client/lib/errors"; -import { ManagedBadge } from "~/client/components/managed-badge"; +import { cn } from "~/client/lib/utils"; type Props = { volume: Volume; statfs: StatFs; }; -export const VolumeInfoTabContent = ({ volume, statfs }: Props) => { - const navigate = useNavigate(); +const backendLabels: Record = { + directory: "Directory", + nfs: "NFS", + smb: "SMB", + webdav: "WebDAV", + rclone: "rclone", + sftp: "SFTP", +}; - const mountVol = useMutation({ - ...mountVolumeMutation(), - }); +function ConfigRow({ + icon, + label, + value, + mono, +}: { + icon: React.ReactNode; + label: string; + value: string; + mono?: boolean; +}) { + return ( +
+ {icon} + {label} + {value} +
+ ); +} - const unmountVol = useMutation({ - ...unmountVolumeMutation(), - }); +function BackendConfigRows({ volume }: { volume: Volume }) { + const config = volume.config; - const deleteVol = useMutation({ - ...deleteVolumeMutation(), - onSuccess: async () => { - toast.success("Volume deleted successfully"); - await navigate({ to: "/volumes" }); - }, - onError: (error) => { - toast.error("Failed to delete volume", { - description: parseError(error)?.message, - }); - }, - }); + switch (config.backend) { + case "directory": + return } label="Directory Path" value={config.path} mono />; + case "nfs": + return ( + <> + } label="Server" value={config.server} mono /> + } label="Export Path" value={config.exportPath} mono /> + + ); + case "smb": + return ( + <> + } label="Server" value={config.server} mono /> + } label="Share" value={config.share} mono /> + + ); + case "webdav": + return ( + <> + } label="Server" value={config.server} mono /> + } label="Path" value={config.path} mono /> + + ); + case "rclone": + return ( + <> + } label="Remote" value={config.remote} mono /> + } label="Path" value={config.path} mono /> + + ); + case "sftp": + return ( + <> + } label="Host" value={config.host} mono /> + } label="Username" value={config.username} /> + } label="Path" value={config.path} mono /> + + ); + } +} - const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); +function DonutChart({ statfs }: { statfs: StatFs }) { + const chartData = useMemo( + () => [ + { name: "Used", value: statfs.used, fill: "var(--strong-accent)" }, + { name: "Free", value: statfs.free, fill: "lightgray" }, + ], + [statfs], + ); - const handleConfirmDelete = () => { - setShowDeleteConfirm(false); - deleteVol.mutate({ path: { shortId: volume.shortId } }); - }; + const chartConfig = {} satisfies ChartConfig; - const hasLastError = Boolean(volume.lastError); + const usagePercentage = useMemo(() => { + return Math.round((statfs.used / statfs.total) * 100); + }, [statfs]); return ( - <> -
-
- -
-
-
-

Volume Configuration

- {volume.provisioningId && } + + + [, name]} + /> + } + /> + + + + + ); +} + +export const VolumeInfoTabContent = ({ volume, statfs }: Props) => { + const hasStorage = statfs.total > 0; + + return ( + +
+
+ + + Configuration + +
+ } label="Name" value={volume.name} /> + } label="Backend" value={backendLabels[volume.type]} /> + +
+
+ + {hasStorage ? ( +
+ + + Storage + + +
+
+
+ + Total
+
-
- {volume.status !== "mounted" ? ( - - ) : ( - - )} - - - - - - navigate({ to: `/volumes/${volume.shortId}/edit` })}> - - Edit - - - setShowDeleteConfirm(true)} - disabled={deleteVol.isPending} - > - - Delete - - - +
+
+
+ Used +
+ +
+
+
+
+ Free +
+
- {}} - mode="update" - readOnly - /> - - {hasLastError && ( - -
-

Last Error

-

{volume.lastError}

-
-
- )} -
-
-
-
-
- + ) : ( +
+ +

+ No storage data available. +
+ Mount the volume to see usage. +

-
+ )}
- - - - Delete volume? - - Are you sure you want to delete the volume {volume.name}? This action cannot be undone. -
-
- All backup schedules associated with this volume will also be removed. -
-
- - Cancel - - - Delete volume - - -
-
- + ); };