refactor(client): configure time formatting with timezone from server
This commit is contained in:
parent
5a83d6ae86
commit
4c928cbc33
21 changed files with 141 additions and 55 deletions
|
|
@ -3,7 +3,7 @@ import { AlertCircle, CheckCircle2 } from "lucide-react";
|
|||
import { useMemo } from "react";
|
||||
import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||
import { Input } from "~/client/components/ui/input";
|
||||
import { formatDateTime } from "~/client/lib/datetime";
|
||||
import { useTimeFormat } from "~/client/lib/datetime";
|
||||
import { cn } from "~/client/lib/utils";
|
||||
|
||||
interface CronInputProps {
|
||||
|
|
@ -13,6 +13,7 @@ interface CronInputProps {
|
|||
}
|
||||
|
||||
export function CronInput({ value, onChange, error }: CronInputProps) {
|
||||
const { formatDateTime } = useTimeFormat();
|
||||
const { isValid, nextRuns, parseError } = useMemo(() => {
|
||||
if (!value) {
|
||||
return { isValid: false, nextRuns: [], parseError: null };
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { FileBrowser, type FileBrowserUiProps } from "~/client/components/file-b
|
|||
import { useFileBrowser } from "~/client/hooks/use-file-browser";
|
||||
import { parseError } from "~/client/lib/errors";
|
||||
import { normalizeAbsolutePath } from "@zerobyte/core/utils";
|
||||
import { logger } from "~/client/lib/logger";
|
||||
|
||||
type LocalFileBrowserProps = FileBrowserUiProps & {
|
||||
initialPath?: string;
|
||||
|
|
@ -26,7 +27,7 @@ export const LocalFileBrowser = ({ initialPath = "/", enabled = true, ...uiProps
|
|||
return await queryClient.ensureQueryData(browseFilesystemOptions({ query: { path } }));
|
||||
},
|
||||
prefetchFolder: (path) => {
|
||||
void queryClient.prefetchQuery(browseFilesystemOptions({ query: { path } }));
|
||||
void queryClient.prefetchQuery(browseFilesystemOptions({ query: { path } })).catch((e) => logger.error(e));
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { FileBrowser, type FileBrowserUiProps } from "~/client/components/file-b
|
|||
import { useFileBrowser } from "~/client/hooks/use-file-browser";
|
||||
import { parseError } from "~/client/lib/errors";
|
||||
import { normalizeAbsolutePath } from "@zerobyte/core/utils";
|
||||
import { logger } from "~/client/lib/logger";
|
||||
|
||||
type SnapshotTreeBrowserProps = FileBrowserUiProps & {
|
||||
repositoryId: string;
|
||||
|
|
@ -79,12 +80,14 @@ export const SnapshotTreeBrowser = ({
|
|||
);
|
||||
},
|
||||
prefetchFolder: (path) => {
|
||||
void queryClient.prefetchQuery(
|
||||
listSnapshotFilesOptions({
|
||||
path: { shortId: repositoryId, snapshotId },
|
||||
query: { path, offset: 0, limit: pageSize },
|
||||
}),
|
||||
);
|
||||
void queryClient
|
||||
.prefetchQuery(
|
||||
listSnapshotFilesOptions({
|
||||
path: { shortId: repositoryId, snapshotId },
|
||||
query: { path, offset: 0, limit: pageSize },
|
||||
}),
|
||||
)
|
||||
.catch((e) => logger.error(e));
|
||||
},
|
||||
pathTransform: {
|
||||
strip: stripBasePath,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { listFilesOptions } from "~/client/api-client/@tanstack/react-query.gen"
|
|||
import { FileBrowser, type FileBrowserUiProps } from "~/client/components/file-browsers/file-browser";
|
||||
import { useFileBrowser, type FetchFolderResult } from "~/client/hooks/use-file-browser";
|
||||
import { parseError } from "~/client/lib/errors";
|
||||
import { logger } from "~/client/lib/logger";
|
||||
|
||||
type VolumeFileBrowserProps = FileBrowserUiProps & {
|
||||
volumeId: string;
|
||||
|
|
@ -29,12 +30,14 @@ export const VolumeFileBrowser = ({ volumeId, enabled = true, ...uiProps }: Volu
|
|||
);
|
||||
},
|
||||
prefetchFolder: (path) => {
|
||||
void queryClient.prefetchQuery(
|
||||
listFilesOptions({
|
||||
path: { shortId: volumeId },
|
||||
query: { path },
|
||||
}),
|
||||
);
|
||||
void queryClient
|
||||
.prefetchQuery(
|
||||
listFilesOptions({
|
||||
path: { shortId: volumeId },
|
||||
query: { path },
|
||||
}),
|
||||
)
|
||||
.catch((e) => logger.error(e));
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -581,8 +581,10 @@ const NodeButton = memo(({ depth, icon, onClick, onMouseEnter, className, childr
|
|||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
<div
|
||||
// oxlint-disable-next-line jsx_a11y/prefer-tag-over-role
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={cn("flex items-center gap-2 w-full pr-2 text-sm py-1.5 text-left", className)}
|
||||
style={{ paddingLeft }}
|
||||
onClick={onClick}
|
||||
|
|
@ -591,7 +593,7 @@ const NodeButton = memo(({ depth, icon, onClick, onMouseEnter, className, childr
|
|||
>
|
||||
{icon}
|
||||
<div className="truncate w-full flex items-center gap-2">{children}</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import ReactMarkdown from "react-markdown";
|
|||
import remarkGfm from "remark-gfm";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "~/client/components/ui/dialog";
|
||||
import { ScrollArea } from "~/client/components/ui/scroll-area";
|
||||
import { formatDate } from "~/client/lib/datetime";
|
||||
import { useTimeFormat } from "~/client/lib/datetime";
|
||||
import type { UpdateInfoDto } from "~/server/modules/system/system.dto";
|
||||
|
||||
interface ReleaseNotesDialogProps {
|
||||
|
|
@ -12,6 +12,8 @@ interface ReleaseNotesDialogProps {
|
|||
}
|
||||
|
||||
export function ReleaseNotesDialog({ open, onOpenChange, updates }: ReleaseNotesDialogProps) {
|
||||
const { formatDate } = useTimeFormat();
|
||||
|
||||
if (!updates) return null;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ import {
|
|||
DialogTitle,
|
||||
} from "~/client/components/ui/dialog";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||
import { useTimeFormat } from "~/client/lib/datetime";
|
||||
import { formatDuration } from "~/utils/utils";
|
||||
import { formatDateTime } from "~/client/lib/datetime";
|
||||
import {
|
||||
deleteSnapshotsMutation,
|
||||
listSnapshotsQueryKey,
|
||||
|
|
@ -49,6 +49,7 @@ type Props = {
|
|||
export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshotsQueryOptions }: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const { formatDateTime } = useTimeFormat();
|
||||
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false);
|
||||
|
|
|
|||
12
app/client/components/time-ago.tsx
Normal file
12
app/client/components/time-ago.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { useTimeFormat, type DateInput } from "~/client/lib/datetime";
|
||||
|
||||
type Props = {
|
||||
date: DateInput;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function TimeAgo({ date, className }: Props) {
|
||||
const { formatTimeAgo } = useTimeFormat();
|
||||
|
||||
return <span className={className}>{formatTimeAgo(date)}</span>;
|
||||
}
|
||||
|
|
@ -60,4 +60,8 @@ describe("datetime formatters", () => {
|
|||
|
||||
nowSpy.mockRestore();
|
||||
});
|
||||
|
||||
test("formats calendar values with an explicit locale and timezone", () => {
|
||||
expect(formatShortDateTime(sampleDate, { locale: "en-US", timeZone: "UTC" })).toBe("1/10, 2:30 PM");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
import { formatDistanceToNow, isValid } from "date-fns";
|
||||
import { useMemo } from "react";
|
||||
import { Route as RootRoute } from "~/routes/__root";
|
||||
|
||||
type DateInput = Date | string | number | null | undefined;
|
||||
export type DateInput = Date | string | number | null | undefined;
|
||||
|
||||
const getLocales = () => (typeof navigator !== "undefined" ? navigator.languages : undefined);
|
||||
type DateFormatOptions = {
|
||||
locale?: string | string[];
|
||||
timeZone?: string;
|
||||
};
|
||||
|
||||
function formatValidDate(date: DateInput, formatter: (date: Date) => string): string {
|
||||
if (!date) return "Never";
|
||||
|
|
@ -13,10 +18,21 @@ function formatValidDate(date: DateInput, formatter: (date: Date) => string): st
|
|||
return formatter(parsedDate);
|
||||
}
|
||||
|
||||
function getDateTimeFormat(
|
||||
locale: DateFormatOptions["locale"],
|
||||
timeZone: DateFormatOptions["timeZone"],
|
||||
options: Intl.DateTimeFormatOptions,
|
||||
) {
|
||||
return Intl.DateTimeFormat(locale, {
|
||||
...options,
|
||||
timeZone,
|
||||
});
|
||||
}
|
||||
|
||||
// 1/10/2026, 2:30 PM
|
||||
export function formatDateTime(date: DateInput): string {
|
||||
export function formatDateTime(date: DateInput, options: DateFormatOptions = {}): string {
|
||||
return formatValidDate(date, (validDate) =>
|
||||
Intl.DateTimeFormat(getLocales(), {
|
||||
getDateTimeFormat(options.locale, options.timeZone, {
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
|
|
@ -27,9 +43,9 @@ export function formatDateTime(date: DateInput): string {
|
|||
}
|
||||
|
||||
// Jan 10, 2026
|
||||
export function formatDateWithMonth(date: DateInput): string {
|
||||
export function formatDateWithMonth(date: DateInput, options: DateFormatOptions = {}): string {
|
||||
return formatValidDate(date, (validDate) =>
|
||||
Intl.DateTimeFormat(getLocales(), {
|
||||
getDateTimeFormat(options.locale, options.timeZone, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
|
|
@ -38,9 +54,9 @@ export function formatDateWithMonth(date: DateInput): string {
|
|||
}
|
||||
|
||||
// 1/10/2026
|
||||
export function formatDate(date: DateInput): string {
|
||||
export function formatDate(date: DateInput, options: DateFormatOptions = {}): string {
|
||||
return formatValidDate(date, (validDate) =>
|
||||
Intl.DateTimeFormat(getLocales(), {
|
||||
getDateTimeFormat(options.locale, options.timeZone, {
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
|
|
@ -49,9 +65,9 @@ export function formatDate(date: DateInput): string {
|
|||
}
|
||||
|
||||
// 1/10
|
||||
export function formatShortDate(date: DateInput): string {
|
||||
export function formatShortDate(date: DateInput, options: DateFormatOptions = {}): string {
|
||||
return formatValidDate(date, (validDate) =>
|
||||
Intl.DateTimeFormat(getLocales(), {
|
||||
getDateTimeFormat(options.locale, options.timeZone, {
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
}).format(validDate),
|
||||
|
|
@ -59,9 +75,9 @@ export function formatShortDate(date: DateInput): string {
|
|||
}
|
||||
|
||||
// 1/10, 2:30 PM
|
||||
export function formatShortDateTime(date: DateInput): string {
|
||||
export function formatShortDateTime(date: DateInput, options: DateFormatOptions = {}): string {
|
||||
return formatValidDate(date, (validDate) =>
|
||||
Intl.DateTimeFormat(getLocales(), {
|
||||
getDateTimeFormat(options.locale, options.timeZone, {
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
|
|
@ -71,9 +87,9 @@ export function formatShortDateTime(date: DateInput): string {
|
|||
}
|
||||
|
||||
// 2:30 PM
|
||||
export function formatTime(date: DateInput): string {
|
||||
export function formatTime(date: DateInput, options: DateFormatOptions = {}): string {
|
||||
return formatValidDate(date, (validDate) =>
|
||||
Intl.DateTimeFormat(getLocales(), {
|
||||
getDateTimeFormat(options.locale, options.timeZone, {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
}).format(validDate),
|
||||
|
|
@ -91,3 +107,20 @@ export function formatTimeAgo(date: DateInput): string {
|
|||
return timeAgo.replace("about ", "").replace("over ", "").replace("almost ", "").replace("less than ", "");
|
||||
});
|
||||
}
|
||||
|
||||
export function useTimeFormat() {
|
||||
const { locale, timeZone } = RootRoute.useLoaderData();
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
formatDateTime: (date: DateInput) => formatDateTime(date, { locale, timeZone }),
|
||||
formatDateWithMonth: (date: DateInput) => formatDateWithMonth(date, { locale, timeZone }),
|
||||
formatDate: (date: DateInput) => formatDate(date, { locale, timeZone }),
|
||||
formatShortDate: (date: DateInput) => formatShortDate(date, { locale, timeZone }),
|
||||
formatShortDateTime: (date: DateInput) => formatShortDateTime(date, { locale, timeZone }),
|
||||
formatTime: (date: DateInput) => formatTime(date, { locale, timeZone }),
|
||||
formatTimeAgo,
|
||||
}),
|
||||
[locale, timeZone],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import { CalendarClock, Database, HardDrive } from "lucide-react";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { TimeAgo } from "~/client/components/time-ago";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||
import { useTimeFormat } from "~/client/lib/datetime";
|
||||
import type { BackupSchedule } from "~/client/lib/types";
|
||||
import { BackupStatusDot } from "./backup-status-dot";
|
||||
import { formatShortDateTime, formatTimeAgo } from "~/client/lib/datetime";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
export const BackupCard = ({ schedule }: { schedule: BackupSchedule }) => {
|
||||
const { formatShortDateTime } = useTimeFormat();
|
||||
|
||||
return (
|
||||
<Link key={schedule.shortId} to="/backups/$backupId" params={{ backupId: schedule.shortId }}>
|
||||
<Card interactive key={schedule.shortId} className="flex flex-col h-full">
|
||||
|
|
@ -42,7 +45,7 @@ export const BackupCard = ({ schedule }: { schedule: BackupSchedule }) => {
|
|||
<div className="flex items-center text-sm gap-2">
|
||||
<span className="text-muted-foreground shrink-0">Last backup</span>
|
||||
<div className="flex-1 border-b border-dashed border-border/80 dark:border-border/50" />
|
||||
<span className="text-foreground font-mono text-sm shrink-0">{formatTimeAgo(schedule.lastBackupAt)}</span>
|
||||
<TimeAgo date={schedule.lastBackupAt} className="text-foreground font-mono text-sm shrink-0" />
|
||||
</div>
|
||||
<div className="flex items-center text-sm gap-2">
|
||||
<span className="text-muted-foreground shrink-0">Next backup</span>
|
||||
|
|
|
|||
|
|
@ -16,11 +16,12 @@ import type { BackupSchedule } from "~/client/lib/types";
|
|||
import { BackupProgressCard } from "./backup-progress-card";
|
||||
import { getBackupProgressOptions, runForgetMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { toast } from "sonner";
|
||||
import { handleRepositoryError } from "~/client/lib/errors";
|
||||
import { formatShortDateTime, formatTimeAgo } from "~/client/lib/datetime";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "~/client/components/ui/collapsible";
|
||||
import { TimeAgo } from "~/client/components/time-ago";
|
||||
import { useTimeFormat } from "~/client/lib/datetime";
|
||||
import { cn } from "~/client/lib/utils";
|
||||
|
||||
type Props = {
|
||||
|
|
@ -35,6 +36,7 @@ type Props = {
|
|||
export const ScheduleSummary = (props: Props) => {
|
||||
const { schedule, handleToggleEnabled, handleRunBackupNow, handleStopBackup, handleDeleteSchedule, setIsEditMode } =
|
||||
props;
|
||||
const { formatShortDateTime } = useTimeFormat();
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [showForgetConfirm, setShowForgetConfirm] = useState(false);
|
||||
const [showStopConfirm, setShowStopConfirm] = useState(false);
|
||||
|
|
@ -182,7 +184,7 @@ export const ScheduleSummary = (props: Props) => {
|
|||
</div>
|
||||
<div>
|
||||
<p className="text-xs uppercase text-muted-foreground">Last backup</p>
|
||||
<p className="font-medium">{formatTimeAgo(schedule.lastBackupAt)}</p>
|
||||
<TimeAgo date={schedule.lastBackupAt} className="font-medium" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs uppercase text-muted-foreground">Next backup</p>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { useQuery } from "@tanstack/react-query";
|
|||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||
import { Button, buttonVariants } from "~/client/components/ui/button";
|
||||
import type { Snapshot } from "~/client/lib/types";
|
||||
import { formatDateTime } from "~/client/lib/datetime";
|
||||
import { useTimeFormat } from "~/client/lib/datetime";
|
||||
import { cn } from "~/client/lib/utils";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { SnapshotTreeBrowser } from "~/client/components/file-browsers/snapshot-tree-browser";
|
||||
|
|
@ -62,6 +62,7 @@ const TreeBrowserFallback = () => (
|
|||
|
||||
export const SnapshotFileBrowser = (props: Props) => {
|
||||
const { snapshot, repositoryId, backupId, basePath, onDeleteSnapshot, isDeletingSnapshot } = props;
|
||||
const { formatDateTime } = useTimeFormat();
|
||||
|
||||
const scheduleShortId = !basePath ? backupId || snapshot.tags?.[0] : undefined;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { ListSnapshotsResponse } from "~/client/api-client";
|
|||
import { ByteSize } from "~/client/components/bytes-size";
|
||||
import { Card, CardContent } from "~/client/components/ui/card";
|
||||
import { Button } from "~/client/components/ui/button";
|
||||
import { formatDateWithMonth, formatShortDate, formatTime } from "~/client/lib/datetime";
|
||||
import { useTimeFormat } from "~/client/lib/datetime";
|
||||
import { cn } from "~/client/lib/utils";
|
||||
import { RetentionCategoryBadges } from "~/client/components/retention-category-badges";
|
||||
|
||||
|
|
@ -45,6 +45,7 @@ interface Props {
|
|||
export const SnapshotTimeline = (props: Props) => {
|
||||
const { snapshots, snapshotId, loading, onSnapshotSelect, error, initialSortOrder = "asc" } = props;
|
||||
const selectedRef = useRef<HTMLButtonElement>(null);
|
||||
const { formatDateWithMonth, formatShortDate, formatTime } = useTimeFormat();
|
||||
const [sortOrder, setSortOrder] = useState<SnapshotTimelineSortOrder>(initialSortOrder);
|
||||
const sortedSnapshots = useMemo(() => getSortedSnapshots(snapshots, sortOrder), [snapshots, sortOrder]);
|
||||
const snapshotRange = useMemo(() => getSnapshotRange(snapshots), [snapshots]);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { AlertCircle, CheckCircle2 } from "lucide-react";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "~/client/components/ui/collapsible";
|
||||
import { formatDateTime } from "~/client/lib/datetime";
|
||||
import { useTimeFormat } from "~/client/lib/datetime";
|
||||
import { cn, safeJsonParse } from "~/client/lib/utils";
|
||||
import { Card, CardTitle } from "~/client/components/ui/card";
|
||||
|
||||
|
|
@ -23,6 +23,8 @@ type Props = {
|
|||
};
|
||||
|
||||
export const DoctorReport = ({ result, repositoryStatus }: Props) => {
|
||||
const { formatDateTime } = useTimeFormat();
|
||||
|
||||
return (
|
||||
<Card className="px-6 py-6 flex flex-col gap-4 h-full">
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import type { GetSnapshotDetailsResponse } from "~/client/api-client/types.gen";
|
|||
import { Button } from "~/client/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||
import { SnapshotFileBrowser } from "~/client/modules/backups/components/snapshot-file-browser";
|
||||
import { formatDateTime } from "~/client/lib/datetime";
|
||||
import { useTimeFormat } from "~/client/lib/datetime";
|
||||
import { BackupSummaryCard } from "~/client/components/backup-summary-card";
|
||||
import { useState } from "react";
|
||||
import { Database } from "lucide-react";
|
||||
|
|
@ -58,6 +58,7 @@ type Props = {
|
|||
|
||||
export function SnapshotDetailsPage({ repositoryId, snapshotId, initialSnapshot }: Props) {
|
||||
const [showAllPaths, setShowAllPaths] = useState(false);
|
||||
const { formatDateTime } = useTimeFormat();
|
||||
|
||||
const { data: repository } = useSuspenseQuery({
|
||||
...getRepositoryOptions({ path: { shortId: repositoryId } }),
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import {
|
|||
} from "~/client/components/ui/alert-dialog";
|
||||
import type { Repository } from "~/client/lib/types";
|
||||
import type { GetRepositoryStatsResponse } from "~/client/api-client/types.gen";
|
||||
import { formatDateTime, formatTimeAgo } from "~/client/lib/datetime";
|
||||
import {
|
||||
cancelDoctorMutation,
|
||||
deleteRepositoryMutation,
|
||||
|
|
@ -23,6 +22,8 @@ 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";
|
||||
|
|
@ -41,6 +42,7 @@ const getEffectiveLocalPath = (repository: Repository): string | null => {
|
|||
};
|
||||
|
||||
export const RepositoryInfoTabContent = ({ repository, initialStats }: Props) => {
|
||||
const { formatDateTime } = useTimeFormat();
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
|
@ -206,7 +208,7 @@ export const RepositoryInfoTabContent = ({ repository, initialStats }: Props) =>
|
|||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-sm font-medium text-muted-foreground">Last Checked</div>
|
||||
<p className="text-sm">{formatTimeAgo(repository.lastChecked)}</p>
|
||||
<TimeAgo date={repository.lastChecked} className="text-sm" />
|
||||
</div>
|
||||
{hasLocalPath && (
|
||||
<div className="flex flex-col gap-1 @medium:col-span-2">
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~
|
|||
import { Switch } from "~/client/components/ui/switch";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
|
||||
import { useOrganizationContext } from "~/client/hooks/use-org-context";
|
||||
import { formatDateWithMonth } from "~/client/lib/datetime";
|
||||
import { useTimeFormat } from "~/client/lib/datetime";
|
||||
import { getOrigin } from "~/client/functions/get-origin";
|
||||
import { authClient } from "~/client/lib/auth-client";
|
||||
import { cn } from "~/client/lib/utils";
|
||||
|
|
@ -47,6 +47,7 @@ export function SsoSettingsSection({ initialSettings, initialOrigin }: Props) {
|
|||
const { data } = useSuspenseQuery({ queryKey: ["app-origin"], queryFn: originQuery, initialData: initialOrigin });
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { formatDateWithMonth } = useTimeFormat();
|
||||
const { activeOrganization } = useOrganizationContext();
|
||||
const [inviteEmail, setInviteEmail] = useState("");
|
||||
const [inviteRole, setInviteRole] = useState<InvitationRole>("member");
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import { healthCheckVolumeMutation, updateVolumeMutation } from "~/client/api-cl
|
|||
import { OnOff } from "~/client/components/onoff";
|
||||
import { Button } from "~/client/components/ui/button";
|
||||
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 { TimeAgo } from "~/client/components/time-ago";
|
||||
|
||||
type Props = {
|
||||
volume: Volume;
|
||||
|
|
@ -53,9 +53,11 @@ export const HealthchecksCard = ({ volume }: Props) => {
|
|||
{volume.lastError && <span className="text-sm text-red-500 wrap-break-word">{volume.lastError}</span>}
|
||||
{volume.status === "mounted" && <span className="text-md text-success">Healthy</span>}
|
||||
{volume.status !== "unmounted" && (
|
||||
<span className="text-xs text-muted-foreground mb-4">Checked {formatTimeAgo(volume.lastHealthCheck)}</span>
|
||||
<span className="text-xs text-muted-foreground mb-4">
|
||||
Checked <TimeAgo date={volume.lastHealthCheck} />
|
||||
</span>
|
||||
)}
|
||||
<span className="flex justify-between items-center gap-2">
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<span className="text-sm">Remount on error</span>
|
||||
<OnOff
|
||||
isOn={volume.autoRemount}
|
||||
|
|
@ -69,7 +71,7 @@ export const HealthchecksCard = ({ volume }: Props) => {
|
|||
enabledLabel="Enabled"
|
||||
disabledLabel="Paused"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{volume.status !== "unmounted" && (
|
||||
<div className="flex justify-center">
|
||||
|
|
|
|||
|
|
@ -81,9 +81,9 @@ export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
|
|||
|
||||
const deleteVol = useMutation({
|
||||
...deleteVolumeMutation(),
|
||||
onSuccess: () => {
|
||||
onSuccess: async () => {
|
||||
toast.success("Volume deleted successfully");
|
||||
void navigate({ to: "/volumes" });
|
||||
await navigate({ to: "/volumes" });
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to delete volume", {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { useServerEvents } from "~/client/hooks/use-server-events";
|
|||
import { useEffect } from "react";
|
||||
import { ThemeProvider, THEME_COOKIE_NAME } from "~/client/components/theme-provider";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
import { getCookie } from "@tanstack/react-start/server";
|
||||
import { getCookie, getRequestHeaders } from "@tanstack/react-start/server";
|
||||
import { isAuthRoute } from "~/lib/auth-routes";
|
||||
|
||||
const fetchTheme = createServerFn({ method: "GET" }).handler(async () => {
|
||||
|
|
@ -16,13 +16,22 @@ const fetchTheme = createServerFn({ method: "GET" }).handler(async () => {
|
|||
return (themeCookie === "light" ? "light" : "dark") as "light" | "dark";
|
||||
});
|
||||
|
||||
const fetchTimeConfig = createServerFn({ method: "GET" }).handler(async () => {
|
||||
const acceptLanguage = getRequestHeaders().get("accept-language");
|
||||
|
||||
return {
|
||||
locale: (acceptLanguage?.split(",")[0] || "en-US") as string,
|
||||
timeZone: process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
|
||||
};
|
||||
});
|
||||
|
||||
export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
|
||||
server: {
|
||||
middleware: [apiClientMiddleware],
|
||||
},
|
||||
loader: async () => {
|
||||
const theme = await fetchTheme();
|
||||
return { theme };
|
||||
const [theme, timeConfig] = await Promise.all([fetchTheme(), fetchTimeConfig()]);
|
||||
return { theme, ...timeConfig };
|
||||
},
|
||||
head: () => ({
|
||||
meta: [{ title: "Zerobyte - Open Source Backup Solution" }],
|
||||
|
|
|
|||
Loading…
Reference in a new issue