feat(admin): server-safe StatusCard and Import hub polish
Split OnlineUsersWidget from StatusCard, decouple ad delete button, sync badge import to ExternalTexts+WebsiteBadges, add Import section hub with cancelable SSE jobs and upload SQL option. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
db957d7fb1
commit
9c4949186c
21 files changed
+576
-249
No files matched your search
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Check, CloudDownload, Loader2 } from "lucide-react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { adminFetch } from "@/lib/admin-fetch";
|
||||
@@ -26,6 +26,7 @@ export function SyncAllClient() {
|
||||
failed: number;
|
||||
sources: number;
|
||||
} | null>(null);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const startSync = useCallback(async () => {
|
||||
setSyncing(true);
|
||||
@@ -33,11 +34,15 @@ export function SyncAllClient() {
|
||||
setLog([]);
|
||||
setResult(null);
|
||||
|
||||
const abort = new AbortController();
|
||||
abortRef.current = abort;
|
||||
|
||||
try {
|
||||
const res = await adminFetch("/api/admin/import/clone/sync-all", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
signal: abort.signal,
|
||||
});
|
||||
|
||||
if (!res.body) {
|
||||
@@ -117,26 +122,42 @@ export function SyncAllClient() {
|
||||
setResult({ succeeded, failed, sources: 1 });
|
||||
if (succeeded > 0) toast.success(`${succeeded} items synced`);
|
||||
if (failed > 0) toast.error(`${failed} items failed`);
|
||||
} catch {
|
||||
toast.error("Sync request failed");
|
||||
} catch (err) {
|
||||
if ((err as Error).name === "AbortError") {
|
||||
toast.info("Sync cancelled");
|
||||
} else {
|
||||
toast.error("Sync request failed");
|
||||
}
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
abortRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{!result && (
|
||||
<Button onClick={startSync} disabled={syncing} size="lg">
|
||||
{syncing ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
||||
) : (
|
||||
<CloudDownload className="w-4 h-4 mr-2" />
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Button onClick={startSync} disabled={syncing} size="lg">
|
||||
{syncing ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
||||
) : (
|
||||
<CloudDownload className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{syncing
|
||||
? `Syncing… ${progress ? `${progress.done}/${progress.total}` : ""}`
|
||||
: "Start Sync All Sources"}
|
||||
</Button>
|
||||
{syncing && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={() => abortRef.current?.abort()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
{syncing
|
||||
? `Syncing… ${progress ? `${progress.done}/${progress.total}` : ""}`
|
||||
: "Start Sync All Sources"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{progress && (
|
||||
|
||||
Reference in new issue
Block a user