Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/client/api-client/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3311,7 +3311,7 @@ export type GetScheduleMirrorsResponses = {
enabled: boolean;
lastCopyAt: number | null;
lastCopyError: string | null;
lastCopyStatus: "error" | "success" | null;
lastCopyStatus: "error" | "in_progress" | "success" | null;
repository: {
compressionMode: "auto" | "max" | "off" | null;
config:
Expand Down Expand Up @@ -3531,7 +3531,7 @@ export type UpdateScheduleMirrorsResponses = {
enabled: boolean;
lastCopyAt: number | null;
lastCopyError: string | null;
lastCopyStatus: "error" | "success" | null;
lastCopyStatus: "error" | "in_progress" | "success" | null;
repository: {
compressionMode: "auto" | "max" | "off" | null;
config:
Expand Down
8 changes: 4 additions & 4 deletions app/client/hooks/use-server-events.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef } from "react";
import { useCallback, useEffect, useRef } from "react";
import { useQueryClient } from "@tanstack/react-query";
import type {
BackupCompletedEventDto,
Expand Down Expand Up @@ -29,7 +29,7 @@ export interface MirrorEvent {
scheduleId: number;
repositoryId: string;
repositoryName: string;
status?: "success" | "error";
status?: "success" | "error" | "in_progress";
error?: string;
}

Expand Down Expand Up @@ -205,7 +205,7 @@ export function useServerEvents() {
};
}, [queryClient]);

const addEventListener = (event: ServerEventType, handler: EventHandler) => {
const addEventListener = useCallback((event: ServerEventType, handler: EventHandler) => {
if (!handlersRef.current.has(event)) {
handlersRef.current.set(event, new Set());
}
Expand All @@ -214,7 +214,7 @@ export function useServerEvents() {
return () => {
handlersRef.current.get(event)?.delete(handler);
};
};
}, []);

return { addEventListener };
}
121 changes: 79 additions & 42 deletions app/client/modules/backups/components/schedule-mirrors-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { parseError } from "~/client/lib/errors";
import type { Repository } from "~/client/lib/types";
import { RepositoryIcon } from "~/client/components/repository-icon";
import { StatusDot } from "~/client/components/status-dot";
import { useServerEvents } from "~/client/hooks/use-server-events";
import { formatDistanceToNow } from "date-fns";
import { cn } from "~/client/lib/utils";
import type { GetScheduleMirrorsResponse } from "~/client/api-client";
Expand All @@ -34,28 +35,31 @@ type MirrorAssignment = {
repositoryId: string;
enabled: boolean;
lastCopyAt: number | null;
lastCopyStatus: "success" | "error" | null;
lastCopyStatus: "success" | "error" | "in_progress" | null;
lastCopyError: string | null;
};

const buildAssignments = (mirrors: GetScheduleMirrorsResponse) => {
const map = new Map<string, MirrorAssignment>();
for (const mirror of mirrors) {
map.set(mirror.repositoryId, {
repositoryId: mirror.repositoryId,
enabled: mirror.enabled,
lastCopyAt: mirror.lastCopyAt,
lastCopyStatus: mirror.lastCopyStatus,
lastCopyError: mirror.lastCopyError,
});
}
return map;
};
const isSyncing = (assignment: MirrorAssignment) => assignment.lastCopyStatus === "in_progress";

const buildAssignments = (mirrors: GetScheduleMirrorsResponse) =>
new Map<string, MirrorAssignment>(
mirrors.map((mirror) => [
mirror.repositoryId,
{
repositoryId: mirror.repositoryId,
enabled: mirror.enabled,
lastCopyAt: mirror.lastCopyAt,
lastCopyStatus: mirror.lastCopyStatus,
lastCopyError: mirror.lastCopyError,
},
]),
);

export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, repositories, initialData }: Props) => {
const [assignments, setAssignments] = useState<Map<string, MirrorAssignment>>(() => buildAssignments(initialData));
const [hasChanges, setHasChanges] = useState(false);
const [isAddingNew, setIsAddingNew] = useState(false);
const { addEventListener } = useServerEvents();

const { data: currentMirrors } = useSuspenseQuery({
...getScheduleMirrorsOptions({ path: { scheduleId: scheduleId.toString() } }),
Expand Down Expand Up @@ -94,6 +98,42 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit
return map;
}, [compatibility]);

useEffect(() => {
const unsubscribeStarted = addEventListener("mirror:started", (data) => {
const event = data as { scheduleId: number; repositoryId: string };
if (event.scheduleId !== scheduleId) return;
setAssignments((prev) => {
const next = new Map(prev);
const existing = next.get(event.repositoryId);
if (!existing) return prev;
next.set(event.repositoryId, { ...existing, lastCopyStatus: "in_progress", lastCopyError: null });
return next;
});
});

const unsubscribeCompleted = addEventListener("mirror:completed", (data) => {
const event = data as { scheduleId: number; repositoryId: string; status?: "success" | "error"; error?: string };
if (event.scheduleId !== scheduleId) return;
setAssignments((prev) => {
const next = new Map(prev);
const existing = next.get(event.repositoryId);
if (!existing) return prev;
next.set(event.repositoryId, {
...existing,
lastCopyStatus: event.status ?? existing.lastCopyStatus,
lastCopyError: event.error ?? null,
lastCopyAt: Date.now(),
});
return next;
});
});

return () => {
unsubscribeStarted();
unsubscribeCompleted();
};
}, [addEventListener, scheduleId]);

const addRepository = (repositoryId: string) => {
const newAssignments = new Map(assignments);
newAssignments.set(repositoryId, {
Expand Down Expand Up @@ -144,20 +184,8 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit
};

const handleReset = () => {
if (currentMirrors) {
const map = new Map<string, MirrorAssignment>();
for (const mirror of currentMirrors) {
map.set(mirror.repositoryId, {
repositoryId: mirror.repositoryId,
enabled: mirror.enabled,
lastCopyAt: mirror.lastCopyAt,
lastCopyStatus: mirror.lastCopyStatus,
lastCopyError: mirror.lastCopyError,
});
}
setAssignments(map);
setHasChanges(false);
}
setAssignments(buildAssignments(currentMirrors));
setHasChanges(false);
};

const selectableRepositories =
Expand All @@ -176,13 +204,27 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit
.map((id) => repositories?.find((r) => r.id === id))
.filter((r) => r !== undefined);

const getStatusVariant = (status: "success" | "error" | null) => {
const getStatusVariant = (status: string | null) => {
if (status === "success") return "success";
if (status === "error") return "error";
if (status === "in_progress") return "info";
return "neutral";
};

const getLabel = (assignment: MirrorAssignment) => {
if (isSyncing(assignment)) {
return "Syncing...";
}
if (assignment.lastCopyAt) {
return formatDistanceToNow(new Date(assignment.lastCopyAt), { addSuffix: true });
}
return "Never";
};

const getStatusLabel = (assignment: MirrorAssignment) => {
if (isSyncing(assignment)) {
return "Mirror sync in progress";
}
if (assignment.lastCopyStatus === "error" && assignment.lastCopyError) {
return assignment.lastCopyError;
}
Expand Down Expand Up @@ -310,19 +352,14 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit
/>
</TableCell>
<TableCell>
{assignment.lastCopyAt ? (
<div className="flex items-center gap-2">
<StatusDot
variant={getStatusVariant(assignment.lastCopyStatus)}
label={getStatusLabel(assignment)}
/>
<span className="text-sm text-muted-foreground">
{formatDistanceToNow(new Date(assignment.lastCopyAt), { addSuffix: true })}
</span>
</div>
) : (
<span className="text-sm text-muted-foreground">Never</span>
)}
<div className="flex items-center gap-2">
<StatusDot
variant={getStatusVariant(assignment.lastCopyStatus)}
label={getStatusLabel(assignment)}
animated={isSyncing(assignment)}
/>
<span className="text-sm text-muted-foreground">{getLabel(assignment)}</span>
</div>
</TableCell>
<TableCell>
<Button
Expand Down
7 changes: 0 additions & 7 deletions app/client/modules/backups/components/snapshot-timeline.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useEffect } from "react";
import type { ListSnapshotsResponse } from "~/client/api-client";
import { ByteSize } from "~/client/components/bytes-size";
import { Card, CardContent } from "~/client/components/ui/card";
Expand All @@ -17,12 +16,6 @@ interface Props {
export const SnapshotTimeline = (props: Props) => {
const { snapshots, snapshotId, loading, onSnapshotSelect, error } = props;

useEffect(() => {
if (!snapshotId && snapshots.length > 0) {
onSnapshotSelect(snapshots[snapshots.length - 1].short_id);
}
}, [snapshotId, snapshots, onSnapshotSelect]);

if (error) {
return (
<Card>
Expand Down
36 changes: 25 additions & 11 deletions app/client/modules/backups/routes/backups.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from "@dnd-kit/core";
import { arrayMove, SortableContext, sortableKeyboardCoordinates, rectSortingStrategy } from "@dnd-kit/sortable";
import { CalendarClock, Plus } from "lucide-react";
import { useState, useEffect } from "react";
import { useState } from "react";
import { EmptyState } from "~/client/components/empty-state";
import { Button } from "~/client/components/ui/button";
import { Card, CardContent } from "~/client/components/ui/card";
Expand All @@ -27,12 +27,8 @@ export function BackupsPage() {
...listBackupSchedulesOptions(),
});

const [items, setItems] = useState(schedules?.map((s) => s.id) ?? []);
useEffect(() => {
if (schedules) {
setItems(schedules.map((s) => s.id));
}
}, [schedules]);
const [localItems, setLocalItems] = useState<number[] | null>(null);
const items = localItems ?? schedules?.map((s) => s.id) ?? [];

const sensors = useSensors(
useSensor(PointerSensor, {
Expand All @@ -51,10 +47,28 @@ export function BackupsPage() {
const { active, over } = event;

if (over && active.id !== over.id) {
setItems((items) => {
const oldIndex = items.indexOf(active.id as number);
const newIndex = items.indexOf(over.id as number);
const newItems = arrayMove(items, oldIndex, newIndex);
setLocalItems((currentItems) => {
const baseItems = currentItems ?? schedules?.map((s) => s.id) ?? [];
const activeId = active.id as number;
const overId = over.id as number;
let oldIndex = baseItems.indexOf(activeId);
let newIndex = baseItems.indexOf(overId);

if (oldIndex === -1 || newIndex === -1) {
const freshItems = schedules?.map((s) => s.id) ?? [];
oldIndex = freshItems.indexOf(activeId);
newIndex = freshItems.indexOf(overId);

if (oldIndex === -1 || newIndex === -1) {
return currentItems;
}

const newItems = arrayMove(freshItems, oldIndex, newIndex);
reorderMutation.mutate({ body: { scheduleIds: newItems } });
return newItems;
}

const newItems = arrayMove(baseItems, oldIndex, newIndex);
reorderMutation.mutate({ body: { scheduleIds: newItems } });

return newItems;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { arktypeResolver } from "@hookform/resolvers/arktype";
import { type } from "arktype";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { cn } from "~/client/lib/utils";
import { deepClean } from "~/utils/object";
Expand Down Expand Up @@ -119,15 +118,6 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
const { watch } = form;
const watchedType = watch("type");

useEffect(() => {
if (!initialValues) {
form.reset({
name: form.getValues().name || "",
...defaultValuesForType[watchedType as keyof typeof defaultValuesForType],
});
}
}, [watchedType, form, initialValues]);

return (
<Form {...form}>
<form id={formId} onSubmit={form.handleSubmit(onSubmit)} className={cn("space-y-4", className)}>
Expand All @@ -138,12 +128,7 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
{...field}
placeholder="My notification"
max={32}
min={2}
/>
<Input {...field} placeholder="My notification" max={32} min={2} />
</FormControl>
<FormDescription>Unique identifier for this notification destination.</FormDescription>
<FormMessage />
Expand All @@ -158,7 +143,15 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
<FormItem>
<FormLabel>Type</FormLabel>
<Select
onValueChange={field.onChange}
onValueChange={(value) => {
field.onChange(value);
if (!initialValues) {
form.reset({
name: form.getValues().name || "",
...defaultValuesForType[value as keyof typeof defaultValuesForType],
});
}
}}
value={field.value}
disabled={mode === "update"}
>
Expand Down
Loading