refactor: format dates in a human-friendly way
This commit is contained in:
parent
1074d022b3
commit
e8216c4247
12 changed files with 125 additions and 43 deletions
|
|
@ -1,9 +1,9 @@
|
||||||
import { CronExpressionParser } from "cron-parser";
|
import { CronExpressionParser } from "cron-parser";
|
||||||
import { format } from "date-fns";
|
|
||||||
import { AlertCircle, CheckCircle2 } from "lucide-react";
|
import { AlertCircle, CheckCircle2 } from "lucide-react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||||
import { Input } from "~/client/components/ui/input";
|
import { Input } from "~/client/components/ui/input";
|
||||||
|
import { formatDateTime } from "~/client/lib/datetime";
|
||||||
import { cn } from "~/client/lib/utils";
|
import { cn } from "~/client/lib/utils";
|
||||||
|
|
||||||
interface CronInputProps {
|
interface CronInputProps {
|
||||||
|
|
@ -51,7 +51,9 @@ export function CronInput({ value, onChange, error }: CronInputProps) {
|
||||||
placeholder="* * * * *"
|
placeholder="* * * * *"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
className={cn("font-mono", { "border-destructive": error || (value && !isValid) })}
|
className={cn("font-mono", {
|
||||||
|
"border-destructive": error || (value && !isValid),
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
||||||
{value && (
|
{value && (
|
||||||
|
|
@ -77,7 +79,7 @@ export function CronInput({ value, onChange, error }: CronInputProps) {
|
||||||
{nextRuns.map((date, i) => (
|
{nextRuns.map((date, i) => (
|
||||||
<li key={date.toISOString()} className="text-xs font-mono flex items-center gap-2">
|
<li key={date.toISOString()} className="text-xs font-mono flex items-center gap-2">
|
||||||
<span className="text-muted-foreground w-4">{i + 1}.</span>
|
<span className="text-muted-foreground w-4">{i + 1}.</span>
|
||||||
{format(date, "PPP p")}
|
{formatDateTime(date)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export const OnOff = ({ isOn, toggle, enabledLabel, disabledLabel, disabled }: P
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-semibold uppercase tracking-wide transition-colors",
|
"flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-semibold uppercase tracking-wide transition-colors",
|
||||||
isOn
|
isOn
|
||||||
? "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-500/40 dark:bg-emerald-500/10 dark:text-emerald-200"
|
? "border-green-200 bg-green-50 text-green-700 dark:border-green-500/40 dark:bg-green-500/10 dark:text-green-200"
|
||||||
: "border-muted bg-muted/40 text-muted-foreground dark:border-muted/60 dark:bg-muted/10",
|
: "border-muted bg-muted/40 text-muted-foreground dark:border-muted/60 dark:bg-muted/10",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { format } from "date-fns";
|
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "~/client/components/ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "~/client/components/ui/dialog";
|
||||||
import { ScrollArea } from "~/client/components/ui/scroll-area";
|
import { ScrollArea } from "~/client/components/ui/scroll-area";
|
||||||
|
import { formatDate } from "~/client/lib/datetime";
|
||||||
import type { UpdateInfoDto } from "~/server/modules/system/system.dto";
|
import type { UpdateInfoDto } from "~/server/modules/system/system.dto";
|
||||||
|
|
||||||
interface ReleaseNotesDialogProps {
|
interface ReleaseNotesDialogProps {
|
||||||
|
|
@ -29,7 +29,7 @@ export function ReleaseNotesDialog({ open, onOpenChange, updates }: ReleaseNotes
|
||||||
<div key={release.version} className="space-y-4">
|
<div key={release.version} className="space-y-4">
|
||||||
<div className="flex items-center justify-between border-b pb-2">
|
<div className="flex items-center justify-between border-b pb-2">
|
||||||
<h3 className="text-lg font-bold text-foreground">{release.version}</h3>
|
<h3 className="text-lg font-bold text-foreground">{release.version}</h3>
|
||||||
<span className="text-sm text-muted-foreground">{format(new Date(release.publishedAt), "PPP")}</span>
|
<span className="text-sm text-muted-foreground">{formatDate(release.publishedAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="prose prose-sm dark:prose-invert max-w-none prose-pre:bg-muted prose-pre:text-muted-foreground prose-a:text-primary hover:prose-a:underline wrap-anywhere text-wrap prose-pre:whitespace-pre-wrap prose-pre:wrap-anywhere">
|
<div className="prose prose-sm dark:prose-invert max-w-none prose-pre:bg-muted prose-pre:text-muted-foreground prose-a:text-primary hover:prose-a:underline wrap-anywhere text-wrap prose-pre:whitespace-pre-wrap prose-pre:wrap-anywhere">
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{release.body}</ReactMarkdown>
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>{release.body}</ReactMarkdown>
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import {
|
||||||
} from "~/client/components/ui/dialog";
|
} from "~/client/components/ui/dialog";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||||
import { formatDuration } from "~/utils/utils";
|
import { formatDuration } from "~/utils/utils";
|
||||||
|
import { formatDateTime } from "~/client/lib/datetime";
|
||||||
import { deleteSnapshotsMutation, tagSnapshotsMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
import { deleteSnapshotsMutation, tagSnapshotsMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import { parseError } from "~/client/lib/errors";
|
import { parseError } from "~/client/lib/errors";
|
||||||
import type { BackupSchedule, Snapshot } from "../lib/types";
|
import type { BackupSchedule, Snapshot } from "../lib/types";
|
||||||
|
|
@ -185,7 +186,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => {
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
<Calendar className="h-4 w-4 text-muted-foreground" />
|
||||||
<span className="text-sm">{new Date(snapshot.time).toLocaleString()}</span>
|
<span className="text-sm">{formatDateTime(snapshot.time)}</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|
|
||||||
92
app/client/lib/datetime.ts
Normal file
92
app/client/lib/datetime.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { formatDistanceToNow, isValid } from "date-fns";
|
||||||
|
|
||||||
|
// 1/10/2026, 2:30 PM
|
||||||
|
export function formatDateTime(date: Date | string | number | null | undefined): string {
|
||||||
|
if (!date) return "Never";
|
||||||
|
const d = new Date(date);
|
||||||
|
if (!isValid(d)) return "Invalid Date";
|
||||||
|
|
||||||
|
return Intl.DateTimeFormat(navigator.languages, {
|
||||||
|
month: "numeric",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "numeric",
|
||||||
|
}).format(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jan 10, 2026
|
||||||
|
export function formatDateWithMonth(date: Date | string | number | null | undefined): string {
|
||||||
|
if (!date) return "Never";
|
||||||
|
const d = new Date(date);
|
||||||
|
if (!isValid(d)) return "Invalid Date";
|
||||||
|
return Intl.DateTimeFormat(navigator.languages, {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
}).format(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1/10/2026
|
||||||
|
export function formatDate(date: Date | string | number | null | undefined): string {
|
||||||
|
if (!date) return "Never";
|
||||||
|
const d = new Date(date);
|
||||||
|
if (!isValid(d)) return "Invalid Date";
|
||||||
|
|
||||||
|
return Intl.DateTimeFormat(navigator.languages, {
|
||||||
|
month: "numeric",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
}).format(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1/10
|
||||||
|
export function formatShortDate(date: Date | string | number | null | undefined): string {
|
||||||
|
if (!date) return "Never";
|
||||||
|
const d = new Date(date);
|
||||||
|
if (!isValid(d)) return "Invalid Date";
|
||||||
|
|
||||||
|
return Intl.DateTimeFormat(navigator.languages, {
|
||||||
|
month: "numeric",
|
||||||
|
day: "numeric",
|
||||||
|
}).format(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1/10, 2:30 PM
|
||||||
|
export function formatShortDateTime(date: Date | string | number | null | undefined): string {
|
||||||
|
if (!date) return "Never";
|
||||||
|
const d = new Date(date);
|
||||||
|
if (!isValid(d)) return "Invalid Date";
|
||||||
|
|
||||||
|
return Intl.DateTimeFormat(navigator.languages, {
|
||||||
|
month: "numeric",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "numeric",
|
||||||
|
}).format(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2:30 PM
|
||||||
|
export function formatTime(date: Date | string | number | null | undefined): string {
|
||||||
|
if (!date) return "Never";
|
||||||
|
const d = new Date(date);
|
||||||
|
if (!isValid(d)) return "Invalid Date";
|
||||||
|
|
||||||
|
return Intl.DateTimeFormat(navigator.languages, {
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "numeric",
|
||||||
|
}).format(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5 minutes ago
|
||||||
|
export function formatTimeAgo(date: Date | string | number | null | undefined): string {
|
||||||
|
if (!date) return "Never";
|
||||||
|
const d = new Date(date);
|
||||||
|
if (!isValid(d)) return "Invalid Date";
|
||||||
|
|
||||||
|
const timeAgo = formatDistanceToNow(d, {
|
||||||
|
addSuffix: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return timeAgo;
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import { Link } from "react-router";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||||
import type { BackupSchedule } from "~/client/lib/types";
|
import type { BackupSchedule } from "~/client/lib/types";
|
||||||
import { BackupStatusDot } from "./backup-status-dot";
|
import { BackupStatusDot } from "./backup-status-dot";
|
||||||
|
import { formatShortDateTime, formatTimeAgo } from "~/client/lib/datetime";
|
||||||
|
|
||||||
export const BackupCard = ({ schedule }: { schedule: BackupSchedule }) => {
|
export const BackupCard = ({ schedule }: { schedule: BackupSchedule }) => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -36,15 +37,11 @@ export const BackupCard = ({ schedule }: { schedule: BackupSchedule }) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Last backup</span>
|
<span className="text-muted-foreground">Last backup</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">{formatTimeAgo(schedule.lastBackupAt)}</span>
|
||||||
{schedule.lastBackupAt ? new Date(schedule.lastBackupAt).toLocaleDateString() : "Never"}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Next backup</span>
|
<span className="text-muted-foreground">Next backup</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">{formatShortDateTime(schedule.nextBackupAt)}</span>
|
||||||
{schedule.nextBackupAt ? new Date(schedule.nextBackupAt).toLocaleDateString() : "N/A"}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { useMutation } from "@tanstack/react-query";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { parseError } from "~/client/lib/errors";
|
import { parseError } from "~/client/lib/errors";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
|
import { formatShortDateTime, formatTimeAgo } from "~/client/lib/datetime";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
schedule: BackupSchedule;
|
schedule: BackupSchedule;
|
||||||
|
|
@ -155,15 +156,11 @@ export const ScheduleSummary = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs uppercase text-muted-foreground">Last backup</p>
|
<p className="text-xs uppercase text-muted-foreground">Last backup</p>
|
||||||
<p className="font-medium">
|
<p className="font-medium">{formatTimeAgo(schedule.lastBackupAt)}</p>
|
||||||
{schedule.lastBackupAt ? new Date(schedule.lastBackupAt).toLocaleString() : "Never"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs uppercase text-muted-foreground">Next backup</p>
|
<p className="text-xs uppercase text-muted-foreground">Next backup</p>
|
||||||
<p className="font-medium">
|
<p className="font-medium">{formatShortDateTime(schedule.nextBackupAt)}</p>
|
||||||
{schedule.nextBackupAt ? new Date(schedule.nextBackupAt).toLocaleString() : "Never"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/cli
|
||||||
import { Button, buttonVariants } from "~/client/components/ui/button";
|
import { Button, buttonVariants } from "~/client/components/ui/button";
|
||||||
import type { Snapshot } from "~/client/lib/types";
|
import type { Snapshot } from "~/client/lib/types";
|
||||||
import { listSnapshotFilesOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
import { listSnapshotFilesOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
|
import { formatDateTime } from "~/client/lib/datetime";
|
||||||
import { useFileBrowser } from "~/client/hooks/use-file-browser";
|
import { useFileBrowser } from "~/client/hooks/use-file-browser";
|
||||||
import { cn } from "~/client/lib/utils";
|
import { cn } from "~/client/lib/utils";
|
||||||
|
|
||||||
|
|
@ -90,7 +91,7 @@ export const SnapshotFileBrowser = (props: Props) => {
|
||||||
<CardTitle>File Browser</CardTitle>
|
<CardTitle>File Browser</CardTitle>
|
||||||
<CardDescription
|
<CardDescription
|
||||||
className={cn({ hidden: !snapshot.time })}
|
className={cn({ hidden: !snapshot.time })}
|
||||||
>{`Viewing snapshot from ${new Date(snapshot?.time ?? 0).toLocaleString()}`}</CardDescription>
|
>{`Viewing snapshot from ${formatDateTime(snapshot?.time)}`}</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Link
|
<Link
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { cn } from "~/client/lib/utils";
|
|
||||||
import { Card, CardContent } from "~/client/components/ui/card";
|
|
||||||
import { ByteSize } from "~/client/components/bytes-size";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import type { ListSnapshotsResponse } from "~/client/api-client";
|
import type { ListSnapshotsResponse } from "~/client/api-client";
|
||||||
|
import { ByteSize } from "~/client/components/bytes-size";
|
||||||
|
import { Card, CardContent } from "~/client/components/ui/card";
|
||||||
|
import { formatDateWithMonth, formatShortDate, formatTime } from "~/client/lib/datetime";
|
||||||
|
import { cn } from "~/client/lib/utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
snapshots: ListSnapshotsResponse;
|
snapshots: ListSnapshotsResponse;
|
||||||
|
|
@ -76,12 +77,8 @@ export const SnapshotTimeline = (props: Props) => {
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="text-xs font-semibold text-foreground">
|
<div className="text-xs font-semibold text-foreground">{formatShortDate(date)}</div>
|
||||||
{date.toLocaleDateString("en-US", { month: "short", day: "numeric" })}
|
<div className="text-xs text-muted-foreground">{formatTime(date)}</div>
|
||||||
</div>
|
|
||||||
<div className="text-xs text-muted-foreground">
|
|
||||||
{date.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-muted-foreground opacity-75">
|
<div className="text-xs text-muted-foreground opacity-75">
|
||||||
<ByteSize bytes={snapshot.size} />
|
<ByteSize bytes={snapshot.size} />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -98,8 +95,7 @@ export const SnapshotTimeline = (props: Props) => {
|
||||||
<div className="px-4 py-2 text-xs text-muted-foreground bg-card-header border-t border-border flex justify-between">
|
<div className="px-4 py-2 text-xs text-muted-foreground bg-card-header border-t border-border flex justify-between">
|
||||||
<span>{snapshots.length} snapshots</span>
|
<span>{snapshots.length} snapshots</span>
|
||||||
<span>
|
<span>
|
||||||
{new Date(snapshots[0].time).toLocaleDateString()} -{" "}
|
{formatDateWithMonth(snapshots[0].time)} - {formatDateWithMonth(snapshots.at(-1)?.time)}
|
||||||
{new Date(snapshots.at(-1)?.time ?? 0).toLocaleDateString()}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { redirect, useParams, Link, Await } from "react-router";
|
||||||
import { listBackupSchedulesOptions, listSnapshotFilesOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
import { listBackupSchedulesOptions, listSnapshotFilesOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||||
import { SnapshotFileBrowser } from "~/client/modules/backups/components/snapshot-file-browser";
|
import { SnapshotFileBrowser } from "~/client/modules/backups/components/snapshot-file-browser";
|
||||||
|
import { formatDateTime } from "~/client/lib/datetime";
|
||||||
import { getRepository, getSnapshotDetails } from "~/client/api-client";
|
import { getRepository, getSnapshotDetails } from "~/client/api-client";
|
||||||
import type { Route } from "./+types/snapshot-details";
|
import type { Route } from "./+types/snapshot-details";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
|
|
@ -112,7 +113,7 @@ export default function SnapshotDetailsPage({ loaderData }: Route.ComponentProps
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">Time:</span>
|
<span className="text-muted-foreground">Time:</span>
|
||||||
<p>{new Date(data.snapshot.time).toLocaleString()}</p>
|
<p>{formatDateTime(data.snapshot.time)}</p>
|
||||||
</div>
|
</div>
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
<Await resolve={loaderData.snapshot}>
|
<Await resolve={loaderData.snapshot}>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
} from "~/client/components/ui/alert-dialog";
|
} from "~/client/components/ui/alert-dialog";
|
||||||
import type { Repository } from "~/client/lib/types";
|
import type { Repository } from "~/client/lib/types";
|
||||||
import { REPOSITORY_BASE } from "~/client/lib/constants";
|
import { REPOSITORY_BASE } from "~/client/lib/constants";
|
||||||
|
import { formatDateTime, formatTimeAgo } from "~/client/lib/datetime";
|
||||||
import { updateRepositoryMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
import { updateRepositoryMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import type { CompressionMode, RepositoryConfig } from "~/schemas/restic";
|
import type { CompressionMode, RepositoryConfig } from "~/schemas/restic";
|
||||||
|
|
||||||
|
|
@ -131,13 +132,11 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-muted-foreground">Created at</div>
|
<div className="text-sm font-medium text-muted-foreground">Created at</div>
|
||||||
<p className="mt-1 text-sm">{new Date(repository.createdAt).toLocaleString()}</p>
|
<p className="mt-1 text-sm">{formatDateTime(repository.createdAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-muted-foreground">Last checked</div>
|
<div className="text-sm font-medium text-muted-foreground">Last checked</div>
|
||||||
<p className="mt-1 text-sm">
|
<p className="mt-1 text-sm">{formatTimeAgo(repository.lastChecked)}</p>
|
||||||
{repository.lastChecked ? new Date(repository.lastChecked).toLocaleString() : "Never"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{config.cacert && (
|
{config.cacert && (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { formatDistanceToNow } from "date-fns";
|
|
||||||
import { Activity, HeartIcon } from "lucide-react";
|
import { Activity, HeartIcon } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { healthCheckVolumeMutation, updateVolumeMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
import { healthCheckVolumeMutation, updateVolumeMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import { OnOff } from "~/client/components/onoff";
|
import { OnOff } from "~/client/components/onoff";
|
||||||
import { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||||
|
import { formatTimeAgo } from "~/client/lib/datetime";
|
||||||
import type { Volume } from "~/client/lib/types";
|
import type { Volume } from "~/client/lib/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -13,10 +13,6 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HealthchecksCard = ({ volume }: Props) => {
|
export const HealthchecksCard = ({ volume }: Props) => {
|
||||||
const timeAgo = formatDistanceToNow(volume.lastHealthCheck, {
|
|
||||||
addSuffix: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const healthcheck = useMutation({
|
const healthcheck = useMutation({
|
||||||
...healthCheckVolumeMutation(),
|
...healthCheckVolumeMutation(),
|
||||||
onSuccess: (d) => {
|
onSuccess: (d) => {
|
||||||
|
|
@ -55,9 +51,9 @@ export const HealthchecksCard = ({ volume }: Props) => {
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex flex-col flex-1 justify-start">
|
<div className="flex flex-col flex-1 justify-start">
|
||||||
{volume.lastError && <span className="text-sm text-red-500 wrap-break-word">{volume.lastError}</span>}
|
{volume.lastError && <span className="text-sm text-red-500 wrap-break-word">{volume.lastError}</span>}
|
||||||
{volume.status === "mounted" && <span className="text-md text-emerald-500">Healthy</span>}
|
{volume.status === "mounted" && <span className="text-md text-green-500">Healthy</span>}
|
||||||
{volume.status !== "unmounted" && (
|
{volume.status !== "unmounted" && (
|
||||||
<span className="text-xs text-muted-foreground mb-4">Checked {timeAgo || "never"}</span>
|
<span className="text-xs text-muted-foreground mb-4">Checked {formatTimeAgo(volume.lastHealthCheck)}</span>
|
||||||
)}
|
)}
|
||||||
<span className="flex justify-between items-center gap-2">
|
<span className="flex justify-between items-center gap-2">
|
||||||
<span className="text-sm">Remount on error</span>
|
<span className="text-sm">Remount on error</span>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue