@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,41 @@
1
+ // Single source of truth for "what version am I" across the Web/PWA, the
2
+ // Embeddable Lib, and the Tauri desktop webview (which all run this same
3
+ // bundle). Mirrors ../red-skills/src/packages/build-info: the version is
4
+ // injected at build time by Vite `define` (see vite.config.ts /
5
+ // vite.embed.config.ts) from `packages/ui/package.json` — overridable by the
6
+ // `RED_BUILD_VERSION` env the release CI sets from the git tag. Falls back to
7
+ // `0.0.0-dev` for an un-stamped local dev build.
8
+
9
+ declare const __RED_UI_VERSION__: string | undefined;
10
+ declare const __RED_UI_GIT_SHA__: string | undefined;
11
+ declare const __RED_UI_BUILD_TIME__: string | undefined;
12
+
13
+ export interface BuildInfo {
14
+ version: string;
15
+ gitSha: string;
16
+ buildTime: string;
17
+ }
18
+
19
+ function injected(read: () => string | undefined): string | undefined {
20
+ try {
21
+ const v = read();
22
+ return typeof v === "string" && v.length > 0 ? v : undefined;
23
+ } catch {
24
+ return undefined;
25
+ }
26
+ }
27
+
28
+ function stripTagPrefix(v: string): string {
29
+ return v.startsWith("v") ? v.slice(1) : v;
30
+ }
31
+
32
+ export function readBuildInfo(): BuildInfo {
33
+ return {
34
+ version: stripTagPrefix(injected(() => __RED_UI_VERSION__) ?? "0.0.0-dev"),
35
+ gitSha: injected(() => __RED_UI_GIT_SHA__) ?? "unknown",
36
+ buildTime: injected(() => __RED_UI_BUILD_TIME__) ?? "unknown",
37
+ };
38
+ }
39
+
40
+ /** The resolved app version, e.g. `0.1.1` (or `0.0.0-dev` un-stamped). */
41
+ export const RED_UI_VERSION: string = readBuildInfo().version;
@@ -0,0 +1,221 @@
1
+ // Per-collection capability detection.
2
+ //
3
+ // Prefer metadata from `GET /collections/<name>` when reddb exposes it.
4
+ // Fallback: run `SELECT * FROM <name> LIMIT 1` and inspect either the
5
+ // QueryResult.capability field or `red_capabilities` on the first row.
6
+ // Default to `table` when the collection is empty or detection fails — this
7
+ // is the honest baseline, since every collection is at least table-shaped.
8
+
9
+ import type { CollectionMetadata, RedClient } from '#reddb'
10
+ import type { QueryResult } from '#reddb'
11
+
12
+ export type Capability =
13
+ | 'table'
14
+ | 'graph'
15
+ | 'hypertable'
16
+ | 'kv'
17
+ | 'vector'
18
+ | 'queue'
19
+ | 'stats'
20
+ | 'diff'
21
+ | 'document'
22
+
23
+ // Priority order: more specific capabilities win over generic ones. A row
24
+ // tagged ["document","structured","table"] should surface as `document`, not
25
+ // `table`, because `table` is the universal fallback.
26
+ const PRIORITY: Capability[] = [
27
+ 'vector',
28
+ 'queue',
29
+ 'stats',
30
+ 'diff',
31
+ 'hypertable',
32
+ 'graph',
33
+ 'kv',
34
+ 'document',
35
+ 'table',
36
+ ]
37
+
38
+ const ALIASES: Record<string, Capability> = {
39
+ table: 'table',
40
+ structured: 'table',
41
+ graph: 'graph',
42
+ node: 'graph',
43
+ edge: 'graph',
44
+ hypertable: 'hypertable',
45
+ timeseries: 'hypertable',
46
+ 'time-series': 'hypertable',
47
+ kv: 'kv',
48
+ 'key-value': 'kv',
49
+ vector: 'vector',
50
+ embedding: 'vector',
51
+ queue: 'queue',
52
+ stream: 'queue',
53
+ message: 'queue',
54
+ stats: 'stats',
55
+ statistic: 'stats',
56
+ statistics: 'stats',
57
+ metric: 'stats',
58
+ metrics: 'stats',
59
+ document: 'document',
60
+ doc: 'document',
61
+ time_series: 'hypertable',
62
+ key_value: 'kv',
63
+ mixed: 'table',
64
+ hll: 'stats',
65
+ sketch: 'stats',
66
+ filter: 'stats',
67
+ cuckoo_filter: 'stats',
68
+ diff: 'diff',
69
+ vcs_diff: 'diff',
70
+ }
71
+
72
+ function sqlString(s: string): string {
73
+ return `'${s.replace(/'/g, "''")}'`
74
+ }
75
+
76
+ export function pickCapability(tags: ReadonlyArray<string>): Capability {
77
+ if (!tags || tags.length === 0) return 'table'
78
+ const mapped = new Set<Capability>()
79
+ for (const raw of tags) {
80
+ const t = String(raw).toLowerCase()
81
+ const cap = ALIASES[t]
82
+ if (cap) mapped.add(cap)
83
+ }
84
+ for (const cap of PRIORITY) {
85
+ if (mapped.has(cap)) return cap
86
+ }
87
+ return 'table'
88
+ }
89
+
90
+ export function tagsFromCollectionMetadata(meta: CollectionMetadata): string[] {
91
+ const tags: string[] = []
92
+ if (meta.kind) tags.push(meta.kind)
93
+ if (meta.capability) tags.push(meta.capability)
94
+ if (Array.isArray(meta.capabilities)) {
95
+ for (const tag of meta.capabilities) if (typeof tag === 'string') tags.push(tag)
96
+ }
97
+ const schemaKind = meta.schema?.['kind']
98
+ if (typeof schemaKind === 'string') tags.push(schemaKind)
99
+ const schemaCapability = meta.schema?.['capability']
100
+ if (typeof schemaCapability === 'string') tags.push(schemaCapability)
101
+ return tags
102
+ }
103
+
104
+ export function capabilityFromCatalogModel(model: unknown): Capability | null {
105
+ if (typeof model !== 'string') return null
106
+ return ALIASES[model.toLowerCase()] ?? null
107
+ }
108
+
109
+ async function detectProbabilisticCapability(client: RedClient, collection: string): Promise<Capability | null> {
110
+ const safe = collection.replace(/[^A-Za-z0-9_./-]/g, '')
111
+ for (const query of [`HLL INFO ${safe}`, `SKETCH INFO ${safe}`, `FILTER INFO ${safe}`]) {
112
+ try {
113
+ const r = await client.query(query)
114
+ if (r.ok) return 'stats'
115
+ } catch {
116
+ // Try the next probabilistic read form. RedDB may report FILTER as
117
+ // model=mixed in red.collections, so SELECT probing is not enough.
118
+ }
119
+ }
120
+ return null
121
+ }
122
+
123
+ function columnsFromResult(r: QueryResult): string[] {
124
+ const cols = new Set((r.result?.columns ?? []).map((c) => c.toLowerCase()))
125
+ const first = r.result?.records?.[0]?.values
126
+ if (first) {
127
+ for (const key of Object.keys(first)) cols.add(key.toLowerCase())
128
+ }
129
+ return [...cols]
130
+ }
131
+
132
+ function inferCapabilityFromShape(r: QueryResult): Capability | null {
133
+ const cols = columnsFromResult(r)
134
+ const has = (name: string) => cols.includes(name)
135
+ const hasAny = (names: string[]) => names.some(has)
136
+ const first = r.result?.records?.[0]?.values
137
+ const kind = first?.kind ?? first?.red_kind ?? first?.red_entity_type
138
+
139
+ if (kind === 'node' || kind === 'edge' || kind === 'graph') return 'graph'
140
+ if (kind === 'document') return 'document'
141
+ if (kind === 'kv' || (has('key') && has('value'))) return 'kv'
142
+ if (hasAny(['message', 'payload', 'body']) && hasAny(['message_id', 'state', 'status', 'consumer', 'delivery_state'])) return 'queue'
143
+ if (hasAny(['timestamp', 'time', 'ts', 'event_time', 'bucket']) && hasAny(['value', 'count', 'metric'])) return 'hypertable'
144
+ if (hasAny(['metric', 'stat', 'name', 'table']) && hasAny(['value', 'count', 'total', 'avg', 'rate', 'row_count', 'page_count', 'cardinality', 'memory_bytes'])) return 'stats'
145
+ return null
146
+ }
147
+
148
+ export async function detectCapability(
149
+ client: RedClient,
150
+ collection: string,
151
+ opts: { useMetadata?: boolean } = {},
152
+ ): Promise<Capability> {
153
+ if (opts.useMetadata) {
154
+ try {
155
+ const meta = await client.collection(collection)
156
+ const tags = tagsFromCollectionMetadata(meta)
157
+ if (tags.length > 0) return pickCapability(tags)
158
+ } catch {
159
+ // Metadata is optional for older reddb builds; fall through to row probing.
160
+ }
161
+ }
162
+
163
+ try {
164
+ const r = await client.query(`SELECT model FROM red.collections WHERE name = ${sqlString(collection)} LIMIT 1`)
165
+ const model = r.result?.records?.[0]?.values?.model
166
+ const cap = capabilityFromCatalogModel(model)
167
+ if (cap && cap !== 'table') return cap
168
+ if (typeof model === 'string' && model.toLowerCase() === 'mixed') {
169
+ const probabilistic = await detectProbabilisticCapability(client, collection)
170
+ if (probabilistic) return probabilistic
171
+ }
172
+ } catch {
173
+ // Older servers may not expose red.collections; row probing still works.
174
+ }
175
+
176
+ try {
177
+ const safe = collection.replace(/[^A-Za-z0-9_./-]/g, '')
178
+ const r = await client.query(`SELECT * FROM ${safe} LIMIT 1`)
179
+ const tags: string[] = []
180
+ if (r.capability) tags.push(r.capability)
181
+ const first = r.result?.records?.[0]?.values
182
+ const rc = first ? (first['red_capabilities'] as unknown) : undefined
183
+ if (Array.isArray(rc)) {
184
+ for (const t of rc) if (typeof t === 'string') tags.push(t)
185
+ } else if (typeof rc === 'string') {
186
+ // reddb stamps comma-separated capabilities ("graph,graph_node");
187
+ // split so each individual tag has a chance to match an alias.
188
+ for (const t of rc.split(/[,\s]+/)) if (t) tags.push(t)
189
+ }
190
+ const tagged = pickCapability(tags)
191
+ if (tagged !== 'table') return tagged
192
+ return inferCapabilityFromShape(r) ?? 'table'
193
+ } catch {
194
+ return await detectProbabilisticCapability(client, collection) ?? 'table'
195
+ }
196
+ }
197
+
198
+ export interface CapabilityGlyph {
199
+ glyph: string
200
+ label: string
201
+ }
202
+
203
+ // Unicode glyphs chosen to read clearly on both light and dark surfaces
204
+ // at 12–14px monospace. Geometric shapes only — no emoji (color-inconsistent).
205
+ export const CAPABILITY_GLYPHS: Record<Capability, CapabilityGlyph> = {
206
+ table: { glyph: '▦', label: 'Table' },
207
+ graph: { glyph: '◇', label: 'Graph' },
208
+ hypertable: { glyph: '⊞', label: 'Hypertable' },
209
+ kv: { glyph: '⋮', label: 'Key-value' },
210
+ vector: { glyph: '→', label: 'Vector' },
211
+ queue: { glyph: '≡', label: 'Queue' },
212
+ stats: { glyph: '◔', label: 'Stats' },
213
+ diff: { glyph: '⇄', label: 'Diff' },
214
+ document: { glyph: '¶', label: 'Document' },
215
+ }
216
+
217
+ const INTERNAL_PREFIX = /^red[._]/i
218
+
219
+ export function isInternalCollection(name: string): boolean {
220
+ return INTERNAL_PREFIX.test(name)
221
+ }
package/src/lib/cn.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]): string {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,116 @@
1
+ import type { QueryRow } from '#reddb'
2
+
3
+ export interface CollectionCatalogMetadata {
4
+ name: string
5
+ model?: string
6
+ schemaMode?: string
7
+ entities?: number
8
+ segments?: number
9
+ indices?: number
10
+ inMemoryBytes?: number
11
+ onDiskBytes?: number
12
+ internal?: boolean
13
+ tenantId?: string
14
+ queueMode?: string
15
+ dimension?: number
16
+ metric?: string
17
+ sessionKey?: string
18
+ sessionGapMs?: number
19
+ }
20
+
21
+ export function sqlString(s: string): string {
22
+ return `'${s.replace(/'/g, "''")}'`
23
+ }
24
+
25
+ export function collectionCatalogQuery(collection: string): string {
26
+ return [
27
+ 'SELECT name, model, schema_mode, entities, segments, indices,',
28
+ 'in_memory_bytes, on_disk_bytes, internal, tenant_id, queue_mode,',
29
+ 'dimension, metric, session_key, session_gap_ms',
30
+ 'FROM red.collections',
31
+ `WHERE name = ${sqlString(collection)}`,
32
+ 'LIMIT 1',
33
+ ].join(' ')
34
+ }
35
+
36
+ function asString(v: unknown): string | undefined {
37
+ return typeof v === 'string' && v.length > 0 ? v : undefined
38
+ }
39
+
40
+ function asNumber(v: unknown): number | undefined {
41
+ if (typeof v === 'number' && Number.isFinite(v)) return v
42
+ if (typeof v === 'string' && v.trim() !== '') {
43
+ const n = Number(v)
44
+ if (Number.isFinite(n)) return n
45
+ }
46
+ return undefined
47
+ }
48
+
49
+ function asBoolean(v: unknown): boolean | undefined {
50
+ if (typeof v === 'boolean') return v
51
+ if (typeof v === 'string') {
52
+ if (v.toLowerCase() === 'true') return true
53
+ if (v.toLowerCase() === 'false') return false
54
+ }
55
+ return undefined
56
+ }
57
+
58
+ export function collectionCatalogFromRow(row: QueryRow | undefined): CollectionCatalogMetadata | null {
59
+ if (!row) return null
60
+ const v = row.values
61
+ const name = asString(v.name)
62
+ if (!name) return null
63
+ return {
64
+ name,
65
+ model: asString(v.model),
66
+ schemaMode: asString(v.schema_mode),
67
+ entities: asNumber(v.entities),
68
+ segments: asNumber(v.segments),
69
+ indices: asNumber(v.indices),
70
+ inMemoryBytes: asNumber(v.in_memory_bytes),
71
+ onDiskBytes: asNumber(v.on_disk_bytes),
72
+ internal: asBoolean(v.internal),
73
+ tenantId: asString(v.tenant_id),
74
+ queueMode: asString(v.queue_mode),
75
+ dimension: asNumber(v.dimension),
76
+ metric: asString(v.metric),
77
+ sessionKey: asString(v.session_key),
78
+ sessionGapMs: asNumber(v.session_gap_ms),
79
+ }
80
+ }
81
+
82
+ export function formatCount(value: number | undefined): string {
83
+ if (value === undefined) return 'n/a'
84
+ return new Intl.NumberFormat('en-US').format(value)
85
+ }
86
+
87
+ export function formatBytes(value: number | undefined): string {
88
+ if (value === undefined) return 'n/a'
89
+ if (value === 0) return '0 B'
90
+ const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']
91
+ const exponent = Math.min(Math.floor(Math.log(value) / Math.log(1024)), units.length - 1)
92
+ const amount = value / 1024 ** exponent
93
+ const digits = amount >= 100 || exponent === 0 ? 0 : 1
94
+ return `${amount.toFixed(digits)} ${units[exponent]}`
95
+ }
96
+
97
+ export function formatDurationMs(value: number | undefined): string | null {
98
+ if (value === undefined) return null
99
+ if (value < 1000) return `${value} ms`
100
+ if (value < 60_000) return `${(value / 1000).toFixed(value >= 10_000 ? 0 : 1)} s`
101
+ if (value < 3_600_000) return `${(value / 60_000).toFixed(value >= 600_000 ? 0 : 1)} min`
102
+ return `${(value / 3_600_000).toFixed(value >= 36_000_000 ? 0 : 1)} h`
103
+ }
104
+
105
+ export function collectionCatalogBadges(meta: CollectionCatalogMetadata): string[] {
106
+ const badges: string[] = []
107
+ if (meta.schemaMode) badges.push(meta.schemaMode)
108
+ if (meta.dimension !== undefined) badges.push(`${formatCount(meta.dimension)}d`)
109
+ if (meta.metric) badges.push(meta.metric)
110
+ if (meta.queueMode) badges.push(meta.queueMode)
111
+ if (meta.sessionKey) badges.push(`session: ${meta.sessionKey}`)
112
+ const gap = formatDurationMs(meta.sessionGapMs)
113
+ if (gap) badges.push(`gap ${gap}`)
114
+ if (meta.internal) badges.push('internal')
115
+ return badges
116
+ }
@@ -0,0 +1,33 @@
1
+ import type { Capability } from './renderers'
2
+
3
+ export function defaultSubpage(capability: Capability | undefined): string {
4
+ if (!capability) return 'table'
5
+ if (capability === 'graph') return 'graph'
6
+ return capability
7
+ }
8
+
9
+ export function subpageCapability(subpage: string | undefined, fallback: Capability | undefined): Capability | undefined {
10
+ switch (subpage) {
11
+ case 'graph':
12
+ case 'svg':
13
+ return 'graph'
14
+ case 'table':
15
+ return 'table'
16
+ case 'json':
17
+ return 'json'
18
+ case 'hypertable':
19
+ case 'kv':
20
+ case 'vector':
21
+ case 'queue':
22
+ case 'stats':
23
+ case 'diff':
24
+ case 'document':
25
+ return subpage
26
+ default:
27
+ return fallback
28
+ }
29
+ }
30
+
31
+ export function collectionPageHref(collection: string, subpage: string): string {
32
+ return `/c/${encodeURIComponent(collection)}/p/${encodeURIComponent(subpage)}`
33
+ }