@inkindcards/semantic-layer 0.1.3 → 0.2.0

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.
@@ -1,5 +1,5 @@
1
- import { useMetrics } from './chunk-RGVYFSW2.js';
2
- import { useState, useMemo } from 'react';
1
+ import { useMetrics, useAuth, useAdminFields, useAdminRoles, useAdminUsers } from './chunk-JTSHUCCY.js';
2
+ import { useState, useMemo, useCallback } from 'react';
3
3
  import { jsx, jsxs } from 'react/jsx-runtime';
4
4
 
5
5
  var containerStyle = {
@@ -189,7 +189,630 @@ function ResultsTable({
189
189
  ] })
190
190
  ] });
191
191
  }
192
+ var rootStyle = {
193
+ fontFamily: "system-ui, -apple-system, sans-serif",
194
+ maxWidth: 960,
195
+ margin: "0 auto",
196
+ padding: 24
197
+ };
198
+ var headingStyle = {
199
+ fontSize: 24,
200
+ fontWeight: 700,
201
+ color: "#111827",
202
+ margin: "0 0 4px"
203
+ };
204
+ var subStyle = {
205
+ fontSize: 14,
206
+ color: "#6b7280",
207
+ margin: "0 0 20px"
208
+ };
209
+ var searchBarStyle = {
210
+ display: "flex",
211
+ gap: 8,
212
+ marginBottom: 20
213
+ };
214
+ var searchInput = {
215
+ flex: 1,
216
+ padding: "10px 14px",
217
+ fontSize: 14,
218
+ border: "1px solid #d1d5db",
219
+ borderRadius: 8,
220
+ outline: "none",
221
+ boxSizing: "border-box"
222
+ };
223
+ var filterBtn = (active) => ({
224
+ padding: "8px 14px",
225
+ fontSize: 13,
226
+ fontWeight: 500,
227
+ borderRadius: 6,
228
+ border: "1px solid " + (active ? "#3b82f6" : "#d1d5db"),
229
+ backgroundColor: active ? "#eff6ff" : "#fff",
230
+ color: active ? "#1d4ed8" : "#374151",
231
+ cursor: "pointer"
232
+ });
233
+ var categoryStyle = {
234
+ marginBottom: 24
235
+ };
236
+ var categoryHeader = {
237
+ fontSize: 13,
238
+ fontWeight: 600,
239
+ textTransform: "uppercase",
240
+ letterSpacing: "0.05em",
241
+ color: "#6b7280",
242
+ marginBottom: 8,
243
+ paddingBottom: 6,
244
+ borderBottom: "1px solid #e5e7eb"
245
+ };
246
+ var fieldCard = {
247
+ display: "flex",
248
+ justifyContent: "space-between",
249
+ alignItems: "flex-start",
250
+ padding: "10px 12px",
251
+ borderRadius: 6,
252
+ marginBottom: 4,
253
+ backgroundColor: "#fafafa",
254
+ border: "1px solid #f3f4f6"
255
+ };
256
+ var fieldNameStyle = {
257
+ fontSize: 15,
258
+ fontWeight: 600,
259
+ color: "#111827"
260
+ };
261
+ var fieldDescStyle = {
262
+ fontSize: 13,
263
+ color: "#6b7280",
264
+ marginTop: 2
265
+ };
266
+ var codeName = {
267
+ fontSize: 12,
268
+ color: "#6366f1",
269
+ fontFamily: "monospace",
270
+ marginTop: 2
271
+ };
272
+ var badgeStyle = (type) => ({
273
+ display: "inline-block",
274
+ padding: "2px 8px",
275
+ fontSize: 11,
276
+ fontWeight: 600,
277
+ borderRadius: 9999,
278
+ whiteSpace: "nowrap",
279
+ backgroundColor: type === "metric" ? "#eff6ff" : type === "time_dimension" ? "#f0fdf4" : "#f3f4f6",
280
+ color: type === "metric" ? "#1d4ed8" : type === "time_dimension" ? "#15803d" : "#6b7280"
281
+ });
282
+ var copyBtn = {
283
+ padding: "4px 8px",
284
+ fontSize: 11,
285
+ fontWeight: 500,
286
+ color: "#6366f1",
287
+ backgroundColor: "#eef2ff",
288
+ border: "1px solid #c7d2fe",
289
+ borderRadius: 4,
290
+ cursor: "pointer",
291
+ whiteSpace: "nowrap"
292
+ };
293
+ var statsBar = {
294
+ display: "flex",
295
+ gap: 16,
296
+ marginBottom: 16,
297
+ fontSize: 14
298
+ };
299
+ var statItem = {
300
+ display: "flex",
301
+ alignItems: "center",
302
+ gap: 4,
303
+ color: "#6b7280"
304
+ };
305
+ var statNum = { fontWeight: 700, color: "#111827" };
306
+ var emptyStyle = {
307
+ padding: 40,
308
+ textAlign: "center",
309
+ color: "#9ca3af",
310
+ fontSize: 14
311
+ };
312
+ function DataCatalog({ className, showCopyButton = true, fieldTypes }) {
313
+ const { fields, metrics, dimensions, isLoading, error } = useMetrics();
314
+ const [search, setSearch] = useState("");
315
+ const [typeFilter, setTypeFilter] = useState("all");
316
+ const [copiedField, setCopiedField] = useState(null);
317
+ const filtered = useMemo(() => {
318
+ let result = fields;
319
+ if (fieldTypes) result = result.filter((f) => fieldTypes.includes(f.type));
320
+ if (typeFilter !== "all") result = result.filter((f) => f.type === typeFilter);
321
+ if (search) {
322
+ const q = search.toLowerCase();
323
+ result = result.filter(
324
+ (f) => f.displayName.toLowerCase().includes(q) || f.name.toLowerCase().includes(q) || f.description.toLowerCase().includes(q) || f.category.toLowerCase().includes(q)
325
+ );
326
+ }
327
+ return result;
328
+ }, [fields, fieldTypes, typeFilter, search]);
329
+ const grouped = useMemo(() => {
330
+ const map = /* @__PURE__ */ new Map();
331
+ for (const f of filtered) {
332
+ const list = map.get(f.category) || [];
333
+ list.push(f);
334
+ map.set(f.category, list);
335
+ }
336
+ return map;
337
+ }, [filtered]);
338
+ const handleCopy = useCallback(async (name) => {
339
+ try {
340
+ await navigator.clipboard.writeText(name);
341
+ setCopiedField(name);
342
+ setTimeout(() => setCopiedField(null), 1500);
343
+ } catch {
344
+ }
345
+ }, []);
346
+ if (isLoading) return /* @__PURE__ */ jsx("div", { style: { ...rootStyle, ...emptyStyle }, children: "Loading data catalog..." });
347
+ if (error) return /* @__PURE__ */ jsx("div", { style: { ...rootStyle, padding: 24, color: "#ef4444" }, children: error });
348
+ const timeDims = fields.filter((f) => f.type === "time_dimension");
349
+ return /* @__PURE__ */ jsxs("div", { style: rootStyle, className, children: [
350
+ /* @__PURE__ */ jsx("h1", { style: headingStyle, children: "Data Catalog" }),
351
+ /* @__PURE__ */ jsx("p", { style: subStyle, children: "Browse available metrics and dimensions from the semantic layer" }),
352
+ /* @__PURE__ */ jsxs("div", { style: statsBar, children: [
353
+ /* @__PURE__ */ jsxs("div", { style: statItem, children: [
354
+ /* @__PURE__ */ jsx("span", { style: statNum, children: metrics.length }),
355
+ " metrics"
356
+ ] }),
357
+ /* @__PURE__ */ jsxs("div", { style: statItem, children: [
358
+ /* @__PURE__ */ jsx("span", { style: statNum, children: dimensions.length - timeDims.length }),
359
+ " dimensions"
360
+ ] }),
361
+ /* @__PURE__ */ jsxs("div", { style: statItem, children: [
362
+ /* @__PURE__ */ jsx("span", { style: statNum, children: timeDims.length }),
363
+ " time dimensions"
364
+ ] })
365
+ ] }),
366
+ /* @__PURE__ */ jsxs("div", { style: searchBarStyle, children: [
367
+ /* @__PURE__ */ jsx(
368
+ "input",
369
+ {
370
+ style: searchInput,
371
+ placeholder: "Search by name, description, or category...",
372
+ value: search,
373
+ onChange: (e) => setSearch(e.target.value)
374
+ }
375
+ ),
376
+ /* @__PURE__ */ jsx("button", { style: filterBtn(typeFilter === "all"), onClick: () => setTypeFilter("all"), children: "All" }),
377
+ /* @__PURE__ */ jsx("button", { style: filterBtn(typeFilter === "metric"), onClick: () => setTypeFilter("metric"), children: "Metrics" }),
378
+ /* @__PURE__ */ jsx("button", { style: filterBtn(typeFilter === "dimension"), onClick: () => setTypeFilter("dimension"), children: "Dimensions" }),
379
+ /* @__PURE__ */ jsx("button", { style: filterBtn(typeFilter === "time_dimension"), onClick: () => setTypeFilter("time_dimension"), children: "Time" })
380
+ ] }),
381
+ Array.from(grouped.entries()).map(([category, categoryFields]) => /* @__PURE__ */ jsxs("div", { style: categoryStyle, children: [
382
+ /* @__PURE__ */ jsxs("div", { style: categoryHeader, children: [
383
+ category,
384
+ " (",
385
+ categoryFields.length,
386
+ ")"
387
+ ] }),
388
+ categoryFields.map((f) => /* @__PURE__ */ jsxs("div", { style: fieldCard, children: [
389
+ /* @__PURE__ */ jsxs("div", { style: { flex: 1 }, children: [
390
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
391
+ /* @__PURE__ */ jsx("span", { style: fieldNameStyle, children: f.displayName }),
392
+ /* @__PURE__ */ jsx("span", { style: badgeStyle(f.type), children: f.type })
393
+ ] }),
394
+ f.description && /* @__PURE__ */ jsx("div", { style: fieldDescStyle, children: f.description }),
395
+ /* @__PURE__ */ jsx("div", { style: codeName, children: f.name })
396
+ ] }),
397
+ showCopyButton && /* @__PURE__ */ jsx("button", { style: copyBtn, onClick: () => handleCopy(f.name), children: copiedField === f.name ? "Copied!" : "Copy name" })
398
+ ] }, f.id))
399
+ ] }, category)),
400
+ grouped.size === 0 && /* @__PURE__ */ jsx("div", { style: emptyStyle, children: search ? "No fields match your search." : "No data available. The gateway may not be configured yet." })
401
+ ] });
402
+ }
403
+ var rootStyle2 = {
404
+ fontFamily: "system-ui, -apple-system, sans-serif",
405
+ maxWidth: 960,
406
+ margin: "0 auto",
407
+ padding: 24
408
+ };
409
+ var headingStyle2 = {
410
+ fontSize: 24,
411
+ fontWeight: 700,
412
+ color: "#111827",
413
+ margin: "0 0 4px"
414
+ };
415
+ var subStyle2 = {
416
+ fontSize: 14,
417
+ color: "#6b7280",
418
+ margin: "0 0 20px"
419
+ };
420
+ var tabBarStyle = {
421
+ display: "flex",
422
+ gap: 0,
423
+ borderBottom: "1px solid #e5e7eb",
424
+ marginBottom: 24
425
+ };
426
+ var tabBase = {
427
+ padding: "10px 20px",
428
+ fontSize: 14,
429
+ fontWeight: 500,
430
+ cursor: "pointer",
431
+ background: "none",
432
+ border: "none",
433
+ borderBottom: "2px solid transparent",
434
+ color: "#6b7280"
435
+ };
436
+ var tabActive = {
437
+ ...tabBase,
438
+ color: "#111827",
439
+ borderBottomColor: "#3b82f6"
440
+ };
441
+ var tableStyle2 = {
442
+ width: "100%",
443
+ borderCollapse: "collapse",
444
+ fontSize: 14
445
+ };
446
+ var thStyle2 = {
447
+ padding: "10px 12px",
448
+ textAlign: "left",
449
+ fontWeight: 600,
450
+ fontSize: 12,
451
+ color: "#6b7280",
452
+ backgroundColor: "#f9fafb",
453
+ borderBottom: "1px solid #e5e7eb"
454
+ };
455
+ var tdStyle2 = {
456
+ padding: "8px 12px",
457
+ borderBottom: "1px solid #f3f4f6",
458
+ color: "#111827"
459
+ };
460
+ var btnPrimary = {
461
+ padding: "8px 16px",
462
+ fontSize: 14,
463
+ fontWeight: 500,
464
+ color: "#fff",
465
+ backgroundColor: "#3b82f6",
466
+ border: "none",
467
+ borderRadius: 6,
468
+ cursor: "pointer"
469
+ };
470
+ var btnDanger = {
471
+ ...btnPrimary,
472
+ backgroundColor: "#ef4444"
473
+ };
474
+ var btnSecondary = {
475
+ ...btnPrimary,
476
+ backgroundColor: "#fff",
477
+ color: "#374151",
478
+ border: "1px solid #d1d5db"
479
+ };
480
+ var inputStyle = {
481
+ padding: "6px 10px",
482
+ fontSize: 14,
483
+ border: "1px solid #d1d5db",
484
+ borderRadius: 6,
485
+ width: "100%",
486
+ boxSizing: "border-box"
487
+ };
488
+ var selectStyle = { ...inputStyle, width: "auto" };
489
+ var cardStyle = {
490
+ border: "1px solid #e5e7eb",
491
+ borderRadius: 8,
492
+ padding: 16,
493
+ marginBottom: 16,
494
+ backgroundColor: "#fff"
495
+ };
496
+ var errorBanner = {
497
+ padding: "10px 14px",
498
+ backgroundColor: "#fef2f2",
499
+ color: "#b91c1c",
500
+ borderRadius: 6,
501
+ fontSize: 14,
502
+ marginBottom: 16
503
+ };
504
+ var badgeStyle2 = {
505
+ display: "inline-block",
506
+ padding: "2px 8px",
507
+ fontSize: 11,
508
+ fontWeight: 600,
509
+ borderRadius: 9999,
510
+ backgroundColor: "#eff6ff",
511
+ color: "#1d4ed8"
512
+ };
513
+ var badgeGreen = { ...badgeStyle2, backgroundColor: "#f0fdf4", color: "#15803d" };
514
+ var badgeGray = { ...badgeStyle2, backgroundColor: "#f3f4f6", color: "#6b7280" };
515
+ var checkboxStyle = { width: 16, height: 16, cursor: "pointer" };
516
+ var emptyStyle2 = {
517
+ padding: 24,
518
+ textAlign: "center",
519
+ color: "#9ca3af",
520
+ fontSize: 14
521
+ };
522
+ function FieldsTab() {
523
+ const { fields, isLoading, error, createField, updateField, deleteField } = useAdminFields();
524
+ const [showAdd, setShowAdd] = useState(false);
525
+ const [newName, setNewName] = useState("");
526
+ const [newType, setNewType] = useState("metric");
527
+ const [newDisplay, setNewDisplay] = useState("");
528
+ const [newDesc, setNewDesc] = useState("");
529
+ const [actionError, setActionError] = useState(null);
530
+ const handleAdd = useCallback(async () => {
531
+ if (!newName.trim()) return;
532
+ setActionError(null);
533
+ try {
534
+ await createField({
535
+ field_name: newName.trim(),
536
+ field_type: newType,
537
+ display_name: newDisplay.trim() || void 0,
538
+ description: newDesc.trim() || void 0
539
+ });
540
+ setNewName("");
541
+ setNewDisplay("");
542
+ setNewDesc("");
543
+ setShowAdd(false);
544
+ } catch (err) {
545
+ setActionError(err instanceof Error ? err.message : "Failed to create field");
546
+ }
547
+ }, [newName, newType, newDisplay, newDesc, createField]);
548
+ const handleToggle = useCallback(async (f) => {
549
+ try {
550
+ await updateField({ id: f.id, is_active: !f.is_active });
551
+ } catch (err) {
552
+ setActionError(err instanceof Error ? err.message : "Failed to update");
553
+ }
554
+ }, [updateField]);
555
+ const handleDelete = useCallback(async (id) => {
556
+ if (!confirm("Delete this field? This also removes it from all roles.")) return;
557
+ try {
558
+ await deleteField(id);
559
+ } catch (err) {
560
+ setActionError(err instanceof Error ? err.message : "Failed to delete");
561
+ }
562
+ }, [deleteField]);
563
+ if (isLoading) return /* @__PURE__ */ jsx("div", { style: emptyStyle2, children: "Loading fields..." });
564
+ if (error) return /* @__PURE__ */ jsx("div", { style: errorBanner, children: error });
565
+ return /* @__PURE__ */ jsxs("div", { children: [
566
+ actionError && /* @__PURE__ */ jsx("div", { style: errorBanner, children: actionError }),
567
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }, children: [
568
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 14, color: "#6b7280" }, children: [
569
+ fields.length,
570
+ " curated field",
571
+ fields.length !== 1 ? "s" : ""
572
+ ] }),
573
+ /* @__PURE__ */ jsx("button", { style: btnPrimary, onClick: () => setShowAdd(!showAdd), children: showAdd ? "Cancel" : "+ Add field" })
574
+ ] }),
575
+ showAdd && /* @__PURE__ */ jsxs("div", { style: cardStyle, children: [
576
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr auto 1fr", gap: 8, marginBottom: 8 }, children: [
577
+ /* @__PURE__ */ jsx("input", { style: inputStyle, placeholder: "field_name (from dbt)", value: newName, onChange: (e) => setNewName(e.target.value) }),
578
+ /* @__PURE__ */ jsxs("select", { style: selectStyle, value: newType, onChange: (e) => setNewType(e.target.value), children: [
579
+ /* @__PURE__ */ jsx("option", { value: "metric", children: "metric" }),
580
+ /* @__PURE__ */ jsx("option", { value: "dimension", children: "dimension" }),
581
+ /* @__PURE__ */ jsx("option", { value: "time_dimension", children: "time_dimension" })
582
+ ] }),
583
+ /* @__PURE__ */ jsx("input", { style: inputStyle, placeholder: "Display name (optional)", value: newDisplay, onChange: (e) => setNewDisplay(e.target.value) })
584
+ ] }),
585
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8 }, children: [
586
+ /* @__PURE__ */ jsx("input", { style: { ...inputStyle, flex: 1 }, placeholder: "Description (optional)", value: newDesc, onChange: (e) => setNewDesc(e.target.value), onKeyDown: (e) => e.key === "Enter" && handleAdd() }),
587
+ /* @__PURE__ */ jsx("button", { style: btnPrimary, onClick: handleAdd, children: "Save" })
588
+ ] })
589
+ ] }),
590
+ /* @__PURE__ */ jsxs("table", { style: tableStyle2, children: [
591
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
592
+ /* @__PURE__ */ jsx("th", { style: thStyle2, children: "Field Name" }),
593
+ /* @__PURE__ */ jsx("th", { style: thStyle2, children: "Type" }),
594
+ /* @__PURE__ */ jsx("th", { style: thStyle2, children: "Display Name" }),
595
+ /* @__PURE__ */ jsx("th", { style: thStyle2, children: "Active" }),
596
+ /* @__PURE__ */ jsx("th", { style: { ...thStyle2, textAlign: "right" }, children: "Actions" })
597
+ ] }) }),
598
+ /* @__PURE__ */ jsxs("tbody", { children: [
599
+ fields.map((f) => /* @__PURE__ */ jsxs("tr", { children: [
600
+ /* @__PURE__ */ jsx("td", { style: tdStyle2, children: /* @__PURE__ */ jsx("code", { style: { fontSize: 13 }, children: f.field_name }) }),
601
+ /* @__PURE__ */ jsx("td", { style: tdStyle2, children: /* @__PURE__ */ jsx("span", { style: f.field_type === "metric" ? badgeStyle2 : f.field_type === "time_dimension" ? badgeGreen : badgeGray, children: f.field_type }) }),
602
+ /* @__PURE__ */ jsx("td", { style: tdStyle2, children: f.display_name || /* @__PURE__ */ jsx("span", { style: { color: "#9ca3af" }, children: "\u2014" }) }),
603
+ /* @__PURE__ */ jsx("td", { style: tdStyle2, children: /* @__PURE__ */ jsx("input", { type: "checkbox", checked: f.is_active, onChange: () => handleToggle(f), style: checkboxStyle }) }),
604
+ /* @__PURE__ */ jsx("td", { style: { ...tdStyle2, textAlign: "right" }, children: /* @__PURE__ */ jsx("button", { style: { ...btnDanger, padding: "4px 10px", fontSize: 12 }, onClick: () => handleDelete(f.id), children: "Delete" }) })
605
+ ] }, f.id)),
606
+ fields.length === 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { colSpan: 5, style: emptyStyle2, children: "No curated fields yet. Add fields to start restricting access." }) })
607
+ ] })
608
+ ] })
609
+ ] });
610
+ }
611
+ function RolesTab() {
612
+ const { roles, isLoading, error, createRole, deleteRole, setRoleFields } = useAdminRoles();
613
+ const { fields } = useAdminFields();
614
+ const [showAdd, setShowAdd] = useState(false);
615
+ const [newName, setNewName] = useState("");
616
+ const [newDesc, setNewDesc] = useState("");
617
+ const [editingRole, setEditingRole] = useState(null);
618
+ const [actionError, setActionError] = useState(null);
619
+ const handleAdd = useCallback(async () => {
620
+ if (!newName.trim()) return;
621
+ setActionError(null);
622
+ try {
623
+ await createRole({ name: newName.trim(), description: newDesc.trim() || void 0 });
624
+ setNewName("");
625
+ setNewDesc("");
626
+ setShowAdd(false);
627
+ } catch (err) {
628
+ setActionError(err instanceof Error ? err.message : "Failed to create role");
629
+ }
630
+ }, [newName, newDesc, createRole]);
631
+ const handleDelete = useCallback(async (id) => {
632
+ if (!confirm("Delete this role? Users assigned to it will lose this role's access.")) return;
633
+ try {
634
+ await deleteRole(id);
635
+ } catch (err) {
636
+ setActionError(err instanceof Error ? err.message : "Failed to delete");
637
+ }
638
+ }, [deleteRole]);
639
+ const getFieldIds = (role) => new Set((role.role_field_access || []).map((rfa) => rfa.curated_field_id));
640
+ const handleToggleField = useCallback(async (role, fieldId) => {
641
+ const current = getFieldIds(role);
642
+ if (current.has(fieldId)) current.delete(fieldId);
643
+ else current.add(fieldId);
644
+ setActionError(null);
645
+ try {
646
+ await setRoleFields(role.id, Array.from(current));
647
+ } catch (err) {
648
+ setActionError(err instanceof Error ? err.message : "Failed to update");
649
+ }
650
+ }, [setRoleFields]);
651
+ if (isLoading) return /* @__PURE__ */ jsx("div", { style: emptyStyle2, children: "Loading roles..." });
652
+ if (error) return /* @__PURE__ */ jsx("div", { style: errorBanner, children: error });
653
+ return /* @__PURE__ */ jsxs("div", { children: [
654
+ actionError && /* @__PURE__ */ jsx("div", { style: errorBanner, children: actionError }),
655
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }, children: [
656
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 14, color: "#6b7280" }, children: [
657
+ roles.length,
658
+ " role",
659
+ roles.length !== 1 ? "s" : ""
660
+ ] }),
661
+ /* @__PURE__ */ jsx("button", { style: btnPrimary, onClick: () => setShowAdd(!showAdd), children: showAdd ? "Cancel" : "+ Add role" })
662
+ ] }),
663
+ showAdd && /* @__PURE__ */ jsx("div", { style: cardStyle, children: /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8 }, children: [
664
+ /* @__PURE__ */ jsx("input", { style: { ...inputStyle, flex: 1 }, placeholder: "Role name", value: newName, onChange: (e) => setNewName(e.target.value) }),
665
+ /* @__PURE__ */ jsx("input", { style: { ...inputStyle, flex: 2 }, placeholder: "Description (optional)", value: newDesc, onChange: (e) => setNewDesc(e.target.value), onKeyDown: (e) => e.key === "Enter" && handleAdd() }),
666
+ /* @__PURE__ */ jsx("button", { style: btnPrimary, onClick: handleAdd, children: "Save" })
667
+ ] }) }),
668
+ roles.map((role) => {
669
+ const assignedIds = getFieldIds(role);
670
+ const isEditing = editingRole === role.id;
671
+ return /* @__PURE__ */ jsxs("div", { style: cardStyle, children: [
672
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: isEditing ? 12 : 0 }, children: [
673
+ /* @__PURE__ */ jsxs("div", { children: [
674
+ /* @__PURE__ */ jsx("strong", { style: { fontSize: 16 }, children: role.name }),
675
+ role.is_default && /* @__PURE__ */ jsx("span", { style: { ...badgeGreen, marginLeft: 8 }, children: "default" }),
676
+ /* @__PURE__ */ jsxs("span", { style: { ...badgeGray, marginLeft: 8 }, children: [
677
+ assignedIds.size,
678
+ " field",
679
+ assignedIds.size !== 1 ? "s" : ""
680
+ ] }),
681
+ role.description && /* @__PURE__ */ jsx("div", { style: { fontSize: 13, color: "#6b7280", marginTop: 2 }, children: role.description })
682
+ ] }),
683
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8 }, children: [
684
+ /* @__PURE__ */ jsx("button", { style: btnSecondary, onClick: () => setEditingRole(isEditing ? null : role.id), children: isEditing ? "Done" : "Edit fields" }),
685
+ !role.is_default && /* @__PURE__ */ jsx("button", { style: { ...btnDanger, padding: "6px 12px", fontSize: 12 }, onClick: () => handleDelete(role.id), children: "Delete" })
686
+ ] })
687
+ ] }),
688
+ isEditing && fields.length > 0 && /* @__PURE__ */ jsx("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(240px, 1fr))", gap: 4 }, children: fields.filter((f) => f.is_active).map((f) => /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: 8, padding: "4px 0", fontSize: 13, cursor: "pointer" }, children: [
689
+ /* @__PURE__ */ jsx("input", { type: "checkbox", checked: assignedIds.has(f.id), onChange: () => handleToggleField(role, f.id), style: checkboxStyle }),
690
+ /* @__PURE__ */ jsx("span", { children: f.display_name || f.field_name }),
691
+ /* @__PURE__ */ jsx("span", { style: f.field_type === "metric" ? badgeStyle2 : badgeGray, children: f.field_type })
692
+ ] }, f.id)) }),
693
+ isEditing && fields.length === 0 && /* @__PURE__ */ jsx("div", { style: emptyStyle2, children: "No curated fields defined yet. Add fields in the Fields tab first." })
694
+ ] }, role.id);
695
+ }),
696
+ roles.length === 0 && /* @__PURE__ */ jsx("div", { style: emptyStyle2, children: "No roles defined." })
697
+ ] });
698
+ }
699
+ function UsersTab() {
700
+ const { users, isLoading, error, assignUserRole, removeUserRole } = useAdminUsers();
701
+ const { roles } = useAdminRoles();
702
+ const [showAdd, setShowAdd] = useState(false);
703
+ const [newUserId, setNewUserId] = useState("");
704
+ const [newEmail, setNewEmail] = useState("");
705
+ const [newRoleId, setNewRoleId] = useState(0);
706
+ const [newIsAdmin, setNewIsAdmin] = useState(false);
707
+ const [actionError, setActionError] = useState(null);
708
+ const handleAdd = useCallback(async () => {
709
+ if (!newUserId.trim() || !newEmail.trim() || !newRoleId) return;
710
+ setActionError(null);
711
+ try {
712
+ await assignUserRole({ user_id: newUserId.trim(), email: newEmail.trim(), role_id: newRoleId, is_admin: newIsAdmin });
713
+ setNewUserId("");
714
+ setNewEmail("");
715
+ setNewIsAdmin(false);
716
+ setShowAdd(false);
717
+ } catch (err) {
718
+ setActionError(err instanceof Error ? err.message : "Failed to assign role");
719
+ }
720
+ }, [newUserId, newEmail, newRoleId, newIsAdmin, assignUserRole]);
721
+ const handleRemove = useCallback(async (u) => {
722
+ if (!confirm(`Remove ${u.email} from this role?`)) return;
723
+ try {
724
+ await removeUserRole({ user_id: u.user_id, role_id: u.role_id });
725
+ } catch (err) {
726
+ setActionError(err instanceof Error ? err.message : "Failed to remove");
727
+ }
728
+ }, [removeUserRole]);
729
+ const grouped = useMemo(() => {
730
+ const map = /* @__PURE__ */ new Map();
731
+ for (const u of users) {
732
+ const list = map.get(u.email) || [];
733
+ list.push(u);
734
+ map.set(u.email, list);
735
+ }
736
+ return map;
737
+ }, [users]);
738
+ if (isLoading) return /* @__PURE__ */ jsx("div", { style: emptyStyle2, children: "Loading users..." });
739
+ if (error) return /* @__PURE__ */ jsx("div", { style: errorBanner, children: error });
740
+ return /* @__PURE__ */ jsxs("div", { children: [
741
+ actionError && /* @__PURE__ */ jsx("div", { style: errorBanner, children: actionError }),
742
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }, children: [
743
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 14, color: "#6b7280" }, children: [
744
+ grouped.size,
745
+ " user",
746
+ grouped.size !== 1 ? "s" : "",
747
+ " with role assignments"
748
+ ] }),
749
+ /* @__PURE__ */ jsx("button", { style: btnPrimary, onClick: () => setShowAdd(!showAdd), children: showAdd ? "Cancel" : "+ Assign user" })
750
+ ] }),
751
+ showAdd && /* @__PURE__ */ jsxs("div", { style: cardStyle, children: [
752
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr auto", gap: 8, marginBottom: 8 }, children: [
753
+ /* @__PURE__ */ jsx("input", { style: inputStyle, placeholder: "User UUID (from Supabase Auth)", value: newUserId, onChange: (e) => setNewUserId(e.target.value) }),
754
+ /* @__PURE__ */ jsx("input", { style: inputStyle, placeholder: "Email", value: newEmail, onChange: (e) => setNewEmail(e.target.value) }),
755
+ /* @__PURE__ */ jsxs("select", { style: selectStyle, value: newRoleId, onChange: (e) => setNewRoleId(Number(e.target.value)), children: [
756
+ /* @__PURE__ */ jsx("option", { value: 0, children: "Select role..." }),
757
+ roles.map((r) => /* @__PURE__ */ jsx("option", { value: r.id, children: r.name }, r.id))
758
+ ] })
759
+ ] }),
760
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
761
+ /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: 6, fontSize: 13, cursor: "pointer" }, children: [
762
+ /* @__PURE__ */ jsx("input", { type: "checkbox", checked: newIsAdmin, onChange: (e) => setNewIsAdmin(e.target.checked), style: checkboxStyle }),
763
+ "Admin access"
764
+ ] }),
765
+ /* @__PURE__ */ jsx("div", { style: { flex: 1 } }),
766
+ /* @__PURE__ */ jsx("button", { style: btnPrimary, onClick: handleAdd, children: "Assign" })
767
+ ] })
768
+ ] }),
769
+ Array.from(grouped.entries()).map(([email, assignments]) => /* @__PURE__ */ jsxs("div", { style: cardStyle, children: [
770
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
771
+ /* @__PURE__ */ jsxs("div", { children: [
772
+ /* @__PURE__ */ jsx("strong", { style: { fontSize: 14 }, children: email }),
773
+ assignments.some((a) => a.is_admin) && /* @__PURE__ */ jsx("span", { style: { ...badgeGreen, marginLeft: 8 }, children: "admin" })
774
+ ] }),
775
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 12, color: "#9ca3af", fontFamily: "monospace" }, children: [
776
+ assignments[0].user_id.slice(0, 8),
777
+ "..."
778
+ ] })
779
+ ] }),
780
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 6, marginTop: 8, flexWrap: "wrap" }, children: assignments.map((a) => /* @__PURE__ */ jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: 4, ...badgeStyle2 }, children: [
781
+ a.access_roles?.name || `role #${a.role_id}`,
782
+ /* @__PURE__ */ jsx(
783
+ "button",
784
+ {
785
+ onClick: () => handleRemove(a),
786
+ style: { background: "none", border: "none", cursor: "pointer", color: "#6b7280", fontSize: 14, padding: 0, lineHeight: 1 },
787
+ title: "Remove role",
788
+ children: "\xD7"
789
+ }
790
+ )
791
+ ] }, a.id)) })
792
+ ] }, email)),
793
+ grouped.size === 0 && /* @__PURE__ */ jsx("div", { style: emptyStyle2, children: "No user role assignments yet. Users without an explicit role get the default role's access." })
794
+ ] });
795
+ }
796
+ function AdminDashboard({ className }) {
797
+ const { isAuthenticated, isLoading: authLoading } = useAuth();
798
+ const [tab, setTab] = useState("fields");
799
+ const [initError, setInitError] = useState(null);
800
+ if (authLoading) return /* @__PURE__ */ jsx("div", { style: { ...rootStyle2, ...emptyStyle2 }, children: "Loading..." });
801
+ if (!isAuthenticated) return /* @__PURE__ */ jsx("div", { style: { ...rootStyle2, ...emptyStyle2 }, children: "Sign in to access the admin dashboard." });
802
+ return /* @__PURE__ */ jsxs("div", { style: rootStyle2, className, children: [
803
+ /* @__PURE__ */ jsx("h1", { style: headingStyle2, children: "Semantic Layer Admin" }),
804
+ /* @__PURE__ */ jsx("p", { style: subStyle2, children: "Manage curated fields, roles, and user access" }),
805
+ initError && /* @__PURE__ */ jsx("div", { style: errorBanner, children: initError }),
806
+ /* @__PURE__ */ jsx("div", { style: tabBarStyle, children: ["fields", "roles", "users"].map((t) => /* @__PURE__ */ jsx("button", { style: tab === t ? tabActive : tabBase, onClick: () => {
807
+ setTab(t);
808
+ setInitError(null);
809
+ }, children: t.charAt(0).toUpperCase() + t.slice(1) }, t)) }),
810
+ tab === "fields" && /* @__PURE__ */ jsx(FieldsTab, {}),
811
+ tab === "roles" && /* @__PURE__ */ jsx(RolesTab, {}),
812
+ tab === "users" && /* @__PURE__ */ jsx(UsersTab, {})
813
+ ] });
814
+ }
192
815
 
193
- export { MetricPicker, ResultsTable };
816
+ export { AdminDashboard, DataCatalog, MetricPicker, ResultsTable };
194
817
  //# sourceMappingURL=components.js.map
195
818
  //# sourceMappingURL=components.js.map