Spaces:
Sleeping
Sleeping
[KM-591] [KM][FE] Highlight ingest button and show last ingested time for DB clients
Browse files- Show pulsing orange button when a DB has never been ingested to prompt user action
- Revert to muted style after first ingest, with relative timestamp ("5m ago", "2h ago") below the button
- Persist ingest timestamps per client in localStorage (km_ingest_timestamps)
- Fix incorrect toast message referencing non-existent chunks_ingested field; now shows table count from IngestResponse
src/app/components/KnowledgeManagement.tsx
CHANGED
|
@@ -93,6 +93,13 @@ export default function KnowledgeManagement({
|
|
| 93 |
const [loadingDbTypes, setLoadingDbTypes] = useState(false);
|
| 94 |
const [ingesting, setIngesting] = useState<string | null>(null);
|
| 95 |
const [deletingClient, setDeletingClient] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
const pollingTimers = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map());
|
| 97 |
|
| 98 |
// ββ Data Catalog state ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -307,7 +314,14 @@ export default function KnowledgeManagement({
|
|
| 307 |
setIngesting(clientId);
|
| 308 |
try {
|
| 309 |
const res = await ingestDatabaseClient(clientId, userId);
|
| 310 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
} catch (err) {
|
| 312 |
toast.error(err instanceof Error ? err.message : "Ingestion failed");
|
| 313 |
} finally {
|
|
@@ -550,19 +564,39 @@ export default function KnowledgeManagement({
|
|
| 550 |
<p className="text-xs text-slate-400 capitalize">{client.db_type} Β· {client.status}</p>
|
| 551 |
</div>
|
| 552 |
<div className="flex items-center gap-1.5 flex-shrink-0">
|
| 553 |
-
<
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 563 |
)}
|
| 564 |
-
|
| 565 |
-
</button>
|
| 566 |
<button
|
| 567 |
onClick={() => handleDeleteClient(client.id)}
|
| 568 |
disabled={deletingClient === client.id}
|
|
|
|
| 93 |
const [loadingDbTypes, setLoadingDbTypes] = useState(false);
|
| 94 |
const [ingesting, setIngesting] = useState<string | null>(null);
|
| 95 |
const [deletingClient, setDeletingClient] = useState<string | null>(null);
|
| 96 |
+
const [ingestTimestamps, setIngestTimestamps] = useState<Record<string, string>>(() => {
|
| 97 |
+
try {
|
| 98 |
+
return JSON.parse(localStorage.getItem("km_ingest_timestamps") ?? "{}");
|
| 99 |
+
} catch {
|
| 100 |
+
return {};
|
| 101 |
+
}
|
| 102 |
+
});
|
| 103 |
const pollingTimers = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map());
|
| 104 |
|
| 105 |
// ββ Data Catalog state ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 314 |
setIngesting(clientId);
|
| 315 |
try {
|
| 316 |
const res = await ingestDatabaseClient(clientId, userId);
|
| 317 |
+
const tableCount = res.tables?.length ?? 0;
|
| 318 |
+
toast.success(`Ingested ${tableCount} table${tableCount !== 1 ? "s" : ""} successfully`);
|
| 319 |
+
const now = new Date().toISOString();
|
| 320 |
+
setIngestTimestamps((prev) => {
|
| 321 |
+
const updated = { ...prev, [clientId]: now };
|
| 322 |
+
localStorage.setItem("km_ingest_timestamps", JSON.stringify(updated));
|
| 323 |
+
return updated;
|
| 324 |
+
});
|
| 325 |
} catch (err) {
|
| 326 |
toast.error(err instanceof Error ? err.message : "Ingestion failed");
|
| 327 |
} finally {
|
|
|
|
| 564 |
<p className="text-xs text-slate-400 capitalize">{client.db_type} Β· {client.status}</p>
|
| 565 |
</div>
|
| 566 |
<div className="flex items-center gap-1.5 flex-shrink-0">
|
| 567 |
+
<div className="flex flex-col items-end gap-0.5">
|
| 568 |
+
<button
|
| 569 |
+
onClick={() => handleIngest(client.id)}
|
| 570 |
+
disabled={ingesting === client.id || client.status === "inactive"}
|
| 571 |
+
title={ingestTimestamps[client.id] ? `Last ingested: ${new Date(ingestTimestamps[client.id]).toLocaleString()}` : "Never ingested β click to ingest schema"}
|
| 572 |
+
className={[
|
| 573 |
+
"flex items-center gap-1 text-xs px-2.5 py-1 rounded-lg transition disabled:opacity-40 disabled:cursor-not-allowed",
|
| 574 |
+
ingestTimestamps[client.id]
|
| 575 |
+
? "bg-slate-100 hover:bg-orange-100 hover:text-[#FF8F00] text-slate-500"
|
| 576 |
+
: "bg-orange-500 hover:bg-orange-600 text-white shadow-sm shadow-orange-200 animate-pulse hover:animate-none",
|
| 577 |
+
].join(" ")}
|
| 578 |
+
>
|
| 579 |
+
{ingesting === client.id ? (
|
| 580 |
+
<Loader2 className="w-3 h-3 animate-spin" />
|
| 581 |
+
) : (
|
| 582 |
+
<Database className="w-3 h-3" />
|
| 583 |
+
)}
|
| 584 |
+
{ingesting === client.id ? "Ingestingβ¦" : "Ingest"}
|
| 585 |
+
</button>
|
| 586 |
+
{ingestTimestamps[client.id] && (
|
| 587 |
+
<span className="text-[10px] text-slate-300 leading-none">
|
| 588 |
+
{(() => {
|
| 589 |
+
const diff = Date.now() - new Date(ingestTimestamps[client.id]).getTime();
|
| 590 |
+
const mins = Math.floor(diff / 60000);
|
| 591 |
+
if (mins < 1) return "just now";
|
| 592 |
+
if (mins < 60) return `${mins}m ago`;
|
| 593 |
+
const hrs = Math.floor(mins / 60);
|
| 594 |
+
if (hrs < 24) return `${hrs}h ago`;
|
| 595 |
+
return `${Math.floor(hrs / 24)}d ago`;
|
| 596 |
+
})()}
|
| 597 |
+
</span>
|
| 598 |
)}
|
| 599 |
+
</div>
|
|
|
|
| 600 |
<button
|
| 601 |
onClick={() => handleDeleteClient(client.id)}
|
| 602 |
disabled={deletingClient === client.id}
|