refactor: status dot one component
This commit is contained in:
parent
c9e6bd04c9
commit
6d65cd9700
7 changed files with 86 additions and 118 deletions
|
|
@ -1,30 +1,42 @@
|
||||||
import type { VolumeStatus } from "~/client/lib/types";
|
|
||||||
import { cn } from "~/client/lib/utils";
|
import { cn } from "~/client/lib/utils";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
||||||
|
|
||||||
export const StatusDot = ({ status }: { status: VolumeStatus }) => {
|
type StatusVariant = "success" | "neutral" | "error" | "warning" | "info";
|
||||||
|
|
||||||
|
interface StatusDotProps {
|
||||||
|
variant: StatusVariant;
|
||||||
|
label: string;
|
||||||
|
animated?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StatusDot = ({ variant, label, animated }: StatusDotProps) => {
|
||||||
const statusMapping = {
|
const statusMapping = {
|
||||||
mounted: {
|
success: {
|
||||||
color: "bg-green-500",
|
color: "bg-green-500",
|
||||||
colorLight: "bg-emerald-400",
|
colorLight: "bg-emerald-400",
|
||||||
animated: true,
|
animated: animated ?? true,
|
||||||
},
|
},
|
||||||
unmounted: {
|
neutral: {
|
||||||
color: "bg-gray-500",
|
color: "bg-gray-500",
|
||||||
colorLight: "bg-gray-400",
|
colorLight: "bg-gray-400",
|
||||||
animated: false,
|
animated: animated ?? false,
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
color: "bg-red-500",
|
color: "bg-red-500",
|
||||||
colorLight: "bg-amber-700",
|
colorLight: "bg-red-400",
|
||||||
animated: true,
|
animated: animated ?? true,
|
||||||
},
|
},
|
||||||
unknown: {
|
warning: {
|
||||||
color: "bg-yellow-500",
|
color: "bg-yellow-500",
|
||||||
colorLight: "bg-yellow-400",
|
colorLight: "bg-yellow-400",
|
||||||
animated: true,
|
animated: animated ?? true,
|
||||||
},
|
},
|
||||||
}[status];
|
info: {
|
||||||
|
color: "bg-blue-500",
|
||||||
|
colorLight: "bg-blue-400",
|
||||||
|
animated: animated ?? true,
|
||||||
|
},
|
||||||
|
}[variant];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|
@ -42,7 +54,7 @@ export const StatusDot = ({ status }: { status: VolumeStatus }) => {
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p className="capitalize">{status}</p>
|
<p>{label}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
import { cn } from "~/client/lib/utils";
|
import { StatusDot } from "~/client/components/status-dot";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
|
|
||||||
|
|
||||||
type BackupStatus = "active" | "paused" | "error" | "in_progress";
|
|
||||||
|
|
||||||
export const BackupStatusDot = ({
|
export const BackupStatusDot = ({
|
||||||
enabled,
|
enabled,
|
||||||
|
|
@ -12,60 +9,22 @@ export const BackupStatusDot = ({
|
||||||
hasError?: boolean;
|
hasError?: boolean;
|
||||||
isInProgress?: boolean;
|
isInProgress?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
let status: BackupStatus = "paused";
|
let variant: "success" | "neutral" | "error" | "info";
|
||||||
|
let label: string;
|
||||||
|
|
||||||
if (isInProgress) {
|
if (isInProgress) {
|
||||||
status = "in_progress";
|
variant = "info";
|
||||||
|
label = "Backup in progress";
|
||||||
} else if (hasError) {
|
} else if (hasError) {
|
||||||
status = "error";
|
variant = "error";
|
||||||
|
label = "Error";
|
||||||
} else if (enabled) {
|
} else if (enabled) {
|
||||||
status = "active";
|
variant = "success";
|
||||||
|
label = "Active";
|
||||||
|
} else {
|
||||||
|
variant = "neutral";
|
||||||
|
label = "Paused";
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusMapping = {
|
return <StatusDot variant={variant} label={label} />;
|
||||||
active: {
|
|
||||||
color: "bg-green-500",
|
|
||||||
colorLight: "bg-emerald-400",
|
|
||||||
animated: true,
|
|
||||||
label: "Active",
|
|
||||||
},
|
|
||||||
paused: {
|
|
||||||
color: "bg-gray-500",
|
|
||||||
colorLight: "bg-gray-400",
|
|
||||||
animated: false,
|
|
||||||
label: "Paused",
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
color: "bg-red-500",
|
|
||||||
colorLight: "bg-red-400",
|
|
||||||
animated: true,
|
|
||||||
label: "Error",
|
|
||||||
},
|
|
||||||
in_progress: {
|
|
||||||
color: "bg-blue-500",
|
|
||||||
colorLight: "bg-blue-400",
|
|
||||||
animated: true,
|
|
||||||
label: "Backup in progress",
|
|
||||||
},
|
|
||||||
}[status];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<span className="relative flex size-3 mx-auto">
|
|
||||||
{statusMapping.animated && (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"absolute inline-flex h-full w-full animate-ping rounded-full opacity-75",
|
|
||||||
`${statusMapping.colorLight}`,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<span className={cn("relative inline-flex size-3 rounded-full", `${statusMapping.color}`)} />
|
|
||||||
</span>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{statusMapping.label}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,9 @@ export default function CreateNotification() {
|
||||||
|
|
||||||
const createNotification = useMutation({
|
const createNotification = useMutation({
|
||||||
...createNotificationDestinationMutation(),
|
...createNotificationDestinationMutation(),
|
||||||
onSuccess: (data) => {
|
onSuccess: () => {
|
||||||
toast.success("Notification destination created successfully");
|
toast.success("Notification destination created successfully");
|
||||||
navigate(`/notifications/${data.id}`);
|
navigate(`/notifications`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,6 @@ export default function NotificationDetailsPage({ loaderData }: Route.ComponentP
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const formId = useId();
|
const formId = useId();
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
|
||||||
|
|
||||||
const { data } = useQuery({
|
const { data } = useQuery({
|
||||||
...getNotificationDestinationOptions({ path: { id: String(loaderData.id) } }),
|
...getNotificationDestinationOptions({ path: { id: String(loaderData.id) } }),
|
||||||
|
|
@ -82,7 +81,6 @@ export default function NotificationDetailsPage({ loaderData }: Route.ComponentP
|
||||||
...updateNotificationDestinationMutation(),
|
...updateNotificationDestinationMutation(),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Notification destination updated successfully");
|
toast.success("Notification destination updated successfully");
|
||||||
setIsEditing(false);
|
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error("Failed to update notification destination", {
|
toast.error("Failed to update notification destination", {
|
||||||
|
|
@ -146,11 +144,6 @@ export default function NotificationDetailsPage({ loaderData }: Route.ComponentP
|
||||||
<TestTube2 className="h-4 w-4 mr-2" />
|
<TestTube2 className="h-4 w-4 mr-2" />
|
||||||
Test
|
Test
|
||||||
</Button>
|
</Button>
|
||||||
{!isEditing && (
|
|
||||||
<Button onClick={() => setIsEditing(true)} variant="outline">
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setShowDeleteConfirm(true)}
|
onClick={() => setShowDeleteConfirm(true)}
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
|
|
@ -180,46 +173,20 @@ export default function NotificationDetailsPage({ loaderData }: Route.ComponentP
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
<>
|
||||||
{isEditing ? (
|
<CreateNotificationForm
|
||||||
<>
|
mode="update"
|
||||||
<CreateNotificationForm
|
formId={formId}
|
||||||
mode="update"
|
onSubmit={handleSubmit}
|
||||||
formId={formId}
|
initialValues={data.config}
|
||||||
onSubmit={handleSubmit}
|
loading={updateDestination.isPending}
|
||||||
initialValues={data.config}
|
/>
|
||||||
loading={updateDestination.isPending}
|
<div className="flex justify-end gap-2 pt-4 border-t">
|
||||||
/>
|
<Button type="submit" form={formId} loading={updateDestination.isPending}>
|
||||||
<div className="flex justify-end gap-2 pt-4 border-t">
|
Save Changes
|
||||||
<Button
|
</Button>
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => setIsEditing(false)}
|
|
||||||
disabled={updateDestination.isPending}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" form={formId} loading={updateDestination.isPending}>
|
|
||||||
Save Changes
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<div className="text-sm font-medium text-muted-foreground">Type</div>
|
|
||||||
<div className="mt-1 capitalize">{data.type}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-sm font-medium text-muted-foreground">Created</div>
|
|
||||||
<div className="mt-1">{new Date(data.createdAt * 1000).toLocaleString()}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-sm font-medium text-muted-foreground">Last Updated</div>
|
|
||||||
<div className="mt-1">{new Date(data.updatedAt * 1000).toLocaleString()}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,7 @@ export default function Notifications({ loaderData }: Route.ComponentProps) {
|
||||||
<TableCell className="font-medium text-strong-accent">{notification.name}</TableCell>
|
<TableCell className="font-medium text-strong-accent">{notification.name}</TableCell>
|
||||||
<TableCell className="capitalize">{notification.type}</TableCell>
|
<TableCell className="capitalize">{notification.type}</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<StatusDot status={notification.enabled ? "mounted" : "unmounted"} />
|
<StatusDot variant={notification.enabled ? "success" : "neutral"} label={notification.enabled ? "Enabled" : "Disabled"} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import { DockerTabContent } from "../tabs/docker";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
|
||||||
import { useSystemInfo } from "~/client/hooks/use-system-info";
|
import { useSystemInfo } from "~/client/hooks/use-system-info";
|
||||||
import { getVolume } from "~/client/api-client";
|
import { getVolume } from "~/client/api-client";
|
||||||
|
import type { VolumeStatus } from "~/client/lib/types";
|
||||||
import {
|
import {
|
||||||
deleteVolumeMutation,
|
deleteVolumeMutation,
|
||||||
getVolumeOptions,
|
getVolumeOptions,
|
||||||
|
|
@ -31,6 +32,16 @@ import {
|
||||||
unmountVolumeMutation,
|
unmountVolumeMutation,
|
||||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
const getVolumeStatusVariant = (status: VolumeStatus): "success" | "neutral" | "error" | "warning" => {
|
||||||
|
const statusMap = {
|
||||||
|
mounted: "success" as const,
|
||||||
|
unmounted: "neutral" as const,
|
||||||
|
error: "error" as const,
|
||||||
|
unknown: "warning" as const,
|
||||||
|
};
|
||||||
|
return statusMap[status];
|
||||||
|
};
|
||||||
|
|
||||||
export const handle = {
|
export const handle = {
|
||||||
breadcrumb: (match: Route.MetaArgs) => [{ label: "Volumes", href: "/volumes" }, { label: match.params.name }],
|
breadcrumb: (match: Route.MetaArgs) => [{ label: "Volumes", href: "/volumes" }, { label: match.params.name }],
|
||||||
};
|
};
|
||||||
|
|
@ -124,7 +135,12 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) {
|
||||||
<div className="flex flex-col items-start xs:items-center xs:flex-row xs:justify-between">
|
<div className="flex flex-col items-start xs:items-center xs:flex-row xs:justify-between">
|
||||||
<div className="text-sm font-semibold mb-2 xs:mb-0 text-muted-foreground flex items-center gap-2">
|
<div className="text-sm font-semibold mb-2 xs:mb-0 text-muted-foreground flex items-center gap-2">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<StatusDot status={volume.status} /> {volume.status[0].toUpperCase() + volume.status.slice(1)}
|
<StatusDot
|
||||||
|
variant={getVolumeStatusVariant(volume.status)}
|
||||||
|
label={volume.status[0].toUpperCase() + volume.status.slice(1)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{volume.status[0].toUpperCase() + volume.status.slice(1)}
|
||||||
</span>
|
</span>
|
||||||
<VolumeIcon size={14} backend={volume?.config.backend} />
|
<VolumeIcon size={14} backend={volume?.config.backend} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,17 @@ import { VolumeIcon } from "~/client/components/volume-icon";
|
||||||
import type { Route } from "./+types/volumes";
|
import type { Route } from "./+types/volumes";
|
||||||
import { listVolumes } from "~/client/api-client";
|
import { listVolumes } from "~/client/api-client";
|
||||||
import { listVolumesOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
import { listVolumesOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
|
import type { VolumeStatus } from "~/client/lib/types";
|
||||||
|
|
||||||
|
const getVolumeStatusVariant = (status: VolumeStatus): "success" | "neutral" | "error" | "warning" => {
|
||||||
|
const statusMap = {
|
||||||
|
mounted: "success" as const,
|
||||||
|
unmounted: "neutral" as const,
|
||||||
|
error: "error" as const,
|
||||||
|
unknown: "warning" as const,
|
||||||
|
};
|
||||||
|
return statusMap[status];
|
||||||
|
};
|
||||||
|
|
||||||
export const handle = {
|
export const handle = {
|
||||||
breadcrumb: () => [{ label: "Volumes" }],
|
breadcrumb: () => [{ label: "Volumes" }],
|
||||||
|
|
@ -157,7 +168,10 @@ export default function Volumes({ loaderData }: Route.ComponentProps) {
|
||||||
<VolumeIcon backend={volume.type} />
|
<VolumeIcon backend={volume.type} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<StatusDot status={volume.status} />
|
<StatusDot
|
||||||
|
variant={getVolumeStatusVariant(volume.status)}
|
||||||
|
label={volume.status[0].toUpperCase() + volume.status.slice(1)}
|
||||||
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue