@useatlas/react 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -0
- package/dist/chunk-2WFDP7G5.js +231 -0
- package/dist/chunk-2WFDP7G5.js.map +1 -0
- package/dist/chunk-44HBZYKP.js +224 -0
- package/dist/chunk-44HBZYKP.js.map +1 -0
- package/dist/chunk-5SEVKHS5.cjs +229 -0
- package/dist/chunk-5SEVKHS5.cjs.map +1 -0
- package/dist/chunk-UIRB6L36.cjs +249 -0
- package/dist/chunk-UIRB6L36.cjs.map +1 -0
- package/dist/hooks.cjs +251 -0
- package/dist/hooks.cjs.map +1 -0
- package/dist/hooks.d.cts +132 -0
- package/dist/hooks.d.ts +132 -0
- package/dist/hooks.js +237 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.cjs +2976 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +69 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +2926 -0
- package/dist/index.js.map +1 -0
- package/dist/result-chart-NFAJ4IQ5.js +398 -0
- package/dist/result-chart-NFAJ4IQ5.js.map +1 -0
- package/dist/result-chart-YLCKBNV4.cjs +400 -0
- package/dist/result-chart-YLCKBNV4.cjs.map +1 -0
- package/dist/styles.css +59 -0
- package/dist/use-dark-mode-rFxawUv1.d.cts +123 -0
- package/dist/use-dark-mode-rFxawUv1.d.ts +123 -0
- package/dist/widget.css +2 -0
- package/dist/widget.js +445 -0
- package/package.json +113 -0
- package/src/components/__tests__/tool-renderers.test.tsx +239 -0
- package/src/components/actions/action-approval-card.tsx +296 -0
- package/src/components/actions/action-status-badge.tsx +50 -0
- package/src/components/admin/change-password-dialog.tsx +128 -0
- package/src/components/atlas-chat.tsx +656 -0
- package/src/components/chart/chart-detection.ts +318 -0
- package/src/components/chart/result-chart.tsx +590 -0
- package/src/components/chat/api-key-bar.tsx +66 -0
- package/src/components/chat/copy-button.tsx +25 -0
- package/src/components/chat/data-table.tsx +104 -0
- package/src/components/chat/error-banner.tsx +32 -0
- package/src/components/chat/explore-card.tsx +41 -0
- package/src/components/chat/follow-up-chips.tsx +29 -0
- package/src/components/chat/loading-card.tsx +10 -0
- package/src/components/chat/managed-auth-card.tsx +116 -0
- package/src/components/chat/markdown.tsx +146 -0
- package/src/components/chat/python-result-card.tsx +245 -0
- package/src/components/chat/sql-block.tsx +54 -0
- package/src/components/chat/sql-result-card.tsx +163 -0
- package/src/components/chat/starter-prompts.ts +6 -0
- package/src/components/chat/tool-part.tsx +106 -0
- package/src/components/chat/typing-indicator.tsx +22 -0
- package/src/components/conversations/conversation-item.tsx +135 -0
- package/src/components/conversations/conversation-list.tsx +69 -0
- package/src/components/conversations/conversation-sidebar.tsx +113 -0
- package/src/components/conversations/delete-confirmation.tsx +27 -0
- package/src/components/schema-explorer/schema-explorer.tsx +517 -0
- package/src/components/ui/alert-dialog.tsx +196 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/scroll-area.tsx +62 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/table.tsx +116 -0
- package/src/components/ui/toggle-group.tsx +83 -0
- package/src/components/ui/toggle.tsx +47 -0
- package/src/context.tsx +85 -0
- package/src/env.d.ts +9 -0
- package/src/hooks/__tests__/provider.test.tsx +83 -0
- package/src/hooks/__tests__/use-atlas-auth.test.tsx +283 -0
- package/src/hooks/__tests__/use-atlas-chat.test.tsx +157 -0
- package/src/hooks/__tests__/use-atlas-conversations.test.tsx +159 -0
- package/src/hooks/__tests__/use-atlas-theme.test.tsx +56 -0
- package/src/hooks/index.ts +47 -0
- package/src/hooks/provider.tsx +77 -0
- package/src/hooks/theme-init-script.ts +17 -0
- package/src/hooks/use-atlas-auth.ts +131 -0
- package/src/hooks/use-atlas-chat.ts +102 -0
- package/src/hooks/use-atlas-conversations.ts +61 -0
- package/src/hooks/use-atlas-theme.ts +34 -0
- package/src/hooks/use-conversations.ts +189 -0
- package/src/hooks/use-dark-mode.ts +150 -0
- package/src/index.ts +36 -0
- package/src/lib/action-types.ts +11 -0
- package/src/lib/helpers.ts +198 -0
- package/src/lib/tool-renderer-types.ts +76 -0
- package/src/lib/types.ts +29 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles.css +59 -0
- package/src/test-setup.ts +55 -0
- package/src/widget-entry.ts +20 -0
- package/src/widget.css +12 -0
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useRef } from "react";
|
|
4
|
+
import { useAtlasConfig } from "../../context";
|
|
5
|
+
import {
|
|
6
|
+
Sheet,
|
|
7
|
+
SheetContent,
|
|
8
|
+
SheetHeader,
|
|
9
|
+
SheetTitle,
|
|
10
|
+
} from "../ui/sheet";
|
|
11
|
+
import { Input } from "../ui/input";
|
|
12
|
+
import { Badge } from "../ui/badge";
|
|
13
|
+
import { ScrollArea } from "../ui/scroll-area";
|
|
14
|
+
import { Separator } from "../ui/separator";
|
|
15
|
+
import {
|
|
16
|
+
Table,
|
|
17
|
+
TableBody,
|
|
18
|
+
TableCell,
|
|
19
|
+
TableHead,
|
|
20
|
+
TableHeader,
|
|
21
|
+
TableRow,
|
|
22
|
+
} from "../ui/table";
|
|
23
|
+
import { Button } from "../ui/button";
|
|
24
|
+
import { Card, CardContent } from "../ui/card";
|
|
25
|
+
import { ToggleGroup, ToggleGroupItem } from "../ui/toggle-group";
|
|
26
|
+
import {
|
|
27
|
+
Search,
|
|
28
|
+
ArrowLeft,
|
|
29
|
+
ArrowRight,
|
|
30
|
+
TableProperties,
|
|
31
|
+
Eye,
|
|
32
|
+
Columns3,
|
|
33
|
+
Link2,
|
|
34
|
+
Sparkles,
|
|
35
|
+
} from "lucide-react";
|
|
36
|
+
import type {
|
|
37
|
+
SemanticEntitySummary,
|
|
38
|
+
SemanticEntityDetail,
|
|
39
|
+
Dimension,
|
|
40
|
+
Join,
|
|
41
|
+
Measure,
|
|
42
|
+
QueryPattern,
|
|
43
|
+
} from "../../lib/types";
|
|
44
|
+
import { normalizeList } from "../../lib/helpers";
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Helpers
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
type TypeFilter = "all" | "table" | "view";
|
|
51
|
+
|
|
52
|
+
/** Parse a server error response, extracting the message field if present. */
|
|
53
|
+
async function parseErrorResponse(r: Response): Promise<string> {
|
|
54
|
+
try {
|
|
55
|
+
const body = await r.json();
|
|
56
|
+
if (typeof body?.message === "string") return body.message;
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.warn("Could not parse error response body as JSON:", err);
|
|
59
|
+
}
|
|
60
|
+
return `HTTP ${r.status}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Entity list
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
function EntityList({
|
|
68
|
+
entities,
|
|
69
|
+
search,
|
|
70
|
+
onSearchChange,
|
|
71
|
+
typeFilter,
|
|
72
|
+
onTypeFilterChange,
|
|
73
|
+
onSelect,
|
|
74
|
+
}: {
|
|
75
|
+
entities: SemanticEntitySummary[];
|
|
76
|
+
search: string;
|
|
77
|
+
onSearchChange: (v: string) => void;
|
|
78
|
+
typeFilter: TypeFilter;
|
|
79
|
+
onTypeFilterChange: (v: TypeFilter) => void;
|
|
80
|
+
onSelect: (name: string) => void;
|
|
81
|
+
}) {
|
|
82
|
+
const filtered = entities.filter((e) => {
|
|
83
|
+
const matchesSearch =
|
|
84
|
+
!search ||
|
|
85
|
+
e.table.toLowerCase().includes(search.toLowerCase()) ||
|
|
86
|
+
e.description.toLowerCase().includes(search.toLowerCase());
|
|
87
|
+
const matchesType =
|
|
88
|
+
typeFilter === "all" ||
|
|
89
|
+
(typeFilter === "view" ? e.type === "view" : e.type !== "view");
|
|
90
|
+
return matchesSearch && matchesType;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const tableCount = entities.filter((e) => e.type !== "view").length;
|
|
94
|
+
const viewCount = entities.filter((e) => e.type === "view").length;
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="flex h-full flex-col">
|
|
98
|
+
<div className="space-y-3 px-4 pb-3">
|
|
99
|
+
<div className="relative">
|
|
100
|
+
<Search className="absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-zinc-400" />
|
|
101
|
+
<Input
|
|
102
|
+
value={search}
|
|
103
|
+
onChange={(e) => onSearchChange(e.target.value)}
|
|
104
|
+
placeholder="Search tables..."
|
|
105
|
+
className="h-8 pl-8 text-sm"
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
{viewCount > 0 && (
|
|
109
|
+
<ToggleGroup
|
|
110
|
+
type="single"
|
|
111
|
+
size="sm"
|
|
112
|
+
value={typeFilter}
|
|
113
|
+
onValueChange={(v) => { if (v) onTypeFilterChange(v as TypeFilter); }}
|
|
114
|
+
className="justify-start"
|
|
115
|
+
>
|
|
116
|
+
<ToggleGroupItem value="all" className="h-6 px-2 text-xs">
|
|
117
|
+
All ({entities.length})
|
|
118
|
+
</ToggleGroupItem>
|
|
119
|
+
<ToggleGroupItem value="table" className="h-6 px-2 text-xs">
|
|
120
|
+
Tables ({tableCount})
|
|
121
|
+
</ToggleGroupItem>
|
|
122
|
+
<ToggleGroupItem value="view" className="h-6 px-2 text-xs">
|
|
123
|
+
Views ({viewCount})
|
|
124
|
+
</ToggleGroupItem>
|
|
125
|
+
</ToggleGroup>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<Separator />
|
|
130
|
+
|
|
131
|
+
<div className="flex-1 overflow-y-auto overflow-x-hidden p-2">
|
|
132
|
+
{filtered.length === 0 ? (
|
|
133
|
+
<p className="px-2 py-8 text-center text-xs text-zinc-400 dark:text-zinc-500">
|
|
134
|
+
{search ? "No matching entities" : "No entities found"}
|
|
135
|
+
</p>
|
|
136
|
+
) : (
|
|
137
|
+
filtered.map((entity) => (
|
|
138
|
+
<button
|
|
139
|
+
key={entity.table}
|
|
140
|
+
onClick={() => onSelect(entity.table)}
|
|
141
|
+
className="flex w-full min-w-0 items-start gap-2 overflow-hidden rounded-md px-2 py-2 text-left transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800"
|
|
142
|
+
>
|
|
143
|
+
{entity.type === "view" ? (
|
|
144
|
+
<Eye className="mt-0.5 size-3.5 shrink-0 text-zinc-400" />
|
|
145
|
+
) : (
|
|
146
|
+
<TableProperties className="mt-0.5 size-3.5 shrink-0 text-zinc-400" />
|
|
147
|
+
)}
|
|
148
|
+
<div className="min-w-0 flex-1">
|
|
149
|
+
<div className="flex items-center gap-1.5">
|
|
150
|
+
<span className="truncate text-sm font-medium text-zinc-800 dark:text-zinc-200">
|
|
151
|
+
{entity.table}
|
|
152
|
+
</span>
|
|
153
|
+
{entity.type === "view" && (
|
|
154
|
+
<Badge variant="outline" className="shrink-0 text-[10px] px-1 py-0">
|
|
155
|
+
view
|
|
156
|
+
</Badge>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
{entity.description && (
|
|
160
|
+
<p className="mt-0.5 truncate text-xs text-zinc-500 dark:text-zinc-400">
|
|
161
|
+
{entity.description}
|
|
162
|
+
</p>
|
|
163
|
+
)}
|
|
164
|
+
<div className="mt-1 flex gap-2 text-[10px] text-zinc-400 dark:text-zinc-500">
|
|
165
|
+
<span>{entity.columnCount} cols</span>
|
|
166
|
+
{entity.joinCount > 0 && <span>{entity.joinCount} joins</span>}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
<ArrowRight className="mt-1 size-3 shrink-0 text-zinc-300 dark:text-zinc-600" />
|
|
170
|
+
</button>
|
|
171
|
+
))
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Entity detail
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
function EntityDetailView({
|
|
183
|
+
entity,
|
|
184
|
+
onBack,
|
|
185
|
+
onNavigateEntity,
|
|
186
|
+
onInsertQuery,
|
|
187
|
+
}: {
|
|
188
|
+
entity: SemanticEntityDetail;
|
|
189
|
+
onBack: () => void;
|
|
190
|
+
onNavigateEntity: (name: string) => void;
|
|
191
|
+
onInsertQuery: (description: string) => void;
|
|
192
|
+
}) {
|
|
193
|
+
const dimensions = normalizeList(entity.dimensions, "name") as Dimension[];
|
|
194
|
+
const joins = normalizeList(entity.joins, "to") as Join[];
|
|
195
|
+
const measures = normalizeList(entity.measures, "name") as Measure[];
|
|
196
|
+
const patterns = normalizeList(entity.query_patterns, "name") as QueryPattern[];
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<div className="flex h-full flex-col">
|
|
200
|
+
<div className="flex items-center gap-2 px-4 pb-3">
|
|
201
|
+
<Button variant="ghost" size="icon" className="size-7" onClick={onBack}>
|
|
202
|
+
<ArrowLeft className="size-3.5" />
|
|
203
|
+
</Button>
|
|
204
|
+
<div className="min-w-0 flex-1">
|
|
205
|
+
<div className="flex items-center gap-1.5">
|
|
206
|
+
<h3 className="truncate text-sm font-semibold">{entity.table}</h3>
|
|
207
|
+
{entity.type === "view" && (
|
|
208
|
+
<Badge variant="outline" className="text-[10px]">view</Badge>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<Separator />
|
|
215
|
+
|
|
216
|
+
<ScrollArea className="flex-1">
|
|
217
|
+
<div className="space-y-5 p-4">
|
|
218
|
+
{entity.description && (
|
|
219
|
+
<p className="text-xs text-zinc-500 dark:text-zinc-400">{entity.description}</p>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
{/* Columns */}
|
|
223
|
+
<section>
|
|
224
|
+
<h4 className="mb-2 flex items-center gap-1.5 text-xs font-semibold text-zinc-700 dark:text-zinc-300">
|
|
225
|
+
<Columns3 className="size-3" />
|
|
226
|
+
Columns ({dimensions.length})
|
|
227
|
+
</h4>
|
|
228
|
+
<div className="rounded-md border">
|
|
229
|
+
<Table className="table-fixed">
|
|
230
|
+
<TableHeader>
|
|
231
|
+
<TableRow>
|
|
232
|
+
<TableHead className="h-7 w-[30%] text-[10px]">Name</TableHead>
|
|
233
|
+
<TableHead className="h-7 w-[15%] text-[10px]">Type</TableHead>
|
|
234
|
+
<TableHead className="h-7 w-[35%] text-[10px] hidden sm:table-cell">Description</TableHead>
|
|
235
|
+
<TableHead className="h-7 w-[20%] text-[10px] hidden sm:table-cell">Samples</TableHead>
|
|
236
|
+
</TableRow>
|
|
237
|
+
</TableHeader>
|
|
238
|
+
<TableBody>
|
|
239
|
+
{dimensions.map((dim) => (
|
|
240
|
+
<TableRow key={dim.name}>
|
|
241
|
+
<TableCell className="py-1.5 font-mono text-[11px]">
|
|
242
|
+
<span className="flex items-center gap-1">
|
|
243
|
+
<span className="truncate">{dim.name}</span>
|
|
244
|
+
{dim.primary_key && (
|
|
245
|
+
<Badge className="shrink-0 bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400 text-[9px] px-1 py-0">
|
|
246
|
+
PK
|
|
247
|
+
</Badge>
|
|
248
|
+
)}
|
|
249
|
+
{dim.foreign_key && (
|
|
250
|
+
<Badge className="shrink-0 bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400 text-[9px] px-1 py-0">
|
|
251
|
+
FK
|
|
252
|
+
</Badge>
|
|
253
|
+
)}
|
|
254
|
+
</span>
|
|
255
|
+
</TableCell>
|
|
256
|
+
<TableCell className="py-1.5">
|
|
257
|
+
<Badge variant="secondary" className="font-mono text-[9px]">
|
|
258
|
+
{dim.type}
|
|
259
|
+
</Badge>
|
|
260
|
+
</TableCell>
|
|
261
|
+
<TableCell className="hidden py-1.5 text-[11px] text-zinc-500 sm:table-cell">
|
|
262
|
+
<span className="line-clamp-2">{dim.description || "—"}</span>
|
|
263
|
+
</TableCell>
|
|
264
|
+
<TableCell className="hidden py-1.5 text-[11px] text-zinc-400 sm:table-cell">
|
|
265
|
+
<span className="line-clamp-1">
|
|
266
|
+
{dim.sample_values?.length
|
|
267
|
+
? dim.sample_values.slice(0, 3).join(", ")
|
|
268
|
+
: "—"}
|
|
269
|
+
</span>
|
|
270
|
+
</TableCell>
|
|
271
|
+
</TableRow>
|
|
272
|
+
))}
|
|
273
|
+
</TableBody>
|
|
274
|
+
</Table>
|
|
275
|
+
</div>
|
|
276
|
+
</section>
|
|
277
|
+
|
|
278
|
+
{/* Joins */}
|
|
279
|
+
{joins.length > 0 && (
|
|
280
|
+
<section>
|
|
281
|
+
<h4 className="mb-2 flex items-center gap-1.5 text-xs font-semibold text-zinc-700 dark:text-zinc-300">
|
|
282
|
+
<Link2 className="size-3" />
|
|
283
|
+
Relationships ({joins.length})
|
|
284
|
+
</h4>
|
|
285
|
+
<div className="space-y-1.5">
|
|
286
|
+
{joins.map((join, i) => (
|
|
287
|
+
<Card key={i} className="shadow-none">
|
|
288
|
+
<CardContent className="px-3 py-2">
|
|
289
|
+
<div className="flex items-center gap-2">
|
|
290
|
+
<Badge variant="outline" className="shrink-0 text-[9px]">
|
|
291
|
+
{join.relationship || "many_to_one"}
|
|
292
|
+
</Badge>
|
|
293
|
+
<button
|
|
294
|
+
onClick={() => onNavigateEntity(join.to)}
|
|
295
|
+
className="text-xs font-medium text-blue-600 hover:underline dark:text-blue-400"
|
|
296
|
+
>
|
|
297
|
+
{join.to}
|
|
298
|
+
</button>
|
|
299
|
+
</div>
|
|
300
|
+
{join.description && (
|
|
301
|
+
<p className="mt-1 text-[11px] text-zinc-500">{join.description}</p>
|
|
302
|
+
)}
|
|
303
|
+
</CardContent>
|
|
304
|
+
</Card>
|
|
305
|
+
))}
|
|
306
|
+
</div>
|
|
307
|
+
</section>
|
|
308
|
+
)}
|
|
309
|
+
|
|
310
|
+
{/* Measures */}
|
|
311
|
+
{measures.length > 0 && (
|
|
312
|
+
<section>
|
|
313
|
+
<h4 className="mb-2 text-xs font-semibold text-zinc-700 dark:text-zinc-300">
|
|
314
|
+
Measures ({measures.length})
|
|
315
|
+
</h4>
|
|
316
|
+
<div className="space-y-1">
|
|
317
|
+
{measures.map((m) => (
|
|
318
|
+
<div key={m.name} className="flex items-center gap-2 rounded px-2 py-1 text-xs">
|
|
319
|
+
<span className="font-medium text-zinc-700 dark:text-zinc-300">{m.name}</span>
|
|
320
|
+
<code className="rounded bg-zinc-100 px-1.5 py-0.5 text-[10px] text-zinc-500 dark:bg-zinc-800">
|
|
321
|
+
{m.sql}
|
|
322
|
+
</code>
|
|
323
|
+
</div>
|
|
324
|
+
))}
|
|
325
|
+
</div>
|
|
326
|
+
</section>
|
|
327
|
+
)}
|
|
328
|
+
|
|
329
|
+
{/* Query patterns */}
|
|
330
|
+
{patterns.length > 0 && (
|
|
331
|
+
<section>
|
|
332
|
+
<h4 className="mb-2 flex items-center gap-1.5 text-xs font-semibold text-zinc-700 dark:text-zinc-300">
|
|
333
|
+
<Sparkles className="size-3" />
|
|
334
|
+
Query Patterns
|
|
335
|
+
</h4>
|
|
336
|
+
<div className="space-y-1.5">
|
|
337
|
+
{patterns.map((p, i) => (
|
|
338
|
+
<button
|
|
339
|
+
key={`${p.name}-${i}`}
|
|
340
|
+
onClick={() => onInsertQuery(p.description)}
|
|
341
|
+
className="w-full rounded-md border border-zinc-200 bg-zinc-50 px-3 py-2 text-left transition-colors hover:border-zinc-400 hover:bg-zinc-100 dark:border-zinc-700 dark:bg-zinc-900 dark:hover:border-zinc-500 dark:hover:bg-zinc-800"
|
|
342
|
+
>
|
|
343
|
+
<p className="text-xs font-medium text-zinc-700 dark:text-zinc-300">{p.name}</p>
|
|
344
|
+
<p className="mt-0.5 text-[11px] text-zinc-500 dark:text-zinc-400">{p.description}</p>
|
|
345
|
+
</button>
|
|
346
|
+
))}
|
|
347
|
+
</div>
|
|
348
|
+
</section>
|
|
349
|
+
)}
|
|
350
|
+
</div>
|
|
351
|
+
</ScrollArea>
|
|
352
|
+
</div>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// Main schema explorer panel
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
export function SchemaExplorer({
|
|
361
|
+
open,
|
|
362
|
+
onOpenChange,
|
|
363
|
+
onInsertQuery,
|
|
364
|
+
getHeaders,
|
|
365
|
+
getCredentials,
|
|
366
|
+
}: {
|
|
367
|
+
open: boolean;
|
|
368
|
+
onOpenChange: (open: boolean) => void;
|
|
369
|
+
onInsertQuery: (text: string) => void;
|
|
370
|
+
getHeaders: () => Record<string, string>;
|
|
371
|
+
getCredentials: () => RequestCredentials;
|
|
372
|
+
}) {
|
|
373
|
+
const { apiUrl } = useAtlasConfig();
|
|
374
|
+
const [entities, setEntities] = useState<SemanticEntitySummary[]>([]);
|
|
375
|
+
const [selectedEntity, setSelectedEntity] = useState<SemanticEntityDetail | null>(null);
|
|
376
|
+
const [selectedName, setSelectedName] = useState<string | null>(null);
|
|
377
|
+
const [loading, setLoading] = useState(false);
|
|
378
|
+
const [error, setError] = useState<string | null>(null);
|
|
379
|
+
const [detailError, setDetailError] = useState<string | null>(null);
|
|
380
|
+
const [search, setSearch] = useState("");
|
|
381
|
+
const [typeFilter, setTypeFilter] = useState<TypeFilter>("all");
|
|
382
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
383
|
+
|
|
384
|
+
// Fetch entity list when the sheet opens (or when API config changes)
|
|
385
|
+
useEffect(() => {
|
|
386
|
+
if (!open) return;
|
|
387
|
+
|
|
388
|
+
// Reset to list view on every open — prevents stale detail view on reopen
|
|
389
|
+
setSelectedName(null);
|
|
390
|
+
setSelectedEntity(null);
|
|
391
|
+
setDetailError(null);
|
|
392
|
+
abortRef.current?.abort();
|
|
393
|
+
|
|
394
|
+
setLoading(true);
|
|
395
|
+
setError(null);
|
|
396
|
+
|
|
397
|
+
const controller = new AbortController();
|
|
398
|
+
fetch(`${apiUrl}/api/v1/semantic/entities`, {
|
|
399
|
+
headers: getHeaders(),
|
|
400
|
+
credentials: getCredentials(),
|
|
401
|
+
signal: controller.signal,
|
|
402
|
+
})
|
|
403
|
+
.then(async (r) => {
|
|
404
|
+
if (!r.ok) throw new Error(await parseErrorResponse(r));
|
|
405
|
+
return r.json();
|
|
406
|
+
})
|
|
407
|
+
.then((data) => {
|
|
408
|
+
const list = Array.isArray(data?.entities) ? data.entities : [];
|
|
409
|
+
setEntities(list);
|
|
410
|
+
})
|
|
411
|
+
.catch((err) => {
|
|
412
|
+
if (err instanceof DOMException && err.name === "AbortError") return;
|
|
413
|
+
console.warn("Schema explorer: failed to fetch entities:", err);
|
|
414
|
+
setError(err instanceof Error ? err.message : "Failed to load schema");
|
|
415
|
+
})
|
|
416
|
+
.finally(() => setLoading(false));
|
|
417
|
+
|
|
418
|
+
return () => controller.abort();
|
|
419
|
+
}, [open, apiUrl, getHeaders, getCredentials]);
|
|
420
|
+
|
|
421
|
+
function handleSelectEntity(name: string) {
|
|
422
|
+
// Cancel any in-flight detail request
|
|
423
|
+
abortRef.current?.abort();
|
|
424
|
+
const controller = new AbortController();
|
|
425
|
+
abortRef.current = controller;
|
|
426
|
+
|
|
427
|
+
setSelectedName(name);
|
|
428
|
+
setSelectedEntity(null);
|
|
429
|
+
setDetailError(null);
|
|
430
|
+
|
|
431
|
+
fetch(`${apiUrl}/api/v1/semantic/entities/${encodeURIComponent(name)}`, {
|
|
432
|
+
headers: getHeaders(),
|
|
433
|
+
credentials: getCredentials(),
|
|
434
|
+
signal: controller.signal,
|
|
435
|
+
})
|
|
436
|
+
.then(async (r) => {
|
|
437
|
+
if (!r.ok) throw new Error(await parseErrorResponse(r));
|
|
438
|
+
return r.json();
|
|
439
|
+
})
|
|
440
|
+
.then((data) => {
|
|
441
|
+
setSelectedEntity(data?.entity ?? data);
|
|
442
|
+
})
|
|
443
|
+
.catch((err) => {
|
|
444
|
+
if (err instanceof DOMException && err.name === "AbortError") return;
|
|
445
|
+
console.warn("Schema explorer: failed to load entity:", err);
|
|
446
|
+
setDetailError(err instanceof Error ? err.message : "Failed to load entity");
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function handleBack() {
|
|
451
|
+
abortRef.current?.abort();
|
|
452
|
+
setSelectedName(null);
|
|
453
|
+
setSelectedEntity(null);
|
|
454
|
+
setDetailError(null);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function handleInsertQuery(description: string) {
|
|
458
|
+
onInsertQuery(description);
|
|
459
|
+
onOpenChange(false);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return (
|
|
463
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
464
|
+
<SheetContent side="right" className="flex w-full flex-col p-0 sm:max-w-xl">
|
|
465
|
+
<SheetHeader className="px-4 pt-4">
|
|
466
|
+
<SheetTitle className="flex items-center gap-2 text-base">
|
|
467
|
+
<TableProperties className="size-4" />
|
|
468
|
+
Schema Explorer
|
|
469
|
+
</SheetTitle>
|
|
470
|
+
</SheetHeader>
|
|
471
|
+
|
|
472
|
+
<Separator className="mt-3" />
|
|
473
|
+
|
|
474
|
+
<div className="flex-1 overflow-hidden pt-3">
|
|
475
|
+
{loading ? (
|
|
476
|
+
<div className="flex h-full items-center justify-center">
|
|
477
|
+
<p className="text-xs text-zinc-400">Loading schema...</p>
|
|
478
|
+
</div>
|
|
479
|
+
) : error ? (
|
|
480
|
+
<div className="flex h-full items-center justify-center px-4">
|
|
481
|
+
<p className="text-center text-xs text-red-500">{error}</p>
|
|
482
|
+
</div>
|
|
483
|
+
) : selectedName ? (
|
|
484
|
+
detailError ? (
|
|
485
|
+
<div className="flex h-full flex-col items-center justify-center gap-2 px-4">
|
|
486
|
+
<p className="text-center text-xs text-red-500">{detailError}</p>
|
|
487
|
+
<Button variant="ghost" size="sm" onClick={handleBack} className="text-xs">
|
|
488
|
+
<ArrowLeft className="mr-1 size-3" /> Back to list
|
|
489
|
+
</Button>
|
|
490
|
+
</div>
|
|
491
|
+
) : selectedEntity ? (
|
|
492
|
+
<EntityDetailView
|
|
493
|
+
entity={selectedEntity}
|
|
494
|
+
onBack={handleBack}
|
|
495
|
+
onNavigateEntity={handleSelectEntity}
|
|
496
|
+
onInsertQuery={handleInsertQuery}
|
|
497
|
+
/>
|
|
498
|
+
) : (
|
|
499
|
+
<div className="flex h-full items-center justify-center">
|
|
500
|
+
<p className="text-xs text-zinc-400">Loading {selectedName}...</p>
|
|
501
|
+
</div>
|
|
502
|
+
)
|
|
503
|
+
) : (
|
|
504
|
+
<EntityList
|
|
505
|
+
entities={entities}
|
|
506
|
+
search={search}
|
|
507
|
+
onSearchChange={setSearch}
|
|
508
|
+
typeFilter={typeFilter}
|
|
509
|
+
onTypeFilterChange={setTypeFilter}
|
|
510
|
+
onSelect={handleSelectEntity}
|
|
511
|
+
/>
|
|
512
|
+
)}
|
|
513
|
+
</div>
|
|
514
|
+
</SheetContent>
|
|
515
|
+
</Sheet>
|
|
516
|
+
);
|
|
517
|
+
}
|