@reddb-io/ui 0.1.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.
Files changed (169) hide show
  1. package/LICENSE +17 -0
  2. package/bin/red-ui.js +259 -0
  3. package/build/CNAME +1 -0
  4. package/build/_app/env.js +1 -0
  5. package/build/_app/immutable/assets/0.C63jY5jX.css +1 -0
  6. package/build/_app/immutable/assets/10.Dblyk0Kr.css +1 -0
  7. package/build/_app/immutable/assets/EmptyState.RXcH7ryE.css +1 -0
  8. package/build/_app/immutable/assets/Workspace.BCzG1DsH.css +1 -0
  9. package/build/_app/immutable/assets/inter-cyrillic-ext-wght-normal.BOeWTOD4.woff2 +0 -0
  10. package/build/_app/immutable/assets/inter-cyrillic-wght-normal.DqGufNeO.woff2 +0 -0
  11. package/build/_app/immutable/assets/inter-greek-ext-wght-normal.DlzME5K_.woff2 +0 -0
  12. package/build/_app/immutable/assets/inter-greek-wght-normal.CkhJZR-_.woff2 +0 -0
  13. package/build/_app/immutable/assets/inter-latin-ext-wght-normal.DO1Apj_S.woff2 +0 -0
  14. package/build/_app/immutable/assets/inter-latin-wght-normal.Dx4kXJAl.woff2 +0 -0
  15. package/build/_app/immutable/assets/inter-vietnamese-wght-normal.CBcvBZtf.woff2 +0 -0
  16. package/build/_app/immutable/assets/jetbrains-mono-cyrillic-wght-normal.D73BlboJ.woff2 +0 -0
  17. package/build/_app/immutable/assets/jetbrains-mono-greek-wght-normal.Bw9x6K1M.woff2 +0 -0
  18. package/build/_app/immutable/assets/jetbrains-mono-latin-ext-wght-normal.DBQx-q_a.woff2 +0 -0
  19. package/build/_app/immutable/assets/jetbrains-mono-latin-wght-normal.B9CIFXIH.woff2 +0 -0
  20. package/build/_app/immutable/assets/jetbrains-mono-vietnamese-wght-normal.Bt-aOZkq.woff2 +0 -0
  21. package/build/_app/immutable/chunks/3j4cZ58b.js +1 -0
  22. package/build/_app/immutable/chunks/69_IOA4Y.js +1 -0
  23. package/build/_app/immutable/chunks/ALqaDrNQ.js +2 -0
  24. package/build/_app/immutable/chunks/BGcIe2ep.js +2 -0
  25. package/build/_app/immutable/chunks/BMHKbvs8.js +1 -0
  26. package/build/_app/immutable/chunks/BQHbwf54.js +916 -0
  27. package/build/_app/immutable/chunks/Bgso8cDH.js +311 -0
  28. package/build/_app/immutable/chunks/Bmgx1mfv.js +310 -0
  29. package/build/_app/immutable/chunks/BrvvOVaJ.js +1 -0
  30. package/build/_app/immutable/chunks/Bzak7iHL.js +1 -0
  31. package/build/_app/immutable/chunks/D9CTkMjK.js +1 -0
  32. package/build/_app/immutable/chunks/DIeogL5L.js +1 -0
  33. package/build/_app/immutable/chunks/DXY_Rxa7.js +113 -0
  34. package/build/_app/immutable/chunks/DhEqZVGG.js +1 -0
  35. package/build/_app/immutable/chunks/Ov8N-vxt.js +1 -0
  36. package/build/_app/immutable/chunks/oiO4RLqp.js +1 -0
  37. package/build/_app/immutable/entry/app.D_IVtpdv.js +2 -0
  38. package/build/_app/immutable/entry/start.3oC0UQ_Q.js +1 -0
  39. package/build/_app/immutable/nodes/0.CHPiFxDo.js +1 -0
  40. package/build/_app/immutable/nodes/1.BC_4YZ0h.js +1 -0
  41. package/build/_app/immutable/nodes/10.B7DMKSNc.js +60 -0
  42. package/build/_app/immutable/nodes/2.CAG5_cGP.js +1 -0
  43. package/build/_app/immutable/nodes/3.CAG5_cGP.js +1 -0
  44. package/build/_app/immutable/nodes/4.CAG5_cGP.js +1 -0
  45. package/build/_app/immutable/nodes/5.CAG5_cGP.js +1 -0
  46. package/build/_app/immutable/nodes/6.CAG5_cGP.js +1 -0
  47. package/build/_app/immutable/nodes/7.BZX9Iird.js +134 -0
  48. package/build/_app/immutable/nodes/8.CAG5_cGP.js +1 -0
  49. package/build/_app/immutable/nodes/9.CAG5_cGP.js +1 -0
  50. package/build/_app/version.json +1 -0
  51. package/build/favicon.png +0 -0
  52. package/build/index.html +59 -0
  53. package/dist/embed/RedUiEmbedRoot-BGoLxJf1.js +26562 -0
  54. package/dist/embed/RedUiEmbedRoot-BGoLxJf1.js.map +1 -0
  55. package/dist/embed/bundle-mjs-UuVRz3O4.js +3047 -0
  56. package/dist/embed/bundle-mjs-UuVRz3O4.js.map +1 -0
  57. package/dist/embed/core-QuRYvMt8.js +118 -0
  58. package/dist/embed/core-QuRYvMt8.js.map +1 -0
  59. package/dist/embed/index-B6ZQhCsu.js +577 -0
  60. package/dist/embed/index-B6ZQhCsu.js.map +1 -0
  61. package/dist/embed/index-client-ItCEU64C.js +3087 -0
  62. package/dist/embed/index-client-ItCEU64C.js.map +1 -0
  63. package/dist/embed/index-fad77a13.esm-BqPj0saH.js +1318 -0
  64. package/dist/embed/index-fad77a13.esm-BqPj0saH.js.map +1 -0
  65. package/dist/embed/index.js +11 -0
  66. package/dist/embed/index.js.map +1 -0
  67. package/dist/embed/sigma-node-border.esm-CGtlgqmo.js +432 -0
  68. package/dist/embed/sigma-node-border.esm-CGtlgqmo.js.map +1 -0
  69. package/dist/embed/sigma.esm-y0qgY-OC.js +2355 -0
  70. package/dist/embed/sigma.esm-y0qgY-OC.js.map +1 -0
  71. package/dist/embed/ui.css +1 -0
  72. package/package.json +101 -0
  73. package/src/app.css +238 -0
  74. package/src/lib/ActivityIndicator.svelte +100 -0
  75. package/src/lib/ClusterNode.svelte +119 -0
  76. package/src/lib/ClusterView.svelte +541 -0
  77. package/src/lib/CollectionHistory.svelte +266 -0
  78. package/src/lib/CollectionsView.svelte +14 -0
  79. package/src/lib/CommandPalette.svelte +237 -0
  80. package/src/lib/ConnectDropdown.svelte +222 -0
  81. package/src/lib/EmptyState.svelte +27 -0
  82. package/src/lib/LiveChanges.svelte +195 -0
  83. package/src/lib/MasterPasswordDialog.svelte +168 -0
  84. package/src/lib/PageHeader.svelte +32 -0
  85. package/src/lib/PendingChangesPanel.svelte +153 -0
  86. package/src/lib/QueryEditor.svelte +122 -0
  87. package/src/lib/ResultsPane.svelte +747 -0
  88. package/src/lib/SchemaTree.svelte +193 -0
  89. package/src/lib/SecurityView.svelte +255 -0
  90. package/src/lib/ShortcutOverlay.svelte +104 -0
  91. package/src/lib/Splash.svelte +46 -0
  92. package/src/lib/StatusBar.svelte +163 -0
  93. package/src/lib/SubNav.svelte +62 -0
  94. package/src/lib/Topbar.svelte +119 -0
  95. package/src/lib/Workspace.svelte +247 -0
  96. package/src/lib/activity.svelte.ts +91 -0
  97. package/src/lib/build-info.ts +41 -0
  98. package/src/lib/capability.ts +221 -0
  99. package/src/lib/cn.ts +6 -0
  100. package/src/lib/collection-catalog.ts +116 -0
  101. package/src/lib/collection-pages.ts +33 -0
  102. package/src/lib/connection-history.ts +301 -0
  103. package/src/lib/connections.svelte.ts +493 -0
  104. package/src/lib/embed/RedUiEmbedRoot.svelte +31 -0
  105. package/src/lib/embed/index.ts +234 -0
  106. package/src/lib/graph-loader/contract.ts +336 -0
  107. package/src/lib/graph-loader/drilldown.ts +334 -0
  108. package/src/lib/graph-loader/index.ts +13 -0
  109. package/src/lib/index.ts +114 -0
  110. package/src/lib/pending-changes.svelte.ts +149 -0
  111. package/src/lib/query-tabs.svelte.ts +130 -0
  112. package/src/lib/reddb/boot-params.ts +254 -0
  113. package/src/lib/reddb/cdc-stream-client.ts +234 -0
  114. package/src/lib/reddb/client.ts +698 -0
  115. package/src/lib/reddb/connection-bootstrap.ts +260 -0
  116. package/src/lib/reddb/connection-provider.contract.ts +72 -0
  117. package/src/lib/reddb/connection-provider.ts +72 -0
  118. package/src/lib/reddb/index.ts +14 -0
  119. package/src/lib/reddb/injected-client-provider.ts +76 -0
  120. package/src/lib/reddb/local-url-provider.ts +245 -0
  121. package/src/lib/reddb/secure-store.ts +41 -0
  122. package/src/lib/reddb/server-capabilities.ts +71 -0
  123. package/src/lib/reddb/tauri-encrypted-store.ts +64 -0
  124. package/src/lib/reddb/transports.ts +89 -0
  125. package/src/lib/reddb/types.ts +32 -0
  126. package/src/lib/reddb/uri.ts +54 -0
  127. package/src/lib/reddb/web-encrypted-store.ts +201 -0
  128. package/src/lib/renderers/DiffRenderer.svelte +184 -0
  129. package/src/lib/renderers/DocumentRenderer.svelte +134 -0
  130. package/src/lib/renderers/GraphRenderer.svelte +1328 -0
  131. package/src/lib/renderers/HypertableRenderer.svelte +253 -0
  132. package/src/lib/renderers/JsonRenderer.svelte +30 -0
  133. package/src/lib/renderers/KvRenderer.svelte +150 -0
  134. package/src/lib/renderers/QueueRenderer.svelte +286 -0
  135. package/src/lib/renderers/StatsRenderer.svelte +123 -0
  136. package/src/lib/renderers/TableRenderer.svelte +440 -0
  137. package/src/lib/renderers/VectorRenderer.svelte +388 -0
  138. package/src/lib/renderers/diff-render.ts +122 -0
  139. package/src/lib/renderers/diff.ts +9 -0
  140. package/src/lib/renderers/document-render.ts +64 -0
  141. package/src/lib/renderers/document.ts +9 -0
  142. package/src/lib/renderers/graph-render.ts +631 -0
  143. package/src/lib/renderers/graph-sigma.ts +395 -0
  144. package/src/lib/renderers/graph.ts +9 -0
  145. package/src/lib/renderers/hypertable-render.ts +320 -0
  146. package/src/lib/renderers/hypertable.ts +9 -0
  147. package/src/lib/renderers/index.ts +42 -0
  148. package/src/lib/renderers/json-render.ts +89 -0
  149. package/src/lib/renderers/json.ts +10 -0
  150. package/src/lib/renderers/kv-render.ts +193 -0
  151. package/src/lib/renderers/kv.ts +9 -0
  152. package/src/lib/renderers/queue-render.ts +197 -0
  153. package/src/lib/renderers/queue.ts +9 -0
  154. package/src/lib/renderers/registry.ts +68 -0
  155. package/src/lib/renderers/stats-render.ts +73 -0
  156. package/src/lib/renderers/stats.ts +9 -0
  157. package/src/lib/renderers/table-render.ts +156 -0
  158. package/src/lib/renderers/table.ts +11 -0
  159. package/src/lib/renderers/types.ts +33 -0
  160. package/src/lib/renderers/vector-render.ts +154 -0
  161. package/src/lib/renderers/vector.ts +9 -0
  162. package/src/lib/router.svelte.ts +163 -0
  163. package/src/lib/secureStore.svelte.ts +148 -0
  164. package/src/lib/shadow-dom-spike/ShadowDomSpikeCore.svelte +198 -0
  165. package/src/lib/shadow-dom-spike/mount-shadow-dom-spike.ts +94 -0
  166. package/src/lib/shortcuts.ts +22 -0
  167. package/src/lib/surface.ts +80 -0
  168. package/src/lib/tabs.svelte.ts +115 -0
  169. package/src/lib/theme.svelte.ts +63 -0
@@ -0,0 +1,193 @@
1
+ <script lang="ts">
2
+ import { connection } from './connections.svelte'
3
+ import { secureStore } from './secureStore.svelte'
4
+ import { activity } from './activity.svelte'
5
+ import {
6
+ CAPABILITY_GLYPHS,
7
+ detectCapability,
8
+ isInternalCollection,
9
+ type Capability,
10
+ } from './capability'
11
+ import { defaultSubpage } from './collection-pages'
12
+ import { useRouter } from './router.svelte'
13
+ import { Plug, Database, Lock, Search, X } from 'lucide-svelte'
14
+
15
+ const router = useRouter()
16
+
17
+ interface Item {
18
+ name: string
19
+ capability: Capability
20
+ }
21
+
22
+ // The router is the source of truth for which collection is selected.
23
+ const activeCollection = $derived(router.collection)
24
+
25
+ let items = $state<Item[]>([])
26
+ let loading = $state(false)
27
+ let error = $state<string | null>(null)
28
+ let search = $state('')
29
+ let capabilityFilter = $state<Capability | 'all'>('all')
30
+
31
+ const connected = $derived(connection.connected)
32
+ const activeUrl = $derived(connection.active.url)
33
+ const capabilityCounts = $derived.by(() => {
34
+ const counts = new Map<Capability, number>()
35
+ for (const item of items) counts.set(item.capability, (counts.get(item.capability) ?? 0) + 1)
36
+ return [...counts.entries()].sort((a, b) => a[0].localeCompare(b[0]))
37
+ })
38
+ const filteredItems = $derived.by(() => {
39
+ const q = search.trim().toLowerCase()
40
+ return items.filter((item) => {
41
+ if (capabilityFilter !== 'all' && item.capability !== capabilityFilter) return false
42
+ if (!q) return true
43
+ const g = CAPABILITY_GLYPHS[item.capability]
44
+ return item.name.toLowerCase().includes(q) || g.label.toLowerCase().includes(q)
45
+ })
46
+ })
47
+
48
+ async function refresh() {
49
+ const client = connection.client
50
+ if (!client || !connected) {
51
+ items = []
52
+ return
53
+ }
54
+ loading = true
55
+ error = null
56
+ const token = activeUrl
57
+ try {
58
+ const names = await activity.track('schema · list collections', () => client.collections())
59
+ const user = names.filter((n) => !isInternalCollection(n)).sort()
60
+ items = user.map((name) => ({ name, capability: 'table' as Capability }))
61
+ const probes = user.map((name) =>
62
+ activity.track(`schema · probe ${name}`, async () => {
63
+ const cap = await detectCapability(client, name)
64
+ if (token !== activeUrl) return
65
+ items = items.map((it) => (it.name === name ? { ...it, capability: cap } : it))
66
+ }),
67
+ )
68
+ await Promise.all(probes)
69
+ } catch (e) {
70
+ error = (e as Error).message
71
+ items = []
72
+ } finally {
73
+ loading = false
74
+ }
75
+ }
76
+
77
+ $effect(() => {
78
+ // Block all probing until the secure store is unlocked, so the
79
+ // network panel doesn't leak the schema before the user authenticates.
80
+ if (secureStore.locked) return
81
+ void connected
82
+ void activeUrl
83
+ refresh()
84
+ })
85
+
86
+ </script>
87
+
88
+ <div class="px-3 py-2 border-b border-line-1 flex items-center justify-between text-fg-3">
89
+ <div class="flex items-center gap-2">
90
+ <Database class="size-3.5" />
91
+ <span class="type-label">Schema</span>
92
+ </div>
93
+ {#if connected && items.length > 0}
94
+ <span class="font-mono text-[10px] text-fg-3">{items.length}</span>
95
+ {/if}
96
+ </div>
97
+
98
+ {#if secureStore.locked}
99
+ <div class="p-3 flex flex-col items-start gap-2 text-[12px] text-fg-3 font-mono leading-relaxed">
100
+ <Lock class="size-4 text-accent" />
101
+ <span class="text-fg-1">Locked.</span>
102
+ <span>Enter your master password to load the schema.</span>
103
+ </div>
104
+ {:else if !connected}
105
+ <div class="p-3 flex flex-col items-start gap-2 text-[12px] text-fg-3 font-mono leading-relaxed">
106
+ <Plug class="size-4 text-fg-3" />
107
+ <span>No schema to show.</span>
108
+ <span>Connect to a reddb instance to inspect collections.</span>
109
+ </div>
110
+ {:else if error}
111
+ <div class="p-3 text-[12px] text-fg-3 font-mono leading-relaxed">
112
+ <div class="text-fg-2">Couldn't load collections.</div>
113
+ <div class="mt-1 break-words">{error}</div>
114
+ </div>
115
+ {:else if loading && items.length === 0}
116
+ <div class="p-3 text-[12px] text-fg-3 font-mono">Loading…</div>
117
+ {:else if items.length === 0}
118
+ <div class="p-3 flex flex-col items-start gap-2 text-[12px] text-fg-3 font-mono leading-relaxed">
119
+ <span class="text-fg-2">No user collections.</span>
120
+ <span>Seed the instance to populate the schema.</span>
121
+ <code class="px-2 py-1 bg-bg-2 border border-line-1 rounded text-[11px]">./scripts/seed.sh {activeUrl}</code>
122
+ </div>
123
+ {:else}
124
+ <div class="p-2 border-b border-line-1 grid gap-2">
125
+ <div class="flex items-center gap-1.5 h-7 rounded border border-line-1 bg-bg-0 px-2 text-[11px] font-mono">
126
+ <Search class="size-3 text-fg-3" />
127
+ <input
128
+ type="text"
129
+ bind:value={search}
130
+ placeholder="search collections…"
131
+ class="min-w-0 flex-1 bg-transparent text-fg-1 outline-none placeholder:text-fg-3"
132
+ />
133
+ {#if search}
134
+ <button type="button" class="text-fg-3 hover:text-fg-1 cursor-pointer" aria-label="Clear collection search" onclick={() => (search = '')}>
135
+ <X class="size-3" />
136
+ </button>
137
+ {/if}
138
+ </div>
139
+
140
+ <div class="flex flex-wrap gap-1">
141
+ <button
142
+ type="button"
143
+ onclick={() => (capabilityFilter = 'all')}
144
+ class={[
145
+ 'h-5 rounded border px-1.5 font-mono text-[10px] cursor-pointer transition-colors',
146
+ capabilityFilter === 'all' ? 'border-accent/40 bg-accent/10 text-accent' : 'border-line-1 text-fg-3 hover:text-fg-1 hover:border-line-2',
147
+ ].join(' ')}
148
+ >
149
+ all {items.length}
150
+ </button>
151
+ {#each capabilityCounts as [cap, count] (cap)}
152
+ {@const g = CAPABILITY_GLYPHS[cap]}
153
+ <button
154
+ type="button"
155
+ onclick={() => (capabilityFilter = cap)}
156
+ title={g.label}
157
+ class={[
158
+ 'h-5 rounded border px-1.5 font-mono text-[10px] cursor-pointer transition-colors',
159
+ capabilityFilter === cap ? 'border-accent/40 bg-accent/10 text-accent' : 'border-line-1 text-fg-3 hover:text-fg-1 hover:border-line-2',
160
+ ].join(' ')}
161
+ >
162
+ {g.glyph} {count}
163
+ </button>
164
+ {/each}
165
+ </div>
166
+ </div>
167
+ <ul class="py-1" role="list">
168
+ {#if filteredItems.length === 0}
169
+ <li class="px-3 py-2 font-mono text-[12px] text-fg-3">No matching collections.</li>
170
+ {/if}
171
+ {#each filteredItems as item (item.name)}
172
+ {@const g = CAPABILITY_GLYPHS[item.capability]}
173
+ {@const active = item.name === activeCollection}
174
+ {@const target = { view: 'collection' as const, collection: item.name, subpage: defaultSubpage(item.capability) }}
175
+ <li>
176
+ <a
177
+ href={router.href(target)}
178
+ onclick={(e) => router.go(target, e)}
179
+ aria-current={active ? 'page' : undefined}
180
+ title={`${g.label} · ${item.name}`}
181
+ class={[
182
+ 'w-full flex items-center gap-2 px-3 py-1 font-mono text-[12px] no-underline transition-colors',
183
+ active ? 'bg-bg-1 text-fg-0' : 'text-fg-2 hover:bg-bg-1 hover:text-fg-0',
184
+ ].join(' ')}
185
+ >
186
+ <span class="inline-block w-4 text-center text-fg-3" aria-hidden="true">{g.glyph}</span>
187
+ <span class="truncate">{item.name}</span>
188
+ <span class="sr-only">{g.label}</span>
189
+ </a>
190
+ </li>
191
+ {/each}
192
+ </ul>
193
+ {/if}
@@ -0,0 +1,255 @@
1
+ <script lang="ts">
2
+ import { Card, Badge } from '@reddb-io/ui-kit'
3
+ import PageHeader from '$lib/PageHeader.svelte'
4
+ import EmptyState from '$lib/EmptyState.svelte'
5
+ import { activity } from '$lib/activity.svelte'
6
+ import { connection } from '$lib/connections.svelte'
7
+ import { secureStore } from '$lib/secureStore.svelte'
8
+ import type { AuthPolicy, AuthTenant, AuthUser, Whoami } from '#reddb'
9
+ import { Building2, Lock, RefreshCw, ScrollText, Shield, ShieldAlert, Users } from 'lucide-svelte'
10
+
11
+ let whoami = $state<Whoami | null>(null)
12
+ let tenants = $state<AuthTenant[]>([])
13
+ let users = $state<AuthUser[]>([])
14
+ let policies = $state<AuthPolicy[]>([])
15
+ let loading = $state(false)
16
+ let whoamiError = $state<string | null>(null)
17
+ let tenantsError = $state<string | null>(null)
18
+ let usersError = $state<string | null>(null)
19
+ let policiesError = $state<string | null>(null)
20
+
21
+ const activeUrl = $derived(connection.active.url)
22
+ const connected = $derived(connection.connected)
23
+
24
+ function fmtTime(v: number | undefined): string {
25
+ if (!v) return '-'
26
+ const ms = v > 10_000_000_000 ? v : v * 1000
27
+ return new Date(ms).toISOString().replace('T', ' ').slice(0, 19)
28
+ }
29
+
30
+ async function refresh() {
31
+ const client = connection.client
32
+ if (!client || secureStore.locked || !connected) {
33
+ whoami = null
34
+ tenants = []
35
+ users = []
36
+ policies = []
37
+ return
38
+ }
39
+ loading = true
40
+ whoamiError = null
41
+ tenantsError = null
42
+ usersError = null
43
+ policiesError = null
44
+ try {
45
+ whoami = await activity.track('security · whoami', () => client.whoami())
46
+ } catch (e) {
47
+ whoamiError = (e as Error).message
48
+ whoami = null
49
+ }
50
+ try {
51
+ users = await activity.track('security · users', () => client.users())
52
+ } catch (e) {
53
+ usersError = (e as Error).message
54
+ users = []
55
+ } finally {
56
+ loading = false
57
+ }
58
+ }
59
+
60
+ $effect(() => {
61
+ if (secureStore.locked) return
62
+ void activeUrl
63
+ void connected
64
+ refresh()
65
+ })
66
+ </script>
67
+
68
+ <div class="h-full overflow-auto bg-bg-0 p-6 text-fg-1">
69
+ <PageHeader
70
+ eyebrow="Admin"
71
+ title="Security"
72
+ subtitle="Tenants, users, and policy surfaces are permission-aware. Controls only appear when reddb exposes the grant."
73
+ >
74
+ {#snippet actions()}
75
+ <button
76
+ type="button"
77
+ onclick={refresh}
78
+ disabled={loading || secureStore.locked || !connected}
79
+ class="inline-flex h-7 items-center gap-1.5 rounded-md border border-line-2 bg-bg-2 px-2.5 text-[11px] font-mono text-fg-1 hover:border-line-3 hover:text-fg-0 disabled:cursor-not-allowed disabled:opacity-40"
80
+ >
81
+ <RefreshCw class={loading ? 'size-3.5 animate-spin' : 'size-3.5'} />
82
+ refresh
83
+ </button>
84
+ {/snippet}
85
+ </PageHeader>
86
+
87
+ {#if secureStore.locked}
88
+ <EmptyState icon={Lock} title="Security locked" message="Enter the master password before red-ui loads auth data." />
89
+ {:else if !connected}
90
+ <EmptyState icon={ShieldAlert} title="No active connection" message="Connect to a reddb instance to inspect security posture." />
91
+ {:else}
92
+ <div class="grid grid-cols-[minmax(280px,0.75fr)_1.25fr] gap-3">
93
+ <div class="grid gap-3 content-start">
94
+ <Card title="session">
95
+ {#if whoamiError}
96
+ <div class="text-[12px] font-mono text-warn break-words">{whoamiError}</div>
97
+ {:else if whoami}
98
+ <div class="grid grid-cols-[110px_1fr] gap-x-3 gap-y-2 text-[12px] font-mono">
99
+ <span class="text-fg-3">authenticated</span>
100
+ <span>{#if whoami.authenticated}<Badge tone="ok">yes</Badge>{:else}<Badge tone="warn">no</Badge>{/if}</span>
101
+ <span class="text-fg-3">username</span>
102
+ <span class="text-fg-0">{whoami.username}</span>
103
+ <span class="text-fg-3">role</span>
104
+ <span class="text-accent">{whoami.role}</span>
105
+ {#if whoami.note}
106
+ <span class="text-fg-3">note</span>
107
+ <span class="text-fg-1">{whoami.note}</span>
108
+ {/if}
109
+ </div>
110
+ {:else}
111
+ <div class="text-[12px] font-mono text-fg-3">Loading session…</div>
112
+ {/if}
113
+ </Card>
114
+
115
+ <Card title="tenants">
116
+ {#if tenantsError}
117
+ <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">
118
+ <ShieldAlert class="size-3.5" />
119
+ tenants endpoint denied or unavailable
120
+ </div>
121
+ <pre class="whitespace-pre-wrap break-words text-[12px] font-mono text-fg-3">{tenantsError}</pre>
122
+ {:else if tenants.length === 0}
123
+ <div class="flex items-center gap-2 text-[12px] font-mono text-fg-3">
124
+ <Building2 class="size-4" />
125
+ <span>No tenants returned for this principal.</span>
126
+ </div>
127
+ {:else}
128
+ <div class="grid gap-1 text-[12px] font-mono">
129
+ {#each tenants as tenant, i (`${tenant.id ?? tenant.slug ?? tenant.name ?? i}`)}
130
+ <div class="rounded border border-line-1 bg-bg-0 px-2 py-1.5">
131
+ <div class="flex items-center gap-2">
132
+ <span class="text-fg-0">{tenant.name ?? tenant.slug ?? tenant.id ?? 'tenant'}</span>
133
+ {#if tenant.role}<Badge tone="accent">{tenant.role}</Badge>{/if}
134
+ </div>
135
+ <div class="mt-1 text-fg-3">
136
+ {tenant.id ?? tenant.slug ?? '-'}
137
+ {#if tenant.grants?.length} · {tenant.grants.join(', ')}{/if}
138
+ </div>
139
+ </div>
140
+ {/each}
141
+ </div>
142
+ {/if}
143
+ </Card>
144
+
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}
168
+ </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>
177
+ </div>
178
+
179
+ <div class="grid gap-3 content-start">
180
+ <Card title="users">
181
+ {#if usersError}
182
+ <div class="mb-3 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">
183
+ <ShieldAlert class="size-3.5" />
184
+ users endpoint denied or unavailable
185
+ </div>
186
+ <pre class="whitespace-pre-wrap break-words text-[12px] font-mono text-fg-3">{usersError}</pre>
187
+ {:else if users.length === 0}
188
+ <div class="flex items-center gap-2 text-[12px] font-mono text-fg-3">
189
+ <Users class="size-4" />
190
+ <span>No users returned for this principal.</span>
191
+ </div>
192
+ {:else}
193
+ <div class="overflow-auto">
194
+ <table class="w-full border-collapse text-[12px] font-mono">
195
+ <thead class="sticky top-0 bg-bg-1">
196
+ <tr class="border-b border-line-1 text-fg-3">
197
+ <th class="px-2 py-1.5 text-left font-normal">user</th>
198
+ <th class="px-2 py-1.5 text-left font-normal">role</th>
199
+ <th class="px-2 py-1.5 text-left font-normal">mfa</th>
200
+ <th class="px-2 py-1.5 text-left font-normal">created</th>
201
+ <th class="px-2 py-1.5 text-left font-normal">last active</th>
202
+ </tr>
203
+ </thead>
204
+ <tbody>
205
+ {#each users as user (user.username)}
206
+ <tr class="border-b border-line-1/60 hover:bg-bg-1/40">
207
+ <td class="px-2 py-1 text-fg-0">{user.username}</td>
208
+ <td class="px-2 py-1 text-accent">{user.role ?? '-'}</td>
209
+ <td class="px-2 py-1">{#if user.mfa}<Badge tone="ok">on</Badge>{:else}<span class="text-fg-3">-</span>{/if}</td>
210
+ <td class="px-2 py-1 text-fg-2">{fmtTime(user.created_at)}</td>
211
+ <td class="px-2 py-1 text-fg-2">{fmtTime(user.last_active)}</td>
212
+ </tr>
213
+ {/each}
214
+ </tbody>
215
+ </table>
216
+ </div>
217
+ {/if}
218
+ </Card>
219
+
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>
245
+ </tr>
246
+ {/each}
247
+ </tbody>
248
+ </table>
249
+ </div>
250
+ {/if}
251
+ </Card>
252
+ </div>
253
+ </div>
254
+ {/if}
255
+ </div>
@@ -0,0 +1,104 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte'
3
+ import { Kbd } from '@reddb-io/ui-kit'
4
+ import { shortcuts, shortcutContexts, type ShortcutContext } from './shortcuts'
5
+
6
+ let open = $state(false)
7
+
8
+ function close() {
9
+ open = false
10
+ }
11
+
12
+ function isTypingTarget(el: EventTarget | null): boolean {
13
+ if (!(el instanceof HTMLElement)) return false
14
+ if (el.isContentEditable) return true
15
+ const tag = el.tagName
16
+ return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT'
17
+ }
18
+
19
+ function onKey(e: KeyboardEvent) {
20
+ if (e.key === 'Escape' && open) {
21
+ e.preventDefault()
22
+ close()
23
+ return
24
+ }
25
+ if (e.key === '?' && !e.metaKey && !e.ctrlKey && !e.altKey) {
26
+ if (isTypingTarget(e.target)) return
27
+ e.preventDefault()
28
+ open = !open
29
+ }
30
+ }
31
+
32
+ onMount(() => {
33
+ window.addEventListener('keydown', onKey)
34
+ const onOpen = () => { open = true }
35
+ window.addEventListener('red:open-shortcuts', onOpen)
36
+ return () => {
37
+ window.removeEventListener('keydown', onKey)
38
+ window.removeEventListener('red:open-shortcuts', onOpen)
39
+ }
40
+ })
41
+
42
+ const grouped = $derived(
43
+ shortcutContexts
44
+ .map((ctx) => ({ ctx, items: shortcuts.filter((s) => s.context === ctx) }))
45
+ .filter((g) => g.items.length > 0)
46
+ )
47
+
48
+ const ctxBlurb: Record<ShortcutContext, string> = {
49
+ Global: 'Available anywhere in the app',
50
+ Connection: 'Manage which reddb you talk to',
51
+ Workspace: 'Move between tabs and panes',
52
+ Editing: 'While editing data or running queries',
53
+ }
54
+ </script>
55
+
56
+ {#if open}
57
+ <div
58
+ class="fixed inset-0 z-[200] bg-bg-0/70 backdrop-blur-md flex items-center justify-center p-6"
59
+ onclick={close}
60
+ role="presentation"
61
+ >
62
+ <div
63
+ class="w-full max-w-[640px] max-h-[80vh] overflow-y-auto bg-bg-1 border border-line-2 rounded-lg shadow-2xl"
64
+ role="dialog"
65
+ aria-modal="true"
66
+ aria-label="Keyboard shortcuts"
67
+ tabindex="-1"
68
+ onclick={(e) => e.stopPropagation()}
69
+ onkeydown={null}
70
+ >
71
+ <header class="flex items-center justify-between px-5 py-3 border-b border-line-1">
72
+ <div class="flex items-center gap-2">
73
+ <span class="type-label">Keyboard shortcuts</span>
74
+ </div>
75
+ <div class="flex items-center gap-1.5 text-fg-3 text-[11px]">
76
+ <Kbd>Esc</Kbd> close
77
+ </div>
78
+ </header>
79
+
80
+ <div class="p-5 grid gap-5">
81
+ {#each grouped as group}
82
+ <section>
83
+ <div class="flex items-baseline justify-between mb-2">
84
+ <h3 class="type-label m-0">{group.ctx}</h3>
85
+ <span class="text-fg-3 text-[11px]">{ctxBlurb[group.ctx]}</span>
86
+ </div>
87
+ <ul class="grid gap-1 m-0 p-0 list-none">
88
+ {#each group.items as s}
89
+ <li class="flex items-center justify-between gap-3 px-2 py-1.5 rounded hover:bg-bg-2">
90
+ <span class="text-fg-1 text-[13px]">{s.label}</span>
91
+ <span class="inline-flex gap-1 shrink-0">
92
+ {#each s.keys as k}
93
+ <Kbd>{k}</Kbd>
94
+ {/each}
95
+ </span>
96
+ </li>
97
+ {/each}
98
+ </ul>
99
+ </section>
100
+ {/each}
101
+ </div>
102
+ </div>
103
+ </div>
104
+ {/if}
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte'
3
+
4
+ interface Props {
5
+ onDone?: () => void
6
+ }
7
+ let { onDone }: Props = $props()
8
+
9
+ let visible = $state(true)
10
+ let stage = $state(0) // 0: pre, 1: in, 2: out
11
+
12
+ onMount(() => {
13
+ const inT = setTimeout(() => (stage = 1), 20)
14
+ const outT = setTimeout(() => (stage = 2), 800)
15
+ const closeT = setTimeout(() => {
16
+ visible = false
17
+ onDone?.()
18
+ }, 1000)
19
+ return () => {
20
+ clearTimeout(inT)
21
+ clearTimeout(outT)
22
+ clearTimeout(closeT)
23
+ }
24
+ })
25
+ </script>
26
+
27
+ {#if visible}
28
+ <div
29
+ class="fixed inset-0 z-[9999] grid place-items-center bg-bg-0 transition-opacity duration-200 ease-out"
30
+ class:opacity-0={stage === 2}
31
+ class:opacity-100={stage === 1}
32
+ class:pointer-events-none={stage === 2}
33
+ style:opacity={stage === 0 ? 0 : undefined}
34
+ >
35
+ <div class="flex items-center gap-2">
36
+ <div class="w-2 h-2 rounded-full bg-accent shadow-[0_0_12px_var(--color-accent-glow)]"></div>
37
+ <span class="font-mono font-semibold text-fg-0 text-[15px] tracking-tight">red<span class="text-accent mx-px">·</span>ui</span>
38
+ </div>
39
+ </div>
40
+ {/if}
41
+
42
+ <style>
43
+ @media (prefers-reduced-motion: reduce) {
44
+ div { transition: none !important; }
45
+ }
46
+ </style>