mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-05 08:06:19 +00:00
fix few issues
This commit is contained in:
parent
f93d3f0c27
commit
e52e43d073
6 changed files with 393 additions and 250 deletions
|
|
@ -33,7 +33,7 @@ import {
|
|||
} from "@ui/components/dropdown-menu"
|
||||
import { RemoveConnectionDialog } from "@/components/remove-connection-dialog"
|
||||
import { SyncStatusBadge } from "@/components/settings/sync-status-badge"
|
||||
import { SyncHistorySheet } from "@/components/settings/sync-history-sheet"
|
||||
import { SyncHistoryPanel } from "@/components/settings/sync-history-panel"
|
||||
import { useTriggerSync } from "@/hooks/use-trigger-sync"
|
||||
import { formatRelativeTime } from "@/components/settings/sync-utils"
|
||||
import type { ImportProvider } from "@/components/settings/sync-utils"
|
||||
|
|
@ -101,7 +101,6 @@ function ConnectionRow({
|
|||
projects,
|
||||
onTriggerSync,
|
||||
isSyncing,
|
||||
onViewHistory,
|
||||
}: {
|
||||
connection: Connection
|
||||
onDelete: () => void
|
||||
|
|
@ -109,8 +108,8 @@ function ConnectionRow({
|
|||
projects: Project[]
|
||||
onTriggerSync: () => void
|
||||
isSyncing: boolean
|
||||
onViewHistory: () => void
|
||||
}) {
|
||||
const [historyOpen, setHistoryOpen] = useState(false)
|
||||
const config = CONNECTORS[connection.provider as ConnectorProvider]
|
||||
if (!config) return null
|
||||
|
||||
|
|
@ -196,13 +195,25 @@ function ConnectionRow({
|
|||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onViewHistory()
|
||||
setHistoryOpen((v) => !v)
|
||||
}}
|
||||
className="text-[#737373] hover:text-[#FAFAFA] transition-colors disabled:opacity-50 disabled:cursor-not-allowed p-1.5 rounded-lg hover:bg-white/5"
|
||||
aria-label="Sync history"
|
||||
title="Sync history"
|
||||
aria-expanded={historyOpen}
|
||||
title={historyOpen ? "Hide sync history" : "Sync history"}
|
||||
className={cn(
|
||||
"transition-colors disabled:opacity-50 disabled:cursor-not-allowed p-1.5 rounded-lg hover:bg-white/5 flex items-center gap-0.5",
|
||||
historyOpen
|
||||
? "text-[#FAFAFA] bg-white/5"
|
||||
: "text-[#737373] hover:text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
<History className="size-[18px]" />
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"size-3 transition-transform",
|
||||
historyOpen && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -253,6 +264,15 @@ function ConnectionRow({
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{historyOpen && (
|
||||
<div className="border-t border-[rgba(82,89,102,0.12)] pt-3">
|
||||
<SyncHistoryPanel
|
||||
connectionId={connection.id}
|
||||
isOpen={historyOpen}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -276,10 +296,6 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
|
|||
connection: Connection | null
|
||||
}>({ open: false, connection: null })
|
||||
const triggerSync = useTriggerSync()
|
||||
const [syncHistorySheet, setSyncHistorySheet] = useState<{
|
||||
open: boolean
|
||||
connection: Connection | null
|
||||
}>({ open: false, connection: null })
|
||||
|
||||
const projects = (queryClient.getQueryData<Project[]>(["projects"]) ||
|
||||
[]) as Project[]
|
||||
|
|
@ -706,9 +722,6 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
|
|||
triggerSync.variables?.connectionId === connection.id) ||
|
||||
getConnectionMeta(connection).syncInProgress
|
||||
}
|
||||
onViewHistory={() =>
|
||||
setSyncHistorySheet({ open: true, connection })
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -799,13 +812,6 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
|
|||
}}
|
||||
isDeleting={deleteConnectionMutation.isPending}
|
||||
/>
|
||||
<SyncHistorySheet
|
||||
open={syncHistorySheet.open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setSyncHistorySheet({ open: false, connection: null })
|
||||
}}
|
||||
connection={syncHistorySheet.connection}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,16 @@ import { hasActivePlan } from "@lib/queries"
|
|||
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
|
||||
import { useCustomer } from "autumn-js/react"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { Check, History, Loader2, Play, Plus, Trash2, Zap } from "lucide-react"
|
||||
import {
|
||||
Check,
|
||||
ChevronDown,
|
||||
History,
|
||||
Loader2,
|
||||
Play,
|
||||
Plus,
|
||||
Trash2,
|
||||
Zap,
|
||||
} from "lucide-react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { useQueryState } from "nuqs"
|
||||
|
|
@ -21,7 +30,7 @@ import { addDocumentParam } from "@/lib/search-params"
|
|||
import { DEFAULT_PROJECT_ID } from "@lib/constants"
|
||||
import type { Project } from "@lib/types"
|
||||
import { SyncStatusBadge } from "@/components/settings/sync-status-badge"
|
||||
import { SyncHistorySheet } from "@/components/settings/sync-history-sheet"
|
||||
import { SyncHistoryPanel } from "@/components/settings/sync-history-panel"
|
||||
import { useTriggerSync } from "@/hooks/use-trigger-sync"
|
||||
import { formatRelativeTime } from "@/components/settings/sync-utils"
|
||||
import type { ImportProvider } from "@/components/settings/sync-utils"
|
||||
|
|
@ -157,7 +166,6 @@ function ConnectionRow({
|
|||
projects,
|
||||
onTriggerSync,
|
||||
isSyncing,
|
||||
onViewHistory,
|
||||
}: {
|
||||
connection: Connection
|
||||
onDelete: () => void
|
||||
|
|
@ -166,8 +174,8 @@ function ConnectionRow({
|
|||
projects: Project[]
|
||||
onTriggerSync: () => void
|
||||
isSyncing: boolean
|
||||
onViewHistory: () => void
|
||||
}) {
|
||||
const [historyOpen, setHistoryOpen] = useState(false)
|
||||
const config = CONNECTORS[connection.provider as ConnectorProvider]
|
||||
if (!config) return null
|
||||
|
||||
|
|
@ -259,14 +267,26 @@ function ConnectionRow({
|
|||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onViewHistory()
|
||||
setHistoryOpen((v) => !v)
|
||||
}}
|
||||
disabled={disabled}
|
||||
className="text-[#737373] hover:text-[#FAFAFA] transition-colors disabled:opacity-50 disabled:cursor-not-allowed p-1.5 rounded-lg hover:bg-white/5"
|
||||
aria-label="Sync history"
|
||||
title="Sync history"
|
||||
aria-expanded={historyOpen}
|
||||
title={historyOpen ? "Hide sync history" : "Sync history"}
|
||||
className={cn(
|
||||
"transition-colors disabled:opacity-50 disabled:cursor-not-allowed p-1.5 rounded-lg hover:bg-white/5 flex items-center gap-0.5",
|
||||
historyOpen
|
||||
? "text-[#FAFAFA] bg-white/5"
|
||||
: "text-[#737373] hover:text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
<History className="size-[18px]" />
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"size-3 transition-transform",
|
||||
historyOpen && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -314,6 +334,15 @@ function ConnectionRow({
|
|||
{meta.documentCount} {config.documentLabel} connected
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{historyOpen && (
|
||||
<div className="border-t border-[rgba(82,89,102,0.15)] pt-4">
|
||||
<SyncHistoryPanel
|
||||
connectionId={connection.id}
|
||||
isOpen={historyOpen}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -378,10 +407,6 @@ export default function ConnectionsMCP() {
|
|||
connection: Connection | null
|
||||
}>({ open: false, connection: null })
|
||||
const triggerSync = useTriggerSync()
|
||||
const [syncHistorySheet, setSyncHistorySheet] = useState<{
|
||||
open: boolean
|
||||
connection: Connection | null
|
||||
}>({ open: false, connection: null })
|
||||
|
||||
const projects = (queryClient.getQueryData<Project[]>(["projects"]) ||
|
||||
[]) as Project[]
|
||||
|
|
@ -555,9 +580,6 @@ export default function ConnectionsMCP() {
|
|||
connection.id) ||
|
||||
getConnectionMeta(connection).syncInProgress
|
||||
}
|
||||
onViewHistory={() =>
|
||||
setSyncHistorySheet({ open: true, connection })
|
||||
}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
|
|
@ -658,14 +680,6 @@ export default function ConnectionsMCP() {
|
|||
}}
|
||||
isDeleting={deleteConnectionMutation.isPending}
|
||||
/>
|
||||
|
||||
<SyncHistorySheet
|
||||
open={syncHistorySheet.open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setSyncHistorySheet({ open: false, connection: null })
|
||||
}}
|
||||
connection={syncHistorySheet.connection}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
267
apps/web/components/settings/sync-history-panel.tsx
Normal file
267
apps/web/components/settings/sync-history-panel.tsx
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
"use client"
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import { dmSans125ClassName } from "@/lib/fonts"
|
||||
import { useSyncRuns } from "@/hooks/use-sync-runs"
|
||||
import type { SyncRun } from "@/hooks/use-sync-runs"
|
||||
import {
|
||||
formatRelativeTime,
|
||||
TRIGGER_TYPE_LABELS,
|
||||
} from "@/components/settings/sync-utils"
|
||||
|
||||
const STATUS_COLORS: Record<string, { dot: string; text: string }> = {
|
||||
completed: { dot: "bg-[#00AC3F]", text: "text-[#00AC3F]" },
|
||||
failed: { dot: "bg-[#EF4444]", text: "text-[#EF4444]" },
|
||||
running: { dot: "bg-[#4BA0FA] animate-pulse", text: "text-[#4BA0FA]" },
|
||||
}
|
||||
|
||||
function pluralize(count: number, noun: string) {
|
||||
return `${count} ${noun}${count === 1 ? "" : "s"}`
|
||||
}
|
||||
|
||||
/** Calendar-day bucket label for grouping runs in the timeline. */
|
||||
function dayLabel(date: string) {
|
||||
const d = new Date(date)
|
||||
if (Number.isNaN(d.getTime())) return "Unknown"
|
||||
const today = new Date()
|
||||
const startOfDay = (x: Date) =>
|
||||
new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime()
|
||||
const diffDays = Math.round((startOfDay(today) - startOfDay(d)) / 86_400_000)
|
||||
if (diffDays <= 0) return "Today"
|
||||
if (diffDays === 1) return "Yesterday"
|
||||
if (diffDays < 7) return `${diffDays} days ago`
|
||||
return d.toLocaleDateString(undefined, { month: "short", day: "numeric" })
|
||||
}
|
||||
|
||||
function StatTile({ value, label }: { value: string; label: string }) {
|
||||
return (
|
||||
<div className="flex-1 bg-[#0D0F14] rounded-[10px] border border-[rgba(82,89,102,0.2)] px-3 py-2 flex flex-col gap-0.5">
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[15px] font-medium text-[#FAFAFA] tabular-nums",
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
<span className={cn(dmSans125ClassName(), "text-[11px] text-[#737373]")}>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SummaryStats({ runs }: { runs: SyncRun[] }) {
|
||||
const totalItems = runs.reduce((sum, r) => sum + r.itemsProcessed, 0)
|
||||
const finished = runs.filter((r) => r.status !== "running")
|
||||
const succeeded = finished.filter((r) => r.status === "completed").length
|
||||
const successRate =
|
||||
finished.length > 0 ? Math.round((succeeded / finished.length) * 100) : null
|
||||
|
||||
return (
|
||||
<div className="flex items-stretch gap-2">
|
||||
<StatTile
|
||||
value={String(runs.length)}
|
||||
label={runs.length === 1 ? "sync" : "syncs"}
|
||||
/>
|
||||
<StatTile
|
||||
value={String(totalItems)}
|
||||
label={totalItems === 1 ? "item" : "items"}
|
||||
/>
|
||||
<StatTile
|
||||
value={successRate === null ? "—" : `${successRate}%`}
|
||||
label="success"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TimelineRow({ run, isLast }: { run: SyncRun; isLast: boolean }) {
|
||||
const colors = STATUS_COLORS[run.status] ?? {
|
||||
dot: "bg-[#4BA0FA] animate-pulse",
|
||||
text: "text-[#4BA0FA]",
|
||||
}
|
||||
const triggerLabel = TRIGGER_TYPE_LABELS[run.triggerType] ?? run.triggerType
|
||||
const statusLabel = run.status.charAt(0).toUpperCase() + run.status.slice(1)
|
||||
|
||||
return (
|
||||
<div className="flex gap-2.5">
|
||||
{/* Timeline rail: dot + connecting line */}
|
||||
<div className="flex flex-col items-center pt-1.5">
|
||||
<div className={cn("size-[7px] rounded-full shrink-0", colors.dot)} />
|
||||
{!isLast && <div className="w-px flex-1 bg-[#1A1D24] mt-1" />}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className={cn("flex-1 min-w-0", isLast ? "pb-0" : "pb-3")}>
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<div className="flex items-baseline gap-1.5 min-w-0">
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[13px] font-medium shrink-0",
|
||||
colors.text,
|
||||
)}
|
||||
>
|
||||
{statusLabel}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[12px] text-[#737373] truncate",
|
||||
)}
|
||||
>
|
||||
· {triggerLabel}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[12px] text-[#737373] shrink-0",
|
||||
)}
|
||||
>
|
||||
{formatRelativeTime(run.startedAt)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{(run.itemsProcessed > 0 || run.itemsFailed > 0) && (
|
||||
<div
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[12px] text-[#737373] mt-0.5",
|
||||
)}
|
||||
>
|
||||
{pluralize(run.itemsProcessed, "item")} processed
|
||||
{run.itemsFailed > 0 && (
|
||||
<span className="text-[#EF4444]">
|
||||
{" "}
|
||||
· {run.itemsFailed} failed
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{run.error && (
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[12px] text-[#EF4444]/80 break-words line-clamp-3 mt-0.5",
|
||||
)}
|
||||
>
|
||||
{run.error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Timeline({ runs }: { runs: SyncRun[] }) {
|
||||
// Group consecutive runs by calendar-day label, preserving server order.
|
||||
const groups: { label: string; runs: SyncRun[] }[] = []
|
||||
for (const run of runs) {
|
||||
const label = dayLabel(run.startedAt)
|
||||
const last = groups.at(-1)
|
||||
if (last && last.label === label) {
|
||||
last.runs.push(run)
|
||||
} else {
|
||||
groups.push({ label, runs: [run] })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{groups.map((group, gi) => (
|
||||
<div key={`${group.label}-${gi}`} className="flex flex-col gap-1.5">
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[10px] uppercase tracking-wide text-[#525966]",
|
||||
)}
|
||||
>
|
||||
{group.label}
|
||||
</span>
|
||||
<div className="flex flex-col">
|
||||
{group.runs.map((run, i) => (
|
||||
<TimelineRow
|
||||
key={run.id}
|
||||
run={run}
|
||||
isLast={i === group.runs.length - 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface SyncHistoryPanelProps {
|
||||
connectionId: string
|
||||
/** Only fetch / render when expanded. */
|
||||
isOpen: boolean
|
||||
}
|
||||
|
||||
/** Inline sync-history view (stats strip + timeline) rendered inside an expanded connection row. */
|
||||
export function SyncHistoryPanel({
|
||||
connectionId,
|
||||
isOpen,
|
||||
}: SyncHistoryPanelProps) {
|
||||
const {
|
||||
data: syncRuns,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
} = useSyncRuns(isOpen ? connectionId : "")
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
const hasRuns = !isLoading && !error && syncRuns && syncRuns.length > 0
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center py-6">
|
||||
<div className="size-5 border-2 border-[#737373] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && !isLoading && (
|
||||
<div className="flex items-center justify-center gap-2 py-6">
|
||||
<span
|
||||
className={cn(dmSans125ClassName(), "text-[13px] text-[#737373]")}
|
||||
>
|
||||
Failed to load sync history
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => refetch()}
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[13px] text-[#4BA0FA] hover:text-[#4BA0FA]/80 underline cursor-pointer",
|
||||
)}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && !error && syncRuns && syncRuns.length === 0 && (
|
||||
<div className="flex items-center justify-center py-6">
|
||||
<span
|
||||
className={cn(dmSans125ClassName(), "text-[13px] text-[#737373]")}
|
||||
>
|
||||
No syncs yet — runs will appear here.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasRuns && (
|
||||
<>
|
||||
<SummaryStats runs={syncRuns} />
|
||||
<Timeline runs={syncRuns} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,192 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
import { dmSans125ClassName } from "@/lib/fonts"
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@ui/components/sheet"
|
||||
import { useSyncRuns } from "@/hooks/use-sync-runs"
|
||||
import type { SyncRun } from "@/hooks/use-sync-runs"
|
||||
import {
|
||||
formatRelativeTime,
|
||||
TRIGGER_TYPE_LABELS,
|
||||
PROVIDER_DISPLAY_NAMES,
|
||||
} from "@/components/settings/sync-utils"
|
||||
import type { ConnectionResponseSchema } from "@repo/validation/api"
|
||||
import type { z } from "zod"
|
||||
|
||||
type Connection = z.infer<typeof ConnectionResponseSchema>
|
||||
|
||||
const STATUS_COLORS: Record<string, { dot: string; text: string }> = {
|
||||
completed: { dot: "bg-[#00AC3F]", text: "text-[#00AC3F]" },
|
||||
failed: { dot: "bg-[#EF4444]", text: "text-[#EF4444]" },
|
||||
running: { dot: "bg-[#4BA0FA] animate-pulse", text: "text-[#4BA0FA]" },
|
||||
}
|
||||
|
||||
function SyncRunCard({ run }: { run: SyncRun }) {
|
||||
const colors = STATUS_COLORS[run.status] ?? {
|
||||
dot: "bg-[#4BA0FA] animate-pulse",
|
||||
text: "text-[#4BA0FA]",
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-[#14161A] rounded-[12px] p-4 border border-[rgba(82,89,102,0.2)] flex flex-col gap-2">
|
||||
{/* Top row: status + trigger + time */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={cn("size-[7px] rounded-full", colors.dot)} />
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"font-medium text-[14px]",
|
||||
colors.text,
|
||||
)}
|
||||
>
|
||||
{run.status.charAt(0).toUpperCase() + run.status.slice(1)}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[12px] text-[#737373] bg-[#1A1D24] rounded-full px-2 py-0.5",
|
||||
)}
|
||||
>
|
||||
{TRIGGER_TYPE_LABELS[run.triggerType] ?? run.triggerType}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className={cn(dmSans125ClassName(), "text-[12px] text-[#737373]")}
|
||||
>
|
||||
{formatRelativeTime(run.startedAt)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Middle row: item counts */}
|
||||
{(run.itemsProcessed > 0 || run.itemsFailed > 0) && (
|
||||
<div
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"flex items-center gap-1 text-[13px]",
|
||||
)}
|
||||
>
|
||||
<span className="text-[#737373]">{run.itemsProcessed} processed</span>
|
||||
{run.itemsFailed > 0 && (
|
||||
<span className="text-[#EF4444]">· {run.itemsFailed} failed</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bottom row: error message */}
|
||||
{run.error && (
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[13px] text-[#EF4444]/80 break-words line-clamp-3",
|
||||
)}
|
||||
>
|
||||
{run.error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface SyncHistorySheetProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
connection: Connection | null
|
||||
}
|
||||
|
||||
export function SyncHistorySheet({
|
||||
open,
|
||||
onOpenChange,
|
||||
connection,
|
||||
}: SyncHistorySheetProps) {
|
||||
const {
|
||||
data: syncRuns,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
} = useSyncRuns(open && connection ? connection.id : "")
|
||||
|
||||
const providerTitle = connection
|
||||
? (PROVIDER_DISPLAY_NAMES[connection.provider] ?? connection.provider)
|
||||
: ""
|
||||
const email = connection?.email ?? ""
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent
|
||||
side="right"
|
||||
className="bg-[#0D0F14] border-l border-[#1A1D24] w-full max-w-[400px] gap-0 overflow-hidden p-0 [&>button]:text-[#737373] [&>button]:hover:text-[#FAFAFA]"
|
||||
>
|
||||
<SheetHeader className="px-6 pt-6 pb-4 border-b border-[#1A1D24]">
|
||||
<SheetTitle
|
||||
className={cn(dmSans125ClassName(), "text-[#FAFAFA] text-[18px]")}
|
||||
>
|
||||
Sync History
|
||||
</SheetTitle>
|
||||
<SheetDescription
|
||||
className={cn(dmSans125ClassName(), "text-[#737373] text-[14px]")}
|
||||
>
|
||||
{providerTitle}
|
||||
{email ? ` · ${email}` : ""}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="px-6 py-4 overflow-y-auto flex-1 flex flex-col gap-3">
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="size-6 border-2 border-[#737373] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && !isLoading && (
|
||||
<div className="flex flex-col items-center justify-center py-12 gap-2">
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[14px] text-[#737373]",
|
||||
)}
|
||||
>
|
||||
Failed to load sync history
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => refetch()}
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[14px] text-[#4BA0FA] hover:text-[#4BA0FA]/80 underline cursor-pointer",
|
||||
)}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && !error && syncRuns && syncRuns.length === 0 && (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[14px] text-[#737373]",
|
||||
)}
|
||||
>
|
||||
No sync runs yet
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading &&
|
||||
!error &&
|
||||
syncRuns &&
|
||||
syncRuns.length > 0 &&
|
||||
syncRuns.map((run) => <SyncRunCard key={run.id} run={run} />)}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
|
@ -29,10 +29,10 @@ export function SyncStatusBadge({
|
|||
const status = deriveStatus(syncInProgress, lastSyncedAt, isExpired)
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-2", className)}>
|
||||
<div className={cn("flex items-center gap-1.5", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"size-[7px] rounded-full",
|
||||
"size-[6px] rounded-full",
|
||||
status === "syncing" && "bg-[#4BA0FA] animate-pulse",
|
||||
status === "synced" && "bg-[#00AC3F]",
|
||||
status === "expired" && "bg-[#EF4444]",
|
||||
|
|
@ -43,7 +43,7 @@ export function SyncStatusBadge({
|
|||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"font-medium text-[16px] tracking-[-0.16px] text-[#4BA0FA]",
|
||||
"font-medium text-[13px] tracking-[-0.13px] text-[#4BA0FA]",
|
||||
)}
|
||||
>
|
||||
Syncing...
|
||||
|
|
@ -54,7 +54,7 @@ export function SyncStatusBadge({
|
|||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"font-medium text-[16px] tracking-[-0.16px] text-[#00AC3F]",
|
||||
"font-medium text-[13px] tracking-[-0.13px] text-[#00AC3F]",
|
||||
)}
|
||||
>
|
||||
Synced
|
||||
|
|
@ -63,7 +63,7 @@ export function SyncStatusBadge({
|
|||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"font-medium text-[14px] tracking-[-0.14px] text-[#737373]",
|
||||
"font-medium text-[12px] tracking-[-0.12px] text-[#737373]",
|
||||
)}
|
||||
>
|
||||
{formatRelativeTime(lastSyncedAt)}
|
||||
|
|
@ -74,7 +74,7 @@ export function SyncStatusBadge({
|
|||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"font-medium text-[16px] tracking-[-0.16px] text-[#EF4444]",
|
||||
"font-medium text-[13px] tracking-[-0.13px] text-[#EF4444]",
|
||||
)}
|
||||
>
|
||||
Disconnected
|
||||
|
|
@ -84,7 +84,7 @@ export function SyncStatusBadge({
|
|||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"font-medium text-[16px] tracking-[-0.16px] text-[#737373]",
|
||||
"font-medium text-[13px] tracking-[-0.13px] text-[#737373]",
|
||||
)}
|
||||
>
|
||||
Waiting for first sync
|
||||
|
|
|
|||
|
|
@ -3,7 +3,12 @@
|
|||
import { $fetch } from "@lib/api"
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { toast } from "sonner"
|
||||
import type { ConnectionResponseSchema } from "@repo/validation/api"
|
||||
import type { z } from "zod"
|
||||
import type { ImportProvider } from "@/components/settings/sync-utils"
|
||||
import type { SyncRun } from "@/hooks/use-sync-runs"
|
||||
|
||||
type Connection = z.infer<typeof ConnectionResponseSchema>
|
||||
|
||||
export function useTriggerSync() {
|
||||
const queryClient = useQueryClient()
|
||||
|
|
@ -13,9 +18,7 @@ export function useTriggerSync() {
|
|||
provider,
|
||||
containerTags,
|
||||
}: {
|
||||
// connectionId is not sent to the backend — the import endpoint is keyed
|
||||
// by provider, so it re-syncs all connections for that provider.
|
||||
// It's kept here so onSuccess can target cache invalidation.
|
||||
// connectionId isn't sent to the backend (import is keyed by provider) — kept for cache updates
|
||||
connectionId: string
|
||||
provider: ImportProvider
|
||||
containerTags?: string[]
|
||||
|
|
@ -32,15 +35,60 @@ export function useTriggerSync() {
|
|||
}
|
||||
return response.data
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
// Optimistically flip to "syncing" so the badge/button update instantly; the 5s poll then converges on real state
|
||||
onMutate: async (variables) => {
|
||||
await queryClient.cancelQueries({ queryKey: ["connections"] })
|
||||
const previousConnections = queryClient.getQueryData<Connection[]>([
|
||||
"connections",
|
||||
])
|
||||
queryClient.setQueryData<Connection[]>(["connections"], (old) =>
|
||||
old?.map((c) =>
|
||||
c.provider === variables.provider
|
||||
? {
|
||||
...c,
|
||||
metadata: {
|
||||
...((c.metadata as Record<string, unknown> | null) ?? {}),
|
||||
syncInProgress: true,
|
||||
},
|
||||
}
|
||||
: c,
|
||||
),
|
||||
)
|
||||
|
||||
const syncRunsKey = ["sync-runs", variables.connectionId]
|
||||
const previousSyncRuns = queryClient.getQueryData<SyncRun[]>(syncRunsKey)
|
||||
if (previousSyncRuns) {
|
||||
const optimisticRun: SyncRun = {
|
||||
id: `optimistic-${Date.now()}`,
|
||||
connectionId: variables.connectionId,
|
||||
status: "running",
|
||||
triggerType: "manual",
|
||||
startedAt: new Date().toISOString(),
|
||||
completedAt: null,
|
||||
itemsProcessed: 0,
|
||||
itemsFailed: 0,
|
||||
error: null,
|
||||
}
|
||||
queryClient.setQueryData<SyncRun[]>(syncRunsKey, [
|
||||
optimisticRun,
|
||||
...previousSyncRuns,
|
||||
])
|
||||
}
|
||||
|
||||
return { previousConnections, previousSyncRuns, syncRunsKey }
|
||||
},
|
||||
// Don't invalidate connections/sync-runs here — an immediate refetch races the backend and clobbers the optimistic state; the 5s polls handle it
|
||||
onSuccess: () => {
|
||||
toast.success("Sync started")
|
||||
queryClient.invalidateQueries({ queryKey: ["connections"] })
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["sync-runs", variables.connectionId],
|
||||
})
|
||||
queryClient.invalidateQueries({ queryKey: ["processing-documents"] })
|
||||
},
|
||||
onError: (error) => {
|
||||
onError: (error, _variables, context) => {
|
||||
if (context?.previousConnections !== undefined) {
|
||||
queryClient.setQueryData(["connections"], context.previousConnections)
|
||||
}
|
||||
if (context?.previousSyncRuns !== undefined) {
|
||||
queryClient.setQueryData(context.syncRunsKey, context.previousSyncRuns)
|
||||
}
|
||||
toast.error("Failed to start sync", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue