@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.
- package/LICENSE +17 -0
- package/bin/red-ui.js +259 -0
- package/build/CNAME +1 -0
- package/build/_app/env.js +1 -0
- package/build/_app/immutable/assets/0.C63jY5jX.css +1 -0
- package/build/_app/immutable/assets/10.Dblyk0Kr.css +1 -0
- package/build/_app/immutable/assets/EmptyState.RXcH7ryE.css +1 -0
- package/build/_app/immutable/assets/Workspace.BCzG1DsH.css +1 -0
- package/build/_app/immutable/assets/inter-cyrillic-ext-wght-normal.BOeWTOD4.woff2 +0 -0
- package/build/_app/immutable/assets/inter-cyrillic-wght-normal.DqGufNeO.woff2 +0 -0
- package/build/_app/immutable/assets/inter-greek-ext-wght-normal.DlzME5K_.woff2 +0 -0
- package/build/_app/immutable/assets/inter-greek-wght-normal.CkhJZR-_.woff2 +0 -0
- package/build/_app/immutable/assets/inter-latin-ext-wght-normal.DO1Apj_S.woff2 +0 -0
- package/build/_app/immutable/assets/inter-latin-wght-normal.Dx4kXJAl.woff2 +0 -0
- package/build/_app/immutable/assets/inter-vietnamese-wght-normal.CBcvBZtf.woff2 +0 -0
- package/build/_app/immutable/assets/jetbrains-mono-cyrillic-wght-normal.D73BlboJ.woff2 +0 -0
- package/build/_app/immutable/assets/jetbrains-mono-greek-wght-normal.Bw9x6K1M.woff2 +0 -0
- package/build/_app/immutable/assets/jetbrains-mono-latin-ext-wght-normal.DBQx-q_a.woff2 +0 -0
- package/build/_app/immutable/assets/jetbrains-mono-latin-wght-normal.B9CIFXIH.woff2 +0 -0
- package/build/_app/immutable/assets/jetbrains-mono-vietnamese-wght-normal.Bt-aOZkq.woff2 +0 -0
- package/build/_app/immutable/chunks/3j4cZ58b.js +1 -0
- package/build/_app/immutable/chunks/69_IOA4Y.js +1 -0
- package/build/_app/immutable/chunks/ALqaDrNQ.js +2 -0
- package/build/_app/immutable/chunks/BGcIe2ep.js +2 -0
- package/build/_app/immutable/chunks/BMHKbvs8.js +1 -0
- package/build/_app/immutable/chunks/BQHbwf54.js +916 -0
- package/build/_app/immutable/chunks/Bgso8cDH.js +311 -0
- package/build/_app/immutable/chunks/Bmgx1mfv.js +310 -0
- package/build/_app/immutable/chunks/BrvvOVaJ.js +1 -0
- package/build/_app/immutable/chunks/Bzak7iHL.js +1 -0
- package/build/_app/immutable/chunks/D9CTkMjK.js +1 -0
- package/build/_app/immutable/chunks/DIeogL5L.js +1 -0
- package/build/_app/immutable/chunks/DXY_Rxa7.js +113 -0
- package/build/_app/immutable/chunks/DhEqZVGG.js +1 -0
- package/build/_app/immutable/chunks/Ov8N-vxt.js +1 -0
- package/build/_app/immutable/chunks/oiO4RLqp.js +1 -0
- package/build/_app/immutable/entry/app.D_IVtpdv.js +2 -0
- package/build/_app/immutable/entry/start.3oC0UQ_Q.js +1 -0
- package/build/_app/immutable/nodes/0.CHPiFxDo.js +1 -0
- package/build/_app/immutable/nodes/1.BC_4YZ0h.js +1 -0
- package/build/_app/immutable/nodes/10.B7DMKSNc.js +60 -0
- package/build/_app/immutable/nodes/2.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/3.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/4.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/5.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/6.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/7.BZX9Iird.js +134 -0
- package/build/_app/immutable/nodes/8.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/9.CAG5_cGP.js +1 -0
- package/build/_app/version.json +1 -0
- package/build/favicon.png +0 -0
- package/build/index.html +59 -0
- package/dist/embed/RedUiEmbedRoot-BGoLxJf1.js +26562 -0
- package/dist/embed/RedUiEmbedRoot-BGoLxJf1.js.map +1 -0
- package/dist/embed/bundle-mjs-UuVRz3O4.js +3047 -0
- package/dist/embed/bundle-mjs-UuVRz3O4.js.map +1 -0
- package/dist/embed/core-QuRYvMt8.js +118 -0
- package/dist/embed/core-QuRYvMt8.js.map +1 -0
- package/dist/embed/index-B6ZQhCsu.js +577 -0
- package/dist/embed/index-B6ZQhCsu.js.map +1 -0
- package/dist/embed/index-client-ItCEU64C.js +3087 -0
- package/dist/embed/index-client-ItCEU64C.js.map +1 -0
- package/dist/embed/index-fad77a13.esm-BqPj0saH.js +1318 -0
- package/dist/embed/index-fad77a13.esm-BqPj0saH.js.map +1 -0
- package/dist/embed/index.js +11 -0
- package/dist/embed/index.js.map +1 -0
- package/dist/embed/sigma-node-border.esm-CGtlgqmo.js +432 -0
- package/dist/embed/sigma-node-border.esm-CGtlgqmo.js.map +1 -0
- package/dist/embed/sigma.esm-y0qgY-OC.js +2355 -0
- package/dist/embed/sigma.esm-y0qgY-OC.js.map +1 -0
- package/dist/embed/ui.css +1 -0
- package/package.json +101 -0
- package/src/app.css +238 -0
- package/src/lib/ActivityIndicator.svelte +100 -0
- package/src/lib/ClusterNode.svelte +119 -0
- package/src/lib/ClusterView.svelte +541 -0
- package/src/lib/CollectionHistory.svelte +266 -0
- package/src/lib/CollectionsView.svelte +14 -0
- package/src/lib/CommandPalette.svelte +237 -0
- package/src/lib/ConnectDropdown.svelte +222 -0
- package/src/lib/EmptyState.svelte +27 -0
- package/src/lib/LiveChanges.svelte +195 -0
- package/src/lib/MasterPasswordDialog.svelte +168 -0
- package/src/lib/PageHeader.svelte +32 -0
- package/src/lib/PendingChangesPanel.svelte +153 -0
- package/src/lib/QueryEditor.svelte +122 -0
- package/src/lib/ResultsPane.svelte +747 -0
- package/src/lib/SchemaTree.svelte +193 -0
- package/src/lib/SecurityView.svelte +255 -0
- package/src/lib/ShortcutOverlay.svelte +104 -0
- package/src/lib/Splash.svelte +46 -0
- package/src/lib/StatusBar.svelte +163 -0
- package/src/lib/SubNav.svelte +62 -0
- package/src/lib/Topbar.svelte +119 -0
- package/src/lib/Workspace.svelte +247 -0
- package/src/lib/activity.svelte.ts +91 -0
- package/src/lib/build-info.ts +41 -0
- package/src/lib/capability.ts +221 -0
- package/src/lib/cn.ts +6 -0
- package/src/lib/collection-catalog.ts +116 -0
- package/src/lib/collection-pages.ts +33 -0
- package/src/lib/connection-history.ts +301 -0
- package/src/lib/connections.svelte.ts +493 -0
- package/src/lib/embed/RedUiEmbedRoot.svelte +31 -0
- package/src/lib/embed/index.ts +234 -0
- package/src/lib/graph-loader/contract.ts +336 -0
- package/src/lib/graph-loader/drilldown.ts +334 -0
- package/src/lib/graph-loader/index.ts +13 -0
- package/src/lib/index.ts +114 -0
- package/src/lib/pending-changes.svelte.ts +149 -0
- package/src/lib/query-tabs.svelte.ts +130 -0
- package/src/lib/reddb/boot-params.ts +254 -0
- package/src/lib/reddb/cdc-stream-client.ts +234 -0
- package/src/lib/reddb/client.ts +698 -0
- package/src/lib/reddb/connection-bootstrap.ts +260 -0
- package/src/lib/reddb/connection-provider.contract.ts +72 -0
- package/src/lib/reddb/connection-provider.ts +72 -0
- package/src/lib/reddb/index.ts +14 -0
- package/src/lib/reddb/injected-client-provider.ts +76 -0
- package/src/lib/reddb/local-url-provider.ts +245 -0
- package/src/lib/reddb/secure-store.ts +41 -0
- package/src/lib/reddb/server-capabilities.ts +71 -0
- package/src/lib/reddb/tauri-encrypted-store.ts +64 -0
- package/src/lib/reddb/transports.ts +89 -0
- package/src/lib/reddb/types.ts +32 -0
- package/src/lib/reddb/uri.ts +54 -0
- package/src/lib/reddb/web-encrypted-store.ts +201 -0
- package/src/lib/renderers/DiffRenderer.svelte +184 -0
- package/src/lib/renderers/DocumentRenderer.svelte +134 -0
- package/src/lib/renderers/GraphRenderer.svelte +1328 -0
- package/src/lib/renderers/HypertableRenderer.svelte +253 -0
- package/src/lib/renderers/JsonRenderer.svelte +30 -0
- package/src/lib/renderers/KvRenderer.svelte +150 -0
- package/src/lib/renderers/QueueRenderer.svelte +286 -0
- package/src/lib/renderers/StatsRenderer.svelte +123 -0
- package/src/lib/renderers/TableRenderer.svelte +440 -0
- package/src/lib/renderers/VectorRenderer.svelte +388 -0
- package/src/lib/renderers/diff-render.ts +122 -0
- package/src/lib/renderers/diff.ts +9 -0
- package/src/lib/renderers/document-render.ts +64 -0
- package/src/lib/renderers/document.ts +9 -0
- package/src/lib/renderers/graph-render.ts +631 -0
- package/src/lib/renderers/graph-sigma.ts +395 -0
- package/src/lib/renderers/graph.ts +9 -0
- package/src/lib/renderers/hypertable-render.ts +320 -0
- package/src/lib/renderers/hypertable.ts +9 -0
- package/src/lib/renderers/index.ts +42 -0
- package/src/lib/renderers/json-render.ts +89 -0
- package/src/lib/renderers/json.ts +10 -0
- package/src/lib/renderers/kv-render.ts +193 -0
- package/src/lib/renderers/kv.ts +9 -0
- package/src/lib/renderers/queue-render.ts +197 -0
- package/src/lib/renderers/queue.ts +9 -0
- package/src/lib/renderers/registry.ts +68 -0
- package/src/lib/renderers/stats-render.ts +73 -0
- package/src/lib/renderers/stats.ts +9 -0
- package/src/lib/renderers/table-render.ts +156 -0
- package/src/lib/renderers/table.ts +11 -0
- package/src/lib/renderers/types.ts +33 -0
- package/src/lib/renderers/vector-render.ts +154 -0
- package/src/lib/renderers/vector.ts +9 -0
- package/src/lib/router.svelte.ts +163 -0
- package/src/lib/secureStore.svelte.ts +148 -0
- package/src/lib/shadow-dom-spike/ShadowDomSpikeCore.svelte +198 -0
- package/src/lib/shadow-dom-spike/mount-shadow-dom-spike.ts +94 -0
- package/src/lib/shortcuts.ts +22 -0
- package/src/lib/surface.ts +80 -0
- package/src/lib/tabs.svelte.ts +115 -0
- 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,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
|
+
}
|