@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,197 @@
1
+ import type { QueryResult } from '#reddb'
2
+
3
+ export interface QueueMessage {
4
+ index: number
5
+ id: string
6
+ messageColumn: string | null
7
+ message: unknown
8
+ state: string
9
+ consumer: string | null
10
+ dlc: string | null
11
+ values: Record<string, unknown>
12
+ }
13
+
14
+ export interface QueueEvent {
15
+ messageId: string
16
+ collection: string
17
+ op: string
18
+ entityId: string
19
+ lsn: number | null
20
+ ts: number | null
21
+ before: unknown
22
+ after: unknown
23
+ payload: Record<string, unknown>
24
+ }
25
+
26
+ export interface QueueCommand {
27
+ label: string
28
+ query: string
29
+ mutates: boolean
30
+ note: string
31
+ }
32
+
33
+ const MESSAGE_COLUMNS = ['message', 'payload', 'body', 'value', 'data']
34
+ const STATE_COLUMNS = ['state', 'status', 'delivery_state']
35
+ const CONSUMER_COLUMNS = ['consumer', 'consumer_id', 'worker', 'lease_owner']
36
+ const DLC_COLUMNS = ['dlc', 'dlq', 'dead_letter_collection', 'dead_letter_queue']
37
+
38
+ function pick(values: Record<string, unknown>, names: string[]): [string, unknown] | null {
39
+ for (const name of names) {
40
+ if (name in values) return [name, values[name]]
41
+ }
42
+ const lowered = Object.entries(values).find(([k]) => names.includes(k.toLowerCase()))
43
+ return lowered ?? null
44
+ }
45
+
46
+ export function extractQueueMessages(result: QueryResult): QueueMessage[] {
47
+ return result.result.records.map((record, index) => {
48
+ const values = record.values ?? {}
49
+ const message = pick(values, MESSAGE_COLUMNS)
50
+ const state = pick(values, STATE_COLUMNS)
51
+ const consumer = pick(values, CONSUMER_COLUMNS)
52
+ const dlc = pick(values, DLC_COLUMNS)
53
+ return {
54
+ index,
55
+ id: String(values.message_id ?? values.id ?? values.rid ?? index + 1),
56
+ messageColumn: message?.[0] ?? null,
57
+ message: message?.[1] ?? values,
58
+ state: String(state?.[1] ?? 'ready'),
59
+ consumer: consumer?.[1] === undefined || consumer?.[1] === null ? null : String(consumer[1]),
60
+ dlc: dlc?.[1] === undefined || dlc?.[1] === null ? null : String(dlc[1]),
61
+ values,
62
+ }
63
+ })
64
+ }
65
+
66
+ export function hasQueueShape(result: QueryResult): boolean {
67
+ if (result.capability === 'queue') return true
68
+ return result.result.records.some((record) => {
69
+ const keys = Object.keys(record.values ?? {}).map((k) => k.toLowerCase())
70
+ if (keys.includes('message_id') && MESSAGE_COLUMNS.some((c) => keys.includes(c))) return true
71
+ return MESSAGE_COLUMNS.some((c) => keys.includes(c)) && (
72
+ STATE_COLUMNS.some((c) => keys.includes(c)) ||
73
+ CONSUMER_COLUMNS.some((c) => keys.includes(c)) ||
74
+ DLC_COLUMNS.some((c) => keys.includes(c))
75
+ )
76
+ })
77
+ }
78
+
79
+ export function queueSummary(messages: QueueMessage[]) {
80
+ const active = new Set<string>()
81
+ const states = new Map<string, number>()
82
+ const dlcs = new Set<string>()
83
+ const operations = new Map<string, number>()
84
+ const sources = new Set<string>()
85
+ for (const msg of messages) {
86
+ states.set(msg.state, (states.get(msg.state) ?? 0) + 1)
87
+ if (msg.consumer && /active|leased|processing|running/i.test(msg.state)) active.add(msg.consumer)
88
+ if (msg.dlc) dlcs.add(msg.dlc)
89
+ if (msg.message && typeof msg.message === 'object' && !Array.isArray(msg.message)) {
90
+ const payload = msg.message as Record<string, unknown>
91
+ const op = payload.op
92
+ const source = payload.collection
93
+ if (typeof op === 'string') operations.set(op, (operations.get(op) ?? 0) + 1)
94
+ if (typeof source === 'string') sources.add(source)
95
+ }
96
+ }
97
+ return {
98
+ activeConsumers: active.size,
99
+ states: [...states.entries()],
100
+ dlcs: [...dlcs],
101
+ operations: [...operations.entries()].sort((a, b) => a[0].localeCompare(b[0])),
102
+ sources: [...sources].sort(),
103
+ }
104
+ }
105
+
106
+ function numberOrNull(v: unknown): number | null {
107
+ if (typeof v === 'number' && Number.isFinite(v)) return v
108
+ if (typeof v === 'string') {
109
+ const n = Number(v)
110
+ if (Number.isFinite(n)) return n
111
+ }
112
+ return null
113
+ }
114
+
115
+ export function extractQueueEvents(messages: QueueMessage[]): QueueEvent[] {
116
+ const events: QueueEvent[] = []
117
+ for (const msg of messages) {
118
+ if (!msg.message || typeof msg.message !== 'object' || Array.isArray(msg.message)) continue
119
+ const payload = msg.message as Record<string, unknown>
120
+ const collection = payload.collection
121
+ const op = payload.op
122
+ if (typeof collection !== 'string' || typeof op !== 'string') continue
123
+ const id = payload.id ?? (payload.after && typeof payload.after === 'object' && !Array.isArray(payload.after)
124
+ ? (payload.after as Record<string, unknown>).id
125
+ : undefined)
126
+ events.push({
127
+ messageId: msg.id,
128
+ collection,
129
+ op,
130
+ entityId: id === undefined || id === null ? String(payload.rid ?? msg.id) : String(id),
131
+ lsn: numberOrNull(payload.lsn),
132
+ ts: numberOrNull(payload.ts),
133
+ before: payload.before,
134
+ after: payload.after,
135
+ payload,
136
+ })
137
+ }
138
+ return events
139
+ }
140
+
141
+ export function queueCommandPlan(queue: string): QueueCommand[] {
142
+ const safeQueue = queue.replace(/[^A-Za-z0-9_./-]/g, '')
143
+ const consumer = 'red-ui-preview'
144
+ const group = 'showcase'
145
+ return [
146
+ {
147
+ label: 'Backlog depth',
148
+ query: `QUEUE LEN ${safeQueue}`,
149
+ mutates: false,
150
+ note: 'Read-only count of messages currently stored in the queue.',
151
+ },
152
+ {
153
+ label: 'Peek messages',
154
+ query: `QUEUE PEEK ${safeQueue} 25`,
155
+ mutates: false,
156
+ note: 'Read-only inspection. Does not lease, ack, or remove messages.',
157
+ },
158
+ {
159
+ label: 'Read as fanout consumer',
160
+ query: `QUEUE READ ${safeQueue} CONSUMER ${consumer} COUNT 5`,
161
+ mutates: true,
162
+ note: 'Leases messages for this consumer. ACK/NACK decides what happens next.',
163
+ },
164
+ {
165
+ label: 'Read through explicit group',
166
+ query: `QUEUE READ ${safeQueue} GROUP ${group} CONSUMER worker-a COUNT 5`,
167
+ mutates: true,
168
+ note: 'Creates/uses consumer group delivery state for an application tier.',
169
+ },
170
+ {
171
+ label: 'Inspect pending group messages',
172
+ query: `QUEUE PENDING ${safeQueue} GROUP ${group}`,
173
+ mutates: false,
174
+ note: 'Shows leased-but-unacked messages for a group.',
175
+ },
176
+ {
177
+ label: 'Ack a delivered message',
178
+ query: `QUEUE ACK ${safeQueue} GROUP ${group} 'message-id'`,
179
+ mutates: true,
180
+ note: 'Removes a successfully processed message for that group.',
181
+ },
182
+ {
183
+ label: 'Nack a delivered message',
184
+ query: `QUEUE NACK ${safeQueue} GROUP ${group} 'message-id'`,
185
+ mutates: true,
186
+ note: 'Returns or retries a failed message according to queue policy.',
187
+ },
188
+ ]
189
+ }
190
+
191
+ export function queuePushQuery(queue: string, payload: unknown): string {
192
+ const safeQueue = queue.replace(/[^A-Za-z0-9_./-]/g, '')
193
+ const rendered = typeof payload === 'string'
194
+ ? `'${payload.replace(/'/g, "''")}'`
195
+ : JSON.stringify(payload ?? null)
196
+ return `QUEUE PUSH ${safeQueue} ${rendered}`
197
+ }
@@ -0,0 +1,9 @@
1
+ import type { CapabilityRenderer } from './types'
2
+ import { hasQueueShape } from './queue-render'
3
+ import QueueRenderer from './QueueRenderer.svelte'
4
+
5
+ export const queueRenderer: CapabilityRenderer = {
6
+ capability: 'queue',
7
+ renders: (result) => hasQueueShape(result),
8
+ component: QueueRenderer,
9
+ }
@@ -0,0 +1,68 @@
1
+ // CapabilityRenderer registry — deep module behind a tiny surface.
2
+ //
3
+ // Each renderer declares which capability it handles. pick() takes a
4
+ // capability hint (from the query result, slice metadata, or an override)
5
+ // and picks the right renderer; if no renderer matches the hint, it falls
6
+ // back to the renderer that `renders(result)` accepts, and finally to the
7
+ // 'table' renderer which is the universal default.
8
+
9
+ import type { QueryResult } from '#reddb'
10
+ import type { Capability, CapabilityRenderer } from './types'
11
+
12
+ export class RendererRegistry {
13
+ private renderers: CapabilityRenderer[] = []
14
+
15
+ register(renderer: CapabilityRenderer): void {
16
+ const idx = this.renderers.findIndex((r) => r.capability === renderer.capability)
17
+ if (idx >= 0) this.renderers[idx] = renderer
18
+ else this.renderers.push(renderer)
19
+ }
20
+
21
+ unregister(capability: Capability): void {
22
+ this.renderers = this.renderers.filter((r) => r.capability !== capability)
23
+ }
24
+
25
+ list(): readonly CapabilityRenderer[] {
26
+ return this.renderers
27
+ }
28
+
29
+ has(capability: Capability): boolean {
30
+ return this.renderers.some((r) => r.capability === capability)
31
+ }
32
+
33
+ /**
34
+ * Pick a renderer for this result.
35
+ *
36
+ * Resolution order:
37
+ * 1. `override` capability if present and registered.
38
+ * 2. `capability` hint if present, registered, and `renders(result)` agrees.
39
+ * 3. First renderer whose `renders(result)` returns true.
40
+ * 4. The 'table' renderer (universal fallback).
41
+ *
42
+ * Throws if no renderer matches and no 'table' renderer is registered.
43
+ */
44
+ pick(
45
+ capability: Capability | undefined,
46
+ result: QueryResult,
47
+ override?: Capability,
48
+ ): CapabilityRenderer {
49
+ if (override) {
50
+ const r = this.renderers.find((r) => r.capability === override)
51
+ if (r) return r
52
+ }
53
+ if (capability) {
54
+ const r = this.renderers.find((r) => r.capability === capability)
55
+ if (r && r.renders(result)) return r
56
+ }
57
+ const shapeMatch = this.renderers.find((r) => r.renders(result))
58
+ if (shapeMatch) return shapeMatch
59
+ const fallback = this.renderers.find((r) => r.capability === 'table')
60
+ if (!fallback) {
61
+ throw new Error('No renderer matched and no table fallback is registered')
62
+ }
63
+ return fallback
64
+ }
65
+ }
66
+
67
+ /** Shared singleton — renderers self-register into this from ./index.ts. */
68
+ export const registry = new RendererRegistry()
@@ -0,0 +1,73 @@
1
+ import type { QueryResult } from '#reddb'
2
+
3
+ export interface StatMetric {
4
+ key: string
5
+ label: string
6
+ value: number
7
+ unit: string
8
+ source: Record<string, unknown>
9
+ }
10
+
11
+ const NAME_COLUMNS = ['metric', 'stat', 'name', 'key']
12
+ const VALUE_COLUMNS = ['value', 'count', 'total', 'avg', 'rate', 'p50', 'p95', 'p99']
13
+ const UNIT_COLUMNS = ['unit', 'units']
14
+
15
+ function unitFor(key: string): string {
16
+ if (/bytes?|memory|storage|wal/i.test(key)) return 'bytes'
17
+ if (/rps|rate|requests?_per_second|writes?_per_second|reads?_per_second/i.test(key)) return '/s'
18
+ if (/ms|latency|response/i.test(key)) return 'ms'
19
+ if (/percent|ratio|usage/i.test(key)) return '%'
20
+ return ''
21
+ }
22
+
23
+ function explicitUnit(values: Record<string, unknown>): string | null {
24
+ for (const column of UNIT_COLUMNS) {
25
+ const value = values[column]
26
+ if (typeof value === 'string' && value.trim()) return value.trim()
27
+ }
28
+ const entry = Object.entries(values).find(([k, v]) =>
29
+ UNIT_COLUMNS.includes(k.toLowerCase()) && typeof v === 'string' && v.trim(),
30
+ )
31
+ return entry ? String(entry[1]).trim() : null
32
+ }
33
+
34
+ export function extractStats(result: QueryResult): StatMetric[] {
35
+ const metrics: StatMetric[] = []
36
+ for (const record of result.result.records) {
37
+ const values = record.values ?? {}
38
+ const nameEntry = Object.entries(values).find(([k, v]) =>
39
+ NAME_COLUMNS.includes(k.toLowerCase()) && (typeof v === 'string' || typeof v === 'number'),
40
+ )
41
+ const valueEntry = Object.entries(values).find(([k, v]) =>
42
+ VALUE_COLUMNS.includes(k.toLowerCase()) && typeof v === 'number' && Number.isFinite(v),
43
+ )
44
+ if (nameEntry && valueEntry) {
45
+ const label = String(nameEntry[1])
46
+ metrics.push({ key: label, label, value: valueEntry[1] as number, unit: explicitUnit(values) ?? unitFor(label), source: values })
47
+ continue
48
+ }
49
+ for (const [key, value] of Object.entries(values)) {
50
+ if (typeof value !== 'number' || !Number.isFinite(value)) continue
51
+ metrics.push({ key, label: key, value, unit: unitFor(key), source: values })
52
+ }
53
+ }
54
+ return metrics
55
+ }
56
+
57
+ export function hasStatsShape(result: QueryResult): boolean {
58
+ if (result.capability === 'stats') return true
59
+ const metrics = extractStats(result)
60
+ return metrics.length > 0 && metrics.length >= result.result.records.length
61
+ }
62
+
63
+ export function formatMetric(value: number, unit: string): string {
64
+ if (unit === 'bytes') {
65
+ const abs = Math.abs(value)
66
+ if (abs >= 1024 ** 3) return `${(value / 1024 ** 3).toFixed(1)} GiB`
67
+ if (abs >= 1024 ** 2) return `${(value / 1024 ** 2).toFixed(1)} MiB`
68
+ if (abs >= 1024) return `${(value / 1024).toFixed(1)} KiB`
69
+ return `${value} B`
70
+ }
71
+ if (unit === '%' || unit === 'ms' || unit === '/s') return `${value.toLocaleString()}${unit}`
72
+ return value.toLocaleString()
73
+ }
@@ -0,0 +1,9 @@
1
+ import type { CapabilityRenderer } from './types'
2
+ import { hasStatsShape } from './stats-render'
3
+ import StatsRenderer from './StatsRenderer.svelte'
4
+
5
+ export const statsRenderer: CapabilityRenderer = {
6
+ capability: 'stats',
7
+ renders: (result) => hasStatsShape(result),
8
+ component: StatsRenderer,
9
+ }
@@ -0,0 +1,156 @@
1
+ // Pure rendering helpers for the table capability. Used by both the
2
+ // Svelte TableRenderer component (for live UI) and the golden snapshot
3
+ // test (which needs a DOM-free representation).
4
+
5
+ import type { QueryResult } from '#reddb'
6
+
7
+ export const TABLE_ROW_LIMIT = 80
8
+ export const ELLIPSIS_AT = 64
9
+
10
+ /**
11
+ * reddb's reserved public item fields. The server enforces these as
12
+ * unwritable by users (see `crates/reddb-server/src/reserved_fields.rs`)
13
+ * and stamps them onto every read. The UI hides them by default so the
14
+ * user-defined shape is visible; a toggle reintroduces them as a
15
+ * prefix block.
16
+ */
17
+ export const SYSTEM_FIELDS = [
18
+ 'rid',
19
+ 'collection',
20
+ 'kind',
21
+ 'tenant',
22
+ 'created_at',
23
+ 'updated_at',
24
+ ] as const
25
+ const SYSTEM_SET = new Set<string>(SYSTEM_FIELDS)
26
+
27
+ export function isSystemField(column: string): boolean {
28
+ return SYSTEM_SET.has(column.toLowerCase())
29
+ }
30
+
31
+ export interface VisibleColumnsOptions {
32
+ /** When true, reddb system fields lead; when false (default) they're hidden. */
33
+ showSystem?: boolean
34
+ }
35
+
36
+ export function visibleColumns(
37
+ result: QueryResult,
38
+ opts: VisibleColumnsOptions = {},
39
+ ): string[] {
40
+ const all = result.result.columns
41
+ // `red_*` and `body` are noise even in show-system mode — the row is
42
+ // already unrolled, and red_* are internal-only.
43
+ const usable = all.filter((c) => !c.startsWith('red_') && c !== 'body')
44
+
45
+ const userCols = usable.filter((c) => !isSystemField(c))
46
+ const id = userCols.find((c) => c.toLowerCase() === 'id')
47
+ const userRest = userCols.filter((c) => c !== id)
48
+ const userOrdered = [id, ...userRest].filter((c): c is string => Boolean(c))
49
+
50
+ if (!opts.showSystem) {
51
+ return userOrdered.length > 0 ? userOrdered : all
52
+ }
53
+
54
+ // System block, in canonical order (rid first — it's the actual address).
55
+ const systemOrdered = SYSTEM_FIELDS.filter((f) => usable.includes(f))
56
+ return [...systemOrdered, ...userOrdered]
57
+ }
58
+
59
+ export type IdentityKind = 'rid' | 'id'
60
+
61
+ export function identityKind(column: string): IdentityKind | null {
62
+ const c = column.toLowerCase()
63
+ if (c === 'rid') return 'rid'
64
+ if (c === 'id') return 'id'
65
+ return null
66
+ }
67
+
68
+ export function inferPkColumn(columns: string[]): string | null {
69
+ // rid wins — it's the system-assigned row id and the actual key
70
+ // reddb uses to address documents. id is user-defined and may not be unique.
71
+ return (
72
+ columns.find((c) => c.toLowerCase() === 'rid') ??
73
+ columns.find((c) => c.toLowerCase() === 'id') ??
74
+ columns.find((c) => c.toLowerCase().endsWith('_id')) ??
75
+ columns[0] ??
76
+ null
77
+ )
78
+ }
79
+
80
+ export function formatCell(v: unknown): string {
81
+ if (v === null || v === undefined) return ''
82
+ if (typeof v === 'object') return JSON.stringify(v)
83
+ return String(v)
84
+ }
85
+
86
+ export function isNull(v: unknown): boolean {
87
+ return v === null || v === undefined
88
+ }
89
+
90
+ export function ellipsize(s: string, max = ELLIPSIS_AT): string {
91
+ if (s.length <= max) return s
92
+ return s.slice(0, max - 1) + '…'
93
+ }
94
+
95
+ export function visibleRows(result: QueryResult, limit = TABLE_ROW_LIMIT): Array<Record<string, unknown>> {
96
+ return result.result.records.slice(0, limit).map((rec) => rec.values)
97
+ }
98
+
99
+ /**
100
+ * Produce a stable, DOM-free HTML string for the given query result.
101
+ * Used by the golden snapshot test. Intentionally minimal: no inline
102
+ * styles, no event handlers, no editing affordances — those live in the
103
+ * Svelte component.
104
+ */
105
+ export function renderTableHtml(
106
+ result: QueryResult,
107
+ opts: VisibleColumnsOptions = {},
108
+ ): string {
109
+ const columns = visibleColumns(result, opts)
110
+ const pk = inferPkColumn(columns)
111
+ const rows = visibleRows(result)
112
+ const lines: string[] = []
113
+ lines.push('<table>')
114
+ lines.push('<thead><tr>')
115
+ lines.push('<th class="num">#</th>')
116
+ for (const c of columns) {
117
+ const kind = identityKind(c)
118
+ const mark = kind === 'rid'
119
+ ? '<span class="badge sys">sys</span>'
120
+ : kind === 'id'
121
+ ? '<span class="badge pk">pk</span>'
122
+ : c === pk
123
+ ? '<span class="badge pk">pk</span>'
124
+ : ''
125
+ const cls = kind ? ` class="id ${kind}"` : ''
126
+ lines.push(`<th${cls}>${escapeHtml(c)}${mark}</th>`)
127
+ }
128
+ lines.push('</tr></thead>')
129
+ lines.push('<tbody>')
130
+ rows.forEach((row, i) => {
131
+ lines.push('<tr>')
132
+ lines.push(`<td class="num">${i + 1}</td>`)
133
+ for (const c of columns) {
134
+ const v = row[c]
135
+ const kind = identityKind(c)
136
+ const cls = kind ? ` class="id ${kind}"` : ''
137
+ if (isNull(v)) {
138
+ lines.push(`<td${cls}><span class="null">NULL</span></td>`)
139
+ } else {
140
+ lines.push(`<td${cls}><span class="cell">${escapeHtml(ellipsize(formatCell(v)))}</span></td>`)
141
+ }
142
+ }
143
+ lines.push('</tr>')
144
+ })
145
+ lines.push('</tbody>')
146
+ lines.push('</table>')
147
+ return lines.join('')
148
+ }
149
+
150
+ function escapeHtml(s: string): string {
151
+ return s
152
+ .replace(/&/g, '&amp;')
153
+ .replace(/</g, '&lt;')
154
+ .replace(/>/g, '&gt;')
155
+ .replace(/"/g, '&quot;')
156
+ }
@@ -0,0 +1,11 @@
1
+ import type { CapabilityRenderer } from './types'
2
+ import TableRenderer from './TableRenderer.svelte'
3
+
4
+ export const tableRenderer: CapabilityRenderer = {
5
+ capability: 'table',
6
+ // Table is the universal fallback — it can paint any shaped result that
7
+ // has `columns` and `records`. The registry uses this both for hint
8
+ // matches and as the last-resort default.
9
+ renders: (result) => !!result?.result && Array.isArray(result.result.columns),
10
+ component: TableRenderer,
11
+ }
@@ -0,0 +1,33 @@
1
+ import type { Component } from 'svelte'
2
+ import type { QueryResult, RedClient } from '#reddb'
3
+
4
+ export type Capability =
5
+ | 'table'
6
+ | 'graph'
7
+ | 'hypertable'
8
+ | 'kv'
9
+ | 'vector'
10
+ | 'queue'
11
+ | 'stats'
12
+ | 'diff'
13
+ | 'document'
14
+ | 'json'
15
+
16
+ export interface RendererProps {
17
+ result: QueryResult
18
+ collection?: string
19
+ client?: RedClient
20
+ subpage?: string
21
+ /** When true, reddb system fields (rid, collection, kind, tenant,
22
+ * created_at, updated_at) are shown ahead of user fields. Default false:
23
+ * the user-defined shape is what they're trying to read. */
24
+ showSystem?: boolean
25
+ }
26
+
27
+ export interface CapabilityRenderer {
28
+ capability: Capability
29
+ /** Result-shape disambiguation: does this renderer claim this result? */
30
+ renders(result: QueryResult): boolean
31
+ /** Svelte component that paints the result. */
32
+ component: Component<RendererProps>
33
+ }