@reddb-io/ui 0.3.0 → 0.3.2

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.
Files changed (121) hide show
  1. package/build/_app/immutable/assets/0.BWZTMKZW.css +1 -0
  2. package/build/_app/immutable/assets/AnalyticsView.BLAJezw4.css +1 -0
  3. package/build/_app/immutable/assets/{Workspace.CvP5De7i.css → Workspace.DM2E1WiF.css} +1 -1
  4. package/build/_app/immutable/chunks/{C9rXzYLu.js → B-yl15ta.js} +1 -1
  5. package/build/_app/immutable/chunks/B5euRbKa.js +1 -0
  6. package/build/_app/immutable/chunks/BRIMmWzq.js +1 -0
  7. package/build/_app/immutable/chunks/BgOTHNR5.js +1 -0
  8. package/build/_app/immutable/chunks/Bgb6fHGb.js +18 -0
  9. package/build/_app/immutable/chunks/Btd6PFkC.js +1 -0
  10. package/build/_app/immutable/chunks/C-kqMbRi.js +1 -0
  11. package/build/_app/immutable/chunks/C3SGj6Qd.js +2 -0
  12. package/build/_app/immutable/chunks/{C278awGv.js → C7kf23SQ.js} +1 -1
  13. package/build/_app/immutable/chunks/CGFL56WJ.js +1 -0
  14. package/build/_app/immutable/chunks/CHvQYbpl.js +1 -0
  15. package/build/_app/immutable/chunks/CO7K4bD2.js +1164 -0
  16. package/build/_app/immutable/chunks/CP65IkJc.js +1 -0
  17. package/build/_app/immutable/chunks/CxnvJShw.js +404 -0
  18. package/build/_app/immutable/chunks/D02PB2u7.js +1 -0
  19. package/build/_app/immutable/chunks/DCJ0QyYH.js +1 -0
  20. package/build/_app/immutable/chunks/DN7v3Tar.js +2 -0
  21. package/build/_app/immutable/chunks/DRlh90Cv.js +1 -0
  22. package/build/_app/immutable/entry/app.BeX4Tmu5.js +2 -0
  23. package/build/_app/immutable/entry/start.D7y1MZex.js +1 -0
  24. package/build/_app/immutable/nodes/0.C0e5KQVb.js +2 -0
  25. package/build/_app/immutable/nodes/1.ceiVuZbB.js +1 -0
  26. package/build/_app/immutable/nodes/10.DeFqKRR2.js +96 -0
  27. package/build/_app/immutable/nodes/12.CzMvqoYS.js +3 -0
  28. package/build/_app/immutable/nodes/14.CAG5_cGP.js +1 -0
  29. package/build/_app/immutable/nodes/15.CRDBqbiH.js +60 -0
  30. package/build/_app/immutable/nodes/8.CAG5_cGP.js +1 -0
  31. package/build/_app/version.json +1 -1
  32. package/build/index.html +18 -15
  33. package/dist/embed/AnalyticsView-SKyGxeAx.js +23337 -0
  34. package/dist/embed/AnalyticsView-SKyGxeAx.js.map +1 -0
  35. package/dist/embed/RedUiEmbedRoot-BzVzGAFK.js +31960 -0
  36. package/dist/embed/RedUiEmbedRoot-BzVzGAFK.js.map +1 -0
  37. package/dist/embed/event-BvRbkLjD.js +46 -0
  38. package/dist/embed/event-BvRbkLjD.js.map +1 -0
  39. package/dist/embed/index-0hA141D2.js +85 -0
  40. package/dist/embed/index-0hA141D2.js.map +1 -0
  41. package/dist/embed/index-BVxMkfHe.js +8 -0
  42. package/dist/embed/index-BVxMkfHe.js.map +1 -0
  43. package/dist/embed/index-BY6fc1oQ.js +856 -0
  44. package/dist/embed/index-BY6fc1oQ.js.map +1 -0
  45. package/dist/embed/index-client-BHUUkI8H.js +3304 -0
  46. package/dist/embed/index-client-BHUUkI8H.js.map +1 -0
  47. package/dist/embed/index.js +1 -1
  48. package/dist/embed/{sigma.esm-D5yoExsH.js → sigma.esm-BawyGZ3s.js} +2 -2
  49. package/dist/embed/{sigma.esm-D5yoExsH.js.map → sigma.esm-BawyGZ3s.js.map} +1 -1
  50. package/dist/embed/window-ZR8_mM8o.js +1889 -0
  51. package/dist/embed/window-ZR8_mM8o.js.map +1 -0
  52. package/package.json +7 -3
  53. package/src/app.css +86 -20
  54. package/src/lib/AnalyticsView.svelte +209 -0
  55. package/src/lib/AskView.svelte +255 -0
  56. package/src/lib/ClusterView.svelte +415 -95
  57. package/src/lib/CommandPalette.svelte +2 -0
  58. package/src/lib/ConnectDropdown.svelte +60 -3
  59. package/src/lib/DevConsole.svelte +155 -0
  60. package/src/lib/ErrorBoundary.fixture.svelte +11 -0
  61. package/src/lib/ErrorBoundary.fixtures.svelte.ts +5 -0
  62. package/src/lib/ErrorBoundary.svelte +78 -0
  63. package/src/lib/ErrorBoundaryCrashChild.fixture.svelte +9 -0
  64. package/src/lib/GlobalProgressBar.svelte +9 -0
  65. package/src/lib/LoadingOverlay.svelte +109 -0
  66. package/src/lib/MetricChart.svelte +85 -0
  67. package/src/lib/QueryEditor.svelte +48 -7
  68. package/src/lib/ResultsPane.svelte +76 -8
  69. package/src/lib/SchemaTree.svelte +25 -1
  70. package/src/lib/SecurityView.svelte +176 -65
  71. package/src/lib/SettingsView.svelte +1111 -254
  72. package/src/lib/StatusBar.svelte +19 -1
  73. package/src/lib/Topbar.svelte +29 -1
  74. package/src/lib/Workspace.svelte +136 -5
  75. package/src/lib/activity.svelte.ts +44 -35
  76. package/src/lib/collection-preview.ts +14 -0
  77. package/src/lib/connections.svelte.ts +167 -4
  78. package/src/lib/embed/RedUiEmbedRoot.svelte +13 -8
  79. package/src/lib/embed/index.ts +34 -1
  80. package/src/lib/index.ts +10 -0
  81. package/src/lib/loading.svelte.ts +142 -0
  82. package/src/lib/permission-gate.ts +120 -0
  83. package/src/lib/policies.ts +136 -0
  84. package/src/lib/query-tabs.svelte.ts +132 -53
  85. package/src/lib/reddb/analytics.ts +208 -0
  86. package/src/lib/reddb/ask.ts +123 -0
  87. package/src/lib/reddb/client.ts +292 -73
  88. package/src/lib/reddb/cluster-status.ts +306 -0
  89. package/src/lib/reddb/desktop-error.ts +89 -0
  90. package/src/lib/reddb/dev-console.ts +186 -0
  91. package/src/lib/reddb/index.ts +5 -0
  92. package/src/lib/reddb/local-url-provider.ts +28 -1
  93. package/src/lib/reddb/redwire-ws-spike.ts +590 -0
  94. package/src/lib/reddb/tauri-encrypted-store.ts +4 -9
  95. package/src/lib/router.svelte.ts +12 -0
  96. package/src/lib/settings-authoring-client.ts +336 -0
  97. package/src/lib/settings-sections.ts +292 -196
  98. package/src/lib/tabs.svelte.ts +5 -0
  99. package/src/lib/workspace-session.ts +81 -0
  100. package/build/_app/immutable/assets/0.DQDLkSdR.css +0 -1
  101. package/build/_app/immutable/chunks/65XjEwaF.js +0 -367
  102. package/build/_app/immutable/chunks/BGcIe2ep.js +0 -2
  103. package/build/_app/immutable/chunks/BjFADNkA.js +0 -992
  104. package/build/_app/immutable/chunks/BrvvOVaJ.js +0 -1
  105. package/build/_app/immutable/chunks/D9CTkMjK.js +0 -1
  106. package/build/_app/immutable/chunks/oiO4RLqp.js +0 -1
  107. package/build/_app/immutable/chunks/pDJnDxXM.js +0 -1
  108. package/build/_app/immutable/entry/app.DdaMXHAv.js +0 -2
  109. package/build/_app/immutable/entry/start.-MLBtQXB.js +0 -1
  110. package/build/_app/immutable/nodes/0.Ysoz3Y3h.js +0 -1
  111. package/build/_app/immutable/nodes/1.C2jbGTOJ.js +0 -1
  112. package/build/_app/immutable/nodes/12.DQG5zpEx.js +0 -60
  113. package/build/_app/immutable/nodes/8.CrHI4F9p.js +0 -134
  114. package/dist/embed/RedUiEmbedRoot-BwFUA5X-.js +0 -28624
  115. package/dist/embed/RedUiEmbedRoot-BwFUA5X-.js.map +0 -1
  116. package/dist/embed/index-DGhgtnaK.js +0 -577
  117. package/dist/embed/index-DGhgtnaK.js.map +0 -1
  118. package/dist/embed/index-client-ItCEU64C.js +0 -3087
  119. package/dist/embed/index-client-ItCEU64C.js.map +0 -1
  120. /package/build/_app/immutable/assets/{12.Dblyk0Kr.css → 15.Dblyk0Kr.css} +0 -0
  121. /package/build/_app/immutable/nodes/{10.CAG5_cGP.js → 13.CAG5_cGP.js} +0 -0
@@ -7,6 +7,7 @@
7
7
  import { tabs, type Tab } from '$lib/tabs.svelte'
8
8
  import { queryTabs } from '$lib/query-tabs.svelte'
9
9
  import { defaultSubpage, subpageCapability } from '$lib/collection-pages'
10
+ import { collectionPreviewQuery, safeCollectionName } from '$lib/collection-preview'
10
11
  import {
11
12
  collectionCatalogBadges,
12
13
  collectionCatalogFromRow,
@@ -38,6 +39,8 @@
38
39
  { value: 'document', label: 'Document' },
39
40
  { value: 'json', label: 'JSON' },
40
41
  ]
42
+ const resultsSkeletonRows = Array.from({ length: 14 }, (_, i) => i)
43
+ const resultsSkeletonColumns = Array.from({ length: 6 }, (_, i) => i)
41
44
 
42
45
  let results = $state<Record<string, QueryResult | null>>({})
43
46
  let errors = $state<Record<string, string>>({})
@@ -169,10 +172,6 @@
169
172
  }
170
173
  }
171
174
 
172
- function safeCollectionName(collection: string): string {
173
- return collection.replace(/[^A-Za-z0-9_./-]/g, '')
174
- }
175
-
176
175
  function sqlString(s: string): string {
177
176
  return `'${s.replace(/'/g, "''")}'`
178
177
  }
@@ -240,7 +239,8 @@
240
239
  const shouldFetchQueue = subpage === 'queue' || tab.capability === 'queue'
241
240
  const shouldFetchVector = subpage === 'vector' || tab.capability === 'vector'
242
241
  const shouldFetchStats = subpage === 'stats' || tab.capability === 'stats'
243
- const fetchMode = shouldFetchGraph ? 'graph-subgraph' : shouldFetchQueue ? 'queue-peek' : shouldFetchVector ? 'vector-inspector' : shouldFetchStats ? 'stats-info' : tab.capability ?? 'unknown'
242
+ const shouldFetchKv = subpage === 'kv' || tab.capability === 'kv'
243
+ const fetchMode = shouldFetchGraph ? 'graph-subgraph' : shouldFetchQueue ? 'queue-peek' : shouldFetchVector ? 'vector-inspector' : shouldFetchStats ? 'stats-info' : shouldFetchKv ? 'kv-list' : tab.capability ?? 'unknown'
244
244
  const cacheKey = `${tab.key}:${subpage}:${fetchMode}`
245
245
  if (resultKeys[tab.id] && resultKeys[tab.id] !== cacheKey) {
246
246
  delete results[tab.id]
@@ -267,9 +267,14 @@
267
267
  `${collection} · stats preview`,
268
268
  () => fetchStatsCollection(client, collection),
269
269
  )
270
+ : shouldFetchKv
271
+ ? activity.track(
272
+ `${collection} · LIST KV (LIMIT 200)`,
273
+ () => client.query(collectionPreviewQuery(collection, 'kv')),
274
+ )
270
275
  : activity.track(
271
276
  `${collection} · SELECT * (LIMIT 200)`,
272
- () => client.query(`SELECT * FROM ${collection} LIMIT 200`),
277
+ () => client.query(collectionPreviewQuery(collection, tab.capability)),
273
278
  )
274
279
  job
275
280
  .then((r) => { results[tab.id] = r })
@@ -588,8 +593,71 @@
588
593
  <pre class="whitespace-pre-wrap">{err}</pre>
589
594
  </div>
590
595
  {:else if !result}
591
- <div class="h-full grid place-items-center text-fg-3 text-[12px] font-mono">
592
- Loading {tab.label}…
596
+ <div class="h-full flex flex-col" aria-busy="true" aria-label={`Loading ${tab.label} results`}>
597
+ <div class="border-b border-line-1 bg-bg-1/35 px-3 py-2">
598
+ <div class="flex flex-wrap items-center gap-x-4 gap-y-2">
599
+ <div class="min-w-[180px] flex-1">
600
+ <div class="flex items-center gap-2">
601
+ <div class="h-[21px] w-[78px] rounded border border-line-1 bg-bg-2 motion-safe:animate-pulse"></div>
602
+ <div class="h-4 w-44 max-w-[45vw] rounded bg-bg-2 motion-safe:animate-pulse"></div>
603
+ </div>
604
+ <div class="mt-1 flex flex-wrap gap-1">
605
+ <div class="h-[21px] w-24 rounded border border-line-1 bg-bg-0/70 motion-safe:animate-pulse"></div>
606
+ <div class="h-[21px] w-32 rounded border border-line-1 bg-bg-0/70 motion-safe:animate-pulse"></div>
607
+ <div class="h-[21px] w-20 rounded border border-line-1 bg-bg-0/70 motion-safe:animate-pulse"></div>
608
+ </div>
609
+ </div>
610
+
611
+ <div class="flex flex-wrap items-center gap-2">
612
+ <div class="h-[30px] w-[94px] rounded border border-line-1 bg-bg-0/70 motion-safe:animate-pulse"></div>
613
+ <div class="h-[30px] w-[92px] rounded border border-line-1 bg-bg-0/70 motion-safe:animate-pulse"></div>
614
+ <div class="h-[30px] w-[92px] rounded border border-line-1 bg-bg-0/70 motion-safe:animate-pulse"></div>
615
+ <div class="h-[30px] w-[78px] rounded border border-line-1 bg-bg-0/70 motion-safe:animate-pulse"></div>
616
+ <div class="h-[30px] w-[78px] rounded border border-line-1 bg-bg-0/70 motion-safe:animate-pulse"></div>
617
+ </div>
618
+ </div>
619
+ </div>
620
+
621
+ <div class="border-b border-line-1 px-3 py-1.5 flex items-center gap-2 bg-bg-1/40">
622
+ <div class="h-6 w-[70px] rounded border border-line-1 bg-bg-2 motion-safe:animate-pulse"></div>
623
+ <div class="h-6 w-[54px] rounded border border-line-1 bg-bg-2 motion-safe:animate-pulse"></div>
624
+ <div class="h-4 w-px bg-line-1"></div>
625
+ <div class="h-6 w-[76px] rounded border border-line-1 bg-bg-2 motion-safe:animate-pulse"></div>
626
+ <div class="ml-auto h-3 w-[58px] rounded bg-bg-2 motion-safe:animate-pulse"></div>
627
+ </div>
628
+
629
+ <div class="flex-1 overflow-hidden">
630
+ <div class="min-w-[760px]">
631
+ <div class="grid h-8 grid-cols-[40px_repeat(6,minmax(96px,1fr))] border-b border-line-1 bg-bg-1">
632
+ <div class="px-2 py-2">
633
+ <div class="ml-auto h-3 w-4 rounded bg-bg-2 motion-safe:animate-pulse"></div>
634
+ </div>
635
+ {#each resultsSkeletonColumns as col (col)}
636
+ <div class="px-2 py-2">
637
+ <div class="h-3 rounded bg-bg-2 motion-safe:animate-pulse" style={`width: ${col % 3 === 0 ? 72 : col % 3 === 1 ? 104 : 56}px`}></div>
638
+ </div>
639
+ {/each}
640
+ </div>
641
+ {#each resultsSkeletonRows as row (row)}
642
+ <div class="grid h-[29px] grid-cols-[40px_repeat(6,minmax(96px,1fr))] border-b border-line-1/60">
643
+ <div class="px-2 py-2">
644
+ <div class="ml-auto h-3 w-4 rounded bg-bg-2 motion-safe:animate-pulse"></div>
645
+ </div>
646
+ {#each resultsSkeletonColumns as col (col)}
647
+ <div class="px-2 py-2">
648
+ <div class="h-3 rounded bg-bg-2 motion-safe:animate-pulse" style={`width: ${col === 0 ? 48 : 64 + ((row + col) % 4) * 18}px`}></div>
649
+ </div>
650
+ {/each}
651
+ </div>
652
+ {/each}
653
+ </div>
654
+ </div>
655
+
656
+ <div class="border-t border-line-1 px-3 py-1.5 flex items-center gap-3">
657
+ <div class="h-3 w-28 rounded bg-bg-2 motion-safe:animate-pulse"></div>
658
+ <div class="h-3 w-44 rounded bg-bg-2 motion-safe:animate-pulse"></div>
659
+ <div class="h-3 w-20 rounded bg-bg-2 motion-safe:animate-pulse"></div>
660
+ </div>
593
661
  </div>
594
662
  {:else}
595
663
  {@const renderer = pickRenderer(tab, result)}
@@ -27,6 +27,8 @@
27
27
  let error = $state<string | null>(null)
28
28
  let search = $state('')
29
29
  let capabilityFilter = $state<Capability | 'all'>('all')
30
+ const skeletonRows = Array.from({ length: 10 }, (_, i) => i)
31
+ const skeletonChips = Array.from({ length: 4 }, (_, i) => i)
30
32
 
31
33
  const connected = $derived(connection.connected)
32
34
  const activeUrl = $derived(connection.active.url)
@@ -113,7 +115,29 @@
113
115
  <div class="mt-1 break-words">{error}</div>
114
116
  </div>
115
117
  {:else if loading && items.length === 0}
116
- <div class="p-3 text-[12px] text-fg-3 font-mono">Loading…</div>
118
+ <div class="schema-skeleton" aria-busy="true" aria-label="Loading schema">
119
+ <div class="p-2 border-b border-line-1 grid gap-2">
120
+ <div class="flex items-center gap-1.5 h-7 rounded border border-line-1 bg-bg-0 px-2">
121
+ <Search class="size-3 text-fg-3" />
122
+ <div class="h-3 w-32 rounded bg-bg-2 motion-safe:animate-pulse"></div>
123
+ </div>
124
+
125
+ <div class="flex flex-wrap gap-1">
126
+ {#each skeletonChips as chip (chip)}
127
+ <div class="h-5 rounded border border-line-1 bg-bg-1 px-1.5 motion-safe:animate-pulse" style={`width: ${chip === 0 ? 42 : 28 + chip * 8}px`}></div>
128
+ {/each}
129
+ </div>
130
+ </div>
131
+
132
+ <ul class="py-1" role="presentation">
133
+ {#each skeletonRows as row (row)}
134
+ <li class="flex h-7 items-center gap-2 px-3">
135
+ <div class="h-4 w-4 rounded bg-bg-2 motion-safe:animate-pulse"></div>
136
+ <div class="h-3 rounded bg-bg-2 motion-safe:animate-pulse" style={`width: ${row % 3 === 0 ? 132 : row % 3 === 1 ? 96 : 164}px`}></div>
137
+ </li>
138
+ {/each}
139
+ </ul>
140
+ </div>
117
141
  {:else if items.length === 0}
118
142
  <div class="p-3 flex flex-col items-start gap-2 text-[12px] text-fg-3 font-mono leading-relaxed">
119
143
  <span class="text-fg-2">No user collections.</span>
@@ -4,22 +4,33 @@
4
4
  import EmptyState from '$lib/EmptyState.svelte'
5
5
  import { activity } from '$lib/activity.svelte'
6
6
  import { connection } from '$lib/connections.svelte'
7
+ import type { PermissionDecision } from '$lib/permission-gate'
8
+ import {
9
+ POLICY_READ_CHECK,
10
+ assembleCapabilityMatrix,
11
+ capabilityMatrixChecksFromPolicies,
12
+ type CapabilityMatrixRow,
13
+ } from '$lib/policies'
7
14
  import { secureStore } from '$lib/secureStore.svelte'
8
15
  import type { AuthPolicy, AuthTenant, AuthUser, Whoami } from '#reddb'
9
- import { Building2, Lock, RefreshCw, ScrollText, Shield, ShieldAlert, Users } from 'lucide-svelte'
16
+ import { Building2, CheckCircle2, Lock, RefreshCw, ScrollText, ShieldAlert, Users, XCircle } from 'lucide-svelte'
10
17
 
11
18
  let whoami = $state<Whoami | null>(null)
12
19
  let tenants = $state<AuthTenant[]>([])
13
20
  let users = $state<AuthUser[]>([])
14
21
  let policies = $state<AuthPolicy[]>([])
22
+ let policyReadDecision = $state<PermissionDecision | null>(null)
23
+ let capabilityRows = $state<CapabilityMatrixRow[]>([])
15
24
  let loading = $state(false)
16
25
  let whoamiError = $state<string | null>(null)
17
26
  let tenantsError = $state<string | null>(null)
18
27
  let usersError = $state<string | null>(null)
19
28
  let policiesError = $state<string | null>(null)
29
+ let capabilityError = $state<string | null>(null)
20
30
 
21
31
  const activeUrl = $derived(connection.active.url)
22
32
  const connected = $derived(connection.connected)
33
+ const showPolicyPane = $derived(policyReadDecision?.allowed === true)
23
34
 
24
35
  function fmtTime(v: number | undefined): string {
25
36
  if (!v) return '-'
@@ -34,6 +45,8 @@
34
45
  tenants = []
35
46
  users = []
36
47
  policies = []
48
+ policyReadDecision = null
49
+ capabilityRows = []
37
50
  return
38
51
  }
39
52
  loading = true
@@ -41,20 +54,61 @@
41
54
  tenantsError = null
42
55
  usersError = null
43
56
  policiesError = null
57
+ capabilityError = null
58
+ policyReadDecision = null
59
+ capabilityRows = []
44
60
  try {
45
61
  whoami = await activity.track('security · whoami', () => client.whoami())
46
62
  } catch (e) {
47
63
  whoamiError = (e as Error).message
48
64
  whoami = null
49
65
  }
66
+ try {
67
+ tenants = await activity.track('security · tenants', () => client.tenants())
68
+ } catch (e) {
69
+ tenantsError = (e as Error).message
70
+ tenants = []
71
+ }
50
72
  try {
51
73
  users = await activity.track('security · users', () => client.users())
52
74
  } catch (e) {
53
75
  usersError = (e as Error).message
54
76
  users = []
55
- } finally {
56
- loading = false
57
77
  }
78
+ try {
79
+ const [decision] = await activity.track('security · auth.can policy read', () => client.authCan([POLICY_READ_CHECK]))
80
+ policyReadDecision = decision ?? {
81
+ ...POLICY_READ_CHECK,
82
+ allowed: false,
83
+ reason: 'auth.can omitted policy-read decision',
84
+ }
85
+ } catch (e) {
86
+ policyReadDecision = {
87
+ ...POLICY_READ_CHECK,
88
+ allowed: false,
89
+ reason: (e as Error).message || 'auth.can failed',
90
+ }
91
+ }
92
+ if (policyReadDecision.allowed) {
93
+ try {
94
+ policies = await activity.track('security · policies', () => client.policies())
95
+ } catch (e) {
96
+ policiesError = (e as Error).message
97
+ policies = []
98
+ }
99
+ try {
100
+ const checks = capabilityMatrixChecksFromPolicies(policies)
101
+ const decisions = await activity.track('security · auth.can capability matrix', () => client.authCan(checks))
102
+ capabilityRows = assembleCapabilityMatrix(policies, decisions)
103
+ } catch (e) {
104
+ capabilityError = (e as Error).message
105
+ capabilityRows = []
106
+ }
107
+ } else {
108
+ policies = []
109
+ capabilityRows = []
110
+ }
111
+ loading = false
58
112
  }
59
113
 
60
114
  $effect(() => {
@@ -142,38 +196,40 @@
142
196
  {/if}
143
197
  </Card>
144
198
 
145
- <Card title="policies">
146
- {#if policiesError}
147
- <div class="mb-2 inline-flex items-center gap-1.5 rounded border border-warn/40 bg-bg-0 px-2 py-1 text-[11px] font-mono text-warn">
148
- <ShieldAlert class="size-3.5" />
149
- policies endpoint denied or unavailable
150
- </div>
151
- <pre class="whitespace-pre-wrap break-words text-[12px] font-mono text-fg-3">{policiesError}</pre>
152
- {:else if policies.length === 0}
153
- <div class="flex items-center gap-2 text-[12px] font-mono text-fg-3">
154
- <ScrollText class="size-4" />
155
- <span>No policies returned for this principal.</span>
156
- </div>
157
- {:else}
158
- <div class="grid gap-1 text-[12px] font-mono">
159
- {#each policies.slice(0, 6) as policy, i (`${policy.id ?? policy.name ?? i}`)}
160
- <div class="rounded border border-line-1 bg-bg-0 px-2 py-1.5">
161
- <div class="flex items-center gap-2">
162
- <span class="text-fg-0">{policy.name ?? policy.id ?? 'policy'}</span>
163
- {#if policy.effect}<Badge tone={policy.effect === 'deny' ? 'danger' : 'ok'}>{policy.effect}</Badge>{/if}
164
- </div>
165
- <div class="mt-1 text-fg-3">
166
- {policy.action ?? '*'} · {policy.resource ?? '*'}
167
- {#if policy.tenant} · tenant {policy.tenant}{/if}
199
+ {#if showPolicyPane}
200
+ <Card title="policies">
201
+ {#if policiesError}
202
+ <div class="mb-2 inline-flex items-center gap-1.5 rounded border border-warn/40 bg-bg-0 px-2 py-1 text-[11px] font-mono text-warn">
203
+ <ShieldAlert class="size-3.5" />
204
+ policies endpoint denied or unavailable
205
+ </div>
206
+ <pre class="whitespace-pre-wrap break-words text-[12px] font-mono text-fg-3">{policiesError}</pre>
207
+ {:else if policies.length === 0}
208
+ <div class="flex items-center gap-2 text-[12px] font-mono text-fg-3">
209
+ <ScrollText class="size-4" />
210
+ <span>No policies returned for this principal.</span>
211
+ </div>
212
+ {:else}
213
+ <div class="grid gap-1 text-[12px] font-mono">
214
+ {#each policies.slice(0, 6) as policy, i (`${policy.id ?? policy.name ?? i}`)}
215
+ <div class="rounded border border-line-1 bg-bg-0 px-2 py-1.5">
216
+ <div class="flex items-center gap-2">
217
+ <span class="text-fg-0">{policy.name ?? policy.id ?? 'policy'}</span>
218
+ {#if policy.effect}<Badge tone={policy.effect === 'deny' ? 'danger' : 'ok'}>{policy.effect}</Badge>{/if}
219
+ </div>
220
+ <div class="mt-1 text-fg-3">
221
+ {policy.action ?? '*'} · {policy.resource ?? '*'}
222
+ {#if policy.tenant} · tenant {policy.tenant}{/if}
223
+ </div>
168
224
  </div>
169
- </div>
170
- {/each}
171
- {#if policies.length > 6}
172
- <div class="text-[11px] text-fg-3">{policies.length - 6} more policies in the table.</div>
173
- {/if}
174
- </div>
175
- {/if}
176
- </Card>
225
+ {/each}
226
+ {#if policies.length > 6}
227
+ <div class="text-[11px] text-fg-3">{policies.length - 6} more policies in the table.</div>
228
+ {/if}
229
+ </div>
230
+ {/if}
231
+ </Card>
232
+ {/if}
177
233
  </div>
178
234
 
179
235
  <div class="grid gap-3 content-start">
@@ -217,38 +273,93 @@
217
273
  {/if}
218
274
  </Card>
219
275
 
220
- <Card title="policy table">
221
- {#if policiesError}
222
- <div class="text-[12px] font-mono text-fg-3">Policy table unavailable for this principal.</div>
223
- {:else if policies.length === 0}
224
- <div class="text-[12px] font-mono text-fg-3">No policies to tabulate.</div>
225
- {:else}
226
- <div class="overflow-auto">
227
- <table class="w-full border-collapse text-[12px] font-mono">
228
- <thead class="sticky top-0 bg-bg-1">
229
- <tr class="border-b border-line-1 text-fg-3">
230
- <th class="px-2 py-1.5 text-left font-normal">effect</th>
231
- <th class="px-2 py-1.5 text-left font-normal">principal</th>
232
- <th class="px-2 py-1.5 text-left font-normal">action</th>
233
- <th class="px-2 py-1.5 text-left font-normal">resource</th>
234
- <th class="px-2 py-1.5 text-left font-normal">tenant</th>
235
- </tr>
236
- </thead>
237
- <tbody>
238
- {#each policies as policy, i (`${policy.id ?? policy.name ?? i}`)}
239
- <tr class="border-b border-line-1/60 hover:bg-bg-1/40">
240
- <td class="px-2 py-1">{#if policy.effect}<Badge tone={policy.effect === 'deny' ? 'danger' : 'ok'}>{policy.effect}</Badge>{:else}<span class="text-fg-3">-</span>{/if}</td>
241
- <td class="px-2 py-1 text-fg-1">{policy.principal ?? '-'}</td>
242
- <td class="px-2 py-1 text-accent">{policy.action ?? '*'}</td>
243
- <td class="px-2 py-1 text-fg-1">{policy.resource ?? '*'}</td>
244
- <td class="px-2 py-1 text-fg-2">{policy.tenant ?? '-'}</td>
276
+ {#if showPolicyPane}
277
+ <Card title="policy table">
278
+ {#if policiesError}
279
+ <div class="text-[12px] font-mono text-fg-3">Policy table unavailable for this principal.</div>
280
+ {:else if policies.length === 0}
281
+ <div class="text-[12px] font-mono text-fg-3">No policies to tabulate.</div>
282
+ {:else}
283
+ <div class="overflow-auto">
284
+ <table class="w-full border-collapse text-[12px] font-mono">
285
+ <thead class="sticky top-0 bg-bg-1">
286
+ <tr class="border-b border-line-1 text-fg-3">
287
+ <th class="px-2 py-1.5 text-left font-normal">effect</th>
288
+ <th class="px-2 py-1.5 text-left font-normal">principal</th>
289
+ <th class="px-2 py-1.5 text-left font-normal">action</th>
290
+ <th class="px-2 py-1.5 text-left font-normal">resource</th>
291
+ <th class="px-2 py-1.5 text-left font-normal">tenant</th>
245
292
  </tr>
246
- {/each}
247
- </tbody>
248
- </table>
249
- </div>
250
- {/if}
251
- </Card>
293
+ </thead>
294
+ <tbody>
295
+ {#each policies as policy, i (`${policy.id ?? policy.name ?? i}`)}
296
+ <tr class="border-b border-line-1/60 hover:bg-bg-1/40">
297
+ <td class="px-2 py-1">{#if policy.effect}<Badge tone={policy.effect === 'deny' ? 'danger' : 'ok'}>{policy.effect}</Badge>{:else}<span class="text-fg-3">-</span>{/if}</td>
298
+ <td class="px-2 py-1 text-fg-1">{policy.principal ?? '-'}</td>
299
+ <td class="px-2 py-1 text-accent">{policy.action ?? '*'}</td>
300
+ <td class="px-2 py-1 text-fg-1">{policy.resource ?? '*'}</td>
301
+ <td class="px-2 py-1 text-fg-2">{policy.tenant ?? '-'}</td>
302
+ </tr>
303
+ {/each}
304
+ </tbody>
305
+ </table>
306
+ </div>
307
+ {/if}
308
+ </Card>
309
+
310
+ <Card title="my capabilities">
311
+ {#if capabilityError}
312
+ <div class="mb-2 inline-flex items-center gap-1.5 rounded border border-warn/40 bg-bg-0 px-2 py-1 text-[11px] font-mono text-warn">
313
+ <ShieldAlert class="size-3.5" />
314
+ capability matrix unavailable
315
+ </div>
316
+ <pre class="whitespace-pre-wrap break-words text-[12px] font-mono text-fg-3">{capabilityError}</pre>
317
+ {:else if capabilityRows.length === 0}
318
+ <div class="text-[12px] font-mono text-fg-3">No capability checks returned.</div>
319
+ {:else}
320
+ <div class="overflow-auto">
321
+ <table class="w-full border-collapse text-[12px] font-mono">
322
+ <thead class="sticky top-0 bg-bg-1">
323
+ <tr class="border-b border-line-1 text-fg-3">
324
+ <th class="px-2 py-1.5 text-left font-normal">can</th>
325
+ <th class="px-2 py-1.5 text-left font-normal">action</th>
326
+ <th class="px-2 py-1.5 text-left font-normal">resource</th>
327
+ <th class="px-2 py-1.5 text-left font-normal">policy effect</th>
328
+ <th class="px-2 py-1.5 text-left font-normal">reason</th>
329
+ </tr>
330
+ </thead>
331
+ <tbody>
332
+ {#each capabilityRows as row (row.id)}
333
+ <tr class="border-b border-line-1/60 hover:bg-bg-1/40">
334
+ <td class="px-2 py-1">
335
+ {#if row.allowed}
336
+ <span class="inline-flex items-center gap-1 text-ok"><CheckCircle2 class="size-3.5" /> yes</span>
337
+ {:else}
338
+ <span class="inline-flex items-center gap-1 text-danger"><XCircle class="size-3.5" /> no</span>
339
+ {/if}
340
+ </td>
341
+ <td class="px-2 py-1 text-accent">{row.action}</td>
342
+ <td class="px-2 py-1 text-fg-1">{row.resourceLabel}</td>
343
+ <td class="px-2 py-1">
344
+ {#if row.effects.length === 0}
345
+ <span class="text-fg-3">-</span>
346
+ {:else}
347
+ <span class="inline-flex flex-wrap gap-1">
348
+ {#each row.effects as effect}
349
+ <Badge tone={effect === 'deny' ? 'danger' : 'ok'}>{effect}</Badge>
350
+ {/each}
351
+ </span>
352
+ {/if}
353
+ </td>
354
+ <td class="max-w-[320px] px-2 py-1 text-fg-3">{row.reason ?? '-'}</td>
355
+ </tr>
356
+ {/each}
357
+ </tbody>
358
+ </table>
359
+ </div>
360
+ {/if}
361
+ </Card>
362
+ {/if}
252
363
  </div>
253
364
  </div>
254
365
  {/if}