+
+ 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 ? (
+
cancelDoctor.mutate({ path: { shortId: repository.shortId } })}
+ >
+
+ Cancel doctor
+
+ ) : (
+
startDoctor.mutate({ path: { shortId: repository.shortId } })}
+ disabled={startDoctor.isPending}
+ >
+
+ Run doctor
+
+ )}
+
+
+
+ Actions
+
+
+
+
+ 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 ? (
-
cancelDoctor.mutate({ path: { shortId: repository.shortId } })}
- >
-
- Cancel doctor
-
- ) : (
-
startDoctor.mutate({ path: { shortId: repository.shortId } })}
- disabled={startDoctor.isPending}
- >
-
- Run doctor
-
- )}
-
-
-
- Actions
-
-
-
-
- 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 ? (
+
+ toast.promise(unmountVol.mutateAsync({ path: { shortId: volume.shortId } }), {
+ loading: "Unmounting volume...",
+ success: "Volume unmounted successfully",
+ error: (error) => parseError(error)?.message || "Failed to unmount volume",
+ })
+ }
+ loading={unmountVol.isPending}
+ >
+
+ Unmount
+
+ ) : (
+
+ toast.promise(mountVol.mutateAsync({ path: { shortId: volume.shortId } }), {
+ loading: "Mounting volume...",
+ success: "Volume mounted successfully",
+ error: (error) => parseError(error)?.message || "Failed to mount volume",
+ })
+ }
+ loading={mountVol.isPending}
+ >
+
+ Mount
+
+ )}
+
+
+
+ Actions
+
+
+
+
+ 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}
+ />
+
+
+
healthcheck.mutate({ path: { shortId: volume.shortId } })}
+ >
+
+ Check Now
+
+
+
+
+ {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]}
+ />
+ }
+ />
+
+ {
+ if (viewBox && "cx" in viewBox && "cy" in viewBox) {
+ return (
+
+
+ {usagePercentage}%
+
+
+ Used
+
+
+ );
+ }
+ }}
+ />
+
+
+
+ );
+}
+
+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
+
+
+
+
-
- {volume.status !== "mounted" ? (
-
- toast.promise(mountVol.mutateAsync({ path: { shortId: volume.shortId } }), {
- loading: "Mounting volume...",
- success: "Volume mounted successfully",
- error: (error) => parseError(error)?.message || "Failed to mount volume",
- })
- }
- loading={mountVol.isPending}
- >
-
- Mount
-
- ) : (
-
- toast.promise(unmountVol.mutateAsync({ path: { shortId: volume.shortId } }), {
- loading: "Unmounting volume...",
- success: "Volume unmounted successfully",
- error: (error) => parseError(error)?.message || "Failed to unmount volume",
- })
- }
- loading={unmountVol.isPending}
- >
-
- Unmount
-
- )}
-
-
-
- Actions
-
-
-
-
- navigate({ to: `/volumes/${volume.shortId}/edit` })}>
-
- Edit
-
-
- setShowDeleteConfirm(true)}
- disabled={deleteVol.isPending}
- >
-
- Delete
-
-
-
+
+
-
{}}
- 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
-
-
-
-
- >
+
);
};