@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,747 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte'
|
|
3
|
+
import { useRouter } from '$lib/router.svelte'
|
|
4
|
+
import { connection } from '$lib/connections.svelte'
|
|
5
|
+
import { secureStore } from '$lib/secureStore.svelte'
|
|
6
|
+
import { activity } from '$lib/activity.svelte'
|
|
7
|
+
import { tabs, type Tab } from '$lib/tabs.svelte'
|
|
8
|
+
import { queryTabs } from '$lib/query-tabs.svelte'
|
|
9
|
+
import { defaultSubpage, subpageCapability } from '$lib/collection-pages'
|
|
10
|
+
import {
|
|
11
|
+
collectionCatalogBadges,
|
|
12
|
+
collectionCatalogFromRow,
|
|
13
|
+
collectionCatalogQuery,
|
|
14
|
+
formatBytes,
|
|
15
|
+
formatCount,
|
|
16
|
+
type CollectionCatalogMetadata,
|
|
17
|
+
} from '$lib/collection-catalog'
|
|
18
|
+
import { registry } from '$lib/renderers'
|
|
19
|
+
import type { Capability } from '$lib/renderers'
|
|
20
|
+
import CollectionHistory from '$lib/CollectionHistory.svelte'
|
|
21
|
+
import LiveChanges from '$lib/LiveChanges.svelte'
|
|
22
|
+
import QueryEditor from '$lib/QueryEditor.svelte'
|
|
23
|
+
import type { CollectionMetadata, QueryResult, RedClient } from '#reddb'
|
|
24
|
+
import { X, Activity, Database, Plus, FileCode2, EyeOff, Eye, Info, HardDrive, Rows3, Layers3, ScanLine, GitBranch } from 'lucide-svelte'
|
|
25
|
+
|
|
26
|
+
const router = useRouter()
|
|
27
|
+
|
|
28
|
+
const OVERRIDE_CHOICES: { value: '' | Capability; label: string }[] = [
|
|
29
|
+
{ value: '', label: 'Auto' },
|
|
30
|
+
{ value: 'table', label: 'Table' },
|
|
31
|
+
{ value: 'graph', label: 'Graph' },
|
|
32
|
+
{ value: 'hypertable', label: 'Chart' },
|
|
33
|
+
{ value: 'kv', label: 'KV' },
|
|
34
|
+
{ value: 'vector', label: 'Vector' },
|
|
35
|
+
{ value: 'queue', label: 'Queue' },
|
|
36
|
+
{ value: 'stats', label: 'Stats' },
|
|
37
|
+
{ value: 'diff', label: 'Diff' },
|
|
38
|
+
{ value: 'document', label: 'Document' },
|
|
39
|
+
{ value: 'json', label: 'JSON' },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
let results = $state<Record<string, QueryResult | null>>({})
|
|
43
|
+
let errors = $state<Record<string, string>>({})
|
|
44
|
+
let resultKeys = $state<Record<string, string>>({})
|
|
45
|
+
let catalogMetadata = $state<Record<string, CollectionCatalogMetadata | null>>({})
|
|
46
|
+
let catalogErrors = $state<Record<string, string>>({})
|
|
47
|
+
let metadata = $state<Record<string, CollectionMetadata | null>>({})
|
|
48
|
+
let metadataErrors = $state<Record<string, string>>({})
|
|
49
|
+
let historyOpen = $state<Record<string, boolean>>({})
|
|
50
|
+
let previousActiveId = $state<string | null>(null)
|
|
51
|
+
let lastActiveId: string | null = null
|
|
52
|
+
let menuOpen = $state(false)
|
|
53
|
+
let metadataOpen = $state(false)
|
|
54
|
+
|
|
55
|
+
$effect(() => {
|
|
56
|
+
const current = tabs.activeId
|
|
57
|
+
if (current !== lastActiveId) {
|
|
58
|
+
previousActiveId = lastActiveId
|
|
59
|
+
lastActiveId = current
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Fetch a meaningful sub-graph: pull a window of edges first, then the
|
|
65
|
+
* nodes those edges actually reference (plus a few unconnected ones for
|
|
66
|
+
* context). A flat `SELECT *` on a CREATE GRAPH collection returns
|
|
67
|
+
* nodes-then-edges in insertion order, so without this split the first
|
|
68
|
+
* N rows are all nodes and zero edges land in the renderer.
|
|
69
|
+
*/
|
|
70
|
+
async function fetchGraphSubgraph(client: RedClient, collection: string) {
|
|
71
|
+
const EDGE_LIMIT = 50000
|
|
72
|
+
const CENTRALITY_LIMIT = 100
|
|
73
|
+
// reddb's WHERE rid IN (…) silently returns 0 once the list size
|
|
74
|
+
// exceeds a small threshold (somewhere around 8 in 1.3.0). Chunk to
|
|
75
|
+
// stay safely under that ceiling and union the results.
|
|
76
|
+
const IN_CHUNK = 5
|
|
77
|
+
// Run node-batch queries in parallel but bounded so we don't slam the
|
|
78
|
+
// server. 8 concurrent fits ~1k batches in roughly a second.
|
|
79
|
+
const CONCURRENCY = 8
|
|
80
|
+
|
|
81
|
+
const [edgesRes, centralityRes] = await Promise.all([
|
|
82
|
+
activity.track(
|
|
83
|
+
`${collection} · edges (LIMIT ${EDGE_LIMIT})`,
|
|
84
|
+
() => client.query(
|
|
85
|
+
`SELECT * FROM ${collection} WHERE from_rid IS NOT NULL LIMIT ${EDGE_LIMIT}`,
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
activity.track(
|
|
89
|
+
`${collection} · centrality (LIMIT ${CENTRALITY_LIMIT})`,
|
|
90
|
+
() => client.query(`GRAPH CENTRALITY LIMIT ${CENTRALITY_LIMIT}`),
|
|
91
|
+
).catch(() => null),
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
const centralityByRid = new Map<number, { score: number; rank: number; label?: string }>()
|
|
95
|
+
if (centralityRes?.ok) {
|
|
96
|
+
centralityRes.result.records.forEach((rec, index) => {
|
|
97
|
+
const rawId = rec.values.node_id ?? rec.values.rid ?? rec.values.id
|
|
98
|
+
const rawScore = rec.values.score ?? rec.values.centrality_score ?? rec.values.centrality
|
|
99
|
+
const id = typeof rawId === 'number' ? rawId : typeof rawId === 'string' ? Number(rawId) : NaN
|
|
100
|
+
const score = typeof rawScore === 'number' ? rawScore : typeof rawScore === 'string' ? Number(rawScore) : NaN
|
|
101
|
+
if (!Number.isFinite(id) || !Number.isFinite(score)) return
|
|
102
|
+
centralityByRid.set(id, {
|
|
103
|
+
score,
|
|
104
|
+
rank: index + 1,
|
|
105
|
+
label: typeof rec.values.label === 'string' ? rec.values.label : undefined,
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const rids = new Set<number>()
|
|
111
|
+
for (const rec of edgesRes.result.records) {
|
|
112
|
+
const f = rec.values.from_rid
|
|
113
|
+
const t = rec.values.to_rid
|
|
114
|
+
if (typeof f === 'number') rids.add(f)
|
|
115
|
+
if (typeof t === 'number') rids.add(t)
|
|
116
|
+
}
|
|
117
|
+
for (const rid of centralityByRid.keys()) rids.add(rid)
|
|
118
|
+
|
|
119
|
+
const ridList = [...rids]
|
|
120
|
+
const totalChunks = Math.ceil(ridList.length / IN_CHUNK)
|
|
121
|
+
const chunks: { idx: number; ids: string }[] = []
|
|
122
|
+
for (let i = 0; i < ridList.length; i += IN_CHUNK) {
|
|
123
|
+
chunks.push({
|
|
124
|
+
idx: Math.floor(i / IN_CHUNK) + 1,
|
|
125
|
+
ids: ridList.slice(i, i + IN_CHUNK).join(','),
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const nodeRecords: typeof edgesRes.result.records = []
|
|
130
|
+
let nodesRes = edgesRes // template for shape
|
|
131
|
+
|
|
132
|
+
// Bounded-concurrency worker pool. Each worker pulls the next chunk
|
|
133
|
+
// off the shared cursor; that pattern keeps CONCURRENCY requests in
|
|
134
|
+
// flight without spawning a Promise per chunk up-front.
|
|
135
|
+
let cursor = 0
|
|
136
|
+
async function worker() {
|
|
137
|
+
while (cursor < chunks.length) {
|
|
138
|
+
const my = chunks[cursor++]
|
|
139
|
+
const r = await activity.track(
|
|
140
|
+
`${collection} · nodes ${my.idx}/${totalChunks}`,
|
|
141
|
+
() => client.query(`SELECT * FROM ${collection} WHERE rid IN (${my.ids})`),
|
|
142
|
+
)
|
|
143
|
+
nodeRecords.push(...r.result.records.map((rec) => {
|
|
144
|
+
const rid = rec.values.rid
|
|
145
|
+
const centrality = typeof rid === 'number' ? centralityByRid.get(rid) : undefined
|
|
146
|
+
if (!centrality) return rec
|
|
147
|
+
return {
|
|
148
|
+
...rec,
|
|
149
|
+
values: {
|
|
150
|
+
...rec.values,
|
|
151
|
+
centrality_score: centrality.score,
|
|
152
|
+
centrality_rank: centrality.rank,
|
|
153
|
+
centrality_label: centrality.label,
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
}))
|
|
157
|
+
nodesRes = r
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
await Promise.all(Array.from({ length: CONCURRENCY }, worker))
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
...nodesRes,
|
|
164
|
+
record_count: nodeRecords.length + edgesRes.result.records.length,
|
|
165
|
+
result: {
|
|
166
|
+
...nodesRes.result,
|
|
167
|
+
records: [...nodeRecords, ...edgesRes.result.records],
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function safeCollectionName(collection: string): string {
|
|
173
|
+
return collection.replace(/[^A-Za-z0-9_./-]/g, '')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function sqlString(s: string): string {
|
|
177
|
+
return `'${s.replace(/'/g, "''")}'`
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function fetchCollectionCatalog(client: RedClient, collection: string): Promise<CollectionCatalogMetadata> {
|
|
181
|
+
const r = await client.query(collectionCatalogQuery(collection))
|
|
182
|
+
const meta = collectionCatalogFromRow(r.result.records[0])
|
|
183
|
+
if (!meta) throw new Error(`${collection} was not found in red.collections`)
|
|
184
|
+
return meta
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function fetchCollectionModel(client: RedClient, collection: string): Promise<string | null> {
|
|
188
|
+
try {
|
|
189
|
+
const r = await client.query(`SELECT model FROM red.collections WHERE name = ${sqlString(collection)} LIMIT 1`)
|
|
190
|
+
const model = r.result.records[0]?.values.model
|
|
191
|
+
return typeof model === 'string' ? model : null
|
|
192
|
+
} catch {
|
|
193
|
+
return null
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function fetchStatsCollection(client: RedClient, collection: string) {
|
|
198
|
+
const safe = safeCollectionName(collection)
|
|
199
|
+
const model = await fetchCollectionModel(client, collection)
|
|
200
|
+
if (model === 'hll') return client.query(`HLL INFO ${safe}`)
|
|
201
|
+
if (model === 'sketch') return client.query(`SKETCH INFO ${safe}`)
|
|
202
|
+
if (model === 'filter') return client.query(`FILTER INFO ${safe}`)
|
|
203
|
+
if (model === 'mixed') {
|
|
204
|
+
for (const query of [`HLL INFO ${safe}`, `SKETCH INFO ${safe}`, `FILTER INFO ${safe}`]) {
|
|
205
|
+
try {
|
|
206
|
+
return await client.query(query)
|
|
207
|
+
} catch {
|
|
208
|
+
// Try the next probabilistic read form before falling back to SELECT.
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
return await client.query(`SELECT * FROM ${safe} LIMIT 200`)
|
|
214
|
+
} catch {
|
|
215
|
+
for (const query of [`HLL INFO ${safe}`, `SKETCH INFO ${safe}`, `FILTER INFO ${safe}`]) {
|
|
216
|
+
try {
|
|
217
|
+
return await client.query(query)
|
|
218
|
+
} catch {
|
|
219
|
+
// Try the next probabilistic read form.
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
throw new Error(`No stats read form worked for ${collection}`)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function fetchVectorSeed(client: RedClient, collection: string) {
|
|
227
|
+
const safe = safeCollectionName(collection)
|
|
228
|
+
const r = await client.query(`SELECT * FROM ${safe} LIMIT 200`)
|
|
229
|
+
return { ...r, capability: r.capability ?? 'vector' }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
$effect(() => {
|
|
233
|
+
if (secureStore.locked) return
|
|
234
|
+
const client = connection.client
|
|
235
|
+
if (!client) return
|
|
236
|
+
for (const tab of tabs.tabs) {
|
|
237
|
+
if (tab.kind !== 'collection') continue
|
|
238
|
+
const subpage = tab.subpage ?? defaultSubpage(tab.capability)
|
|
239
|
+
const shouldFetchGraph = subpage === 'graph' || subpage === 'svg'
|
|
240
|
+
const shouldFetchQueue = subpage === 'queue' || tab.capability === 'queue'
|
|
241
|
+
const shouldFetchVector = subpage === 'vector' || tab.capability === 'vector'
|
|
242
|
+
const shouldFetchStats = subpage === 'stats' || tab.capability === 'stats'
|
|
243
|
+
const fetchMode = shouldFetchGraph ? 'graph-subgraph' : shouldFetchQueue ? 'queue-peek' : shouldFetchVector ? 'vector-inspector' : shouldFetchStats ? 'stats-info' : tab.capability ?? 'unknown'
|
|
244
|
+
const cacheKey = `${tab.key}:${subpage}:${fetchMode}`
|
|
245
|
+
if (resultKeys[tab.id] && resultKeys[tab.id] !== cacheKey) {
|
|
246
|
+
delete results[tab.id]
|
|
247
|
+
delete errors[tab.id]
|
|
248
|
+
}
|
|
249
|
+
if (results[tab.id] !== undefined || errors[tab.id]) continue
|
|
250
|
+
results[tab.id] = null
|
|
251
|
+
resultKeys[tab.id] = cacheKey
|
|
252
|
+
const collection = tab.key
|
|
253
|
+
const job = shouldFetchGraph
|
|
254
|
+
? fetchGraphSubgraph(client, collection)
|
|
255
|
+
: shouldFetchQueue
|
|
256
|
+
? activity.track(
|
|
257
|
+
`${collection} · QUEUE PEEK (LIMIT 200)`,
|
|
258
|
+
() => client.query(`QUEUE PEEK ${collection} 200`),
|
|
259
|
+
)
|
|
260
|
+
: shouldFetchVector
|
|
261
|
+
? activity.track(
|
|
262
|
+
`${collection} · vector inspector seed`,
|
|
263
|
+
() => fetchVectorSeed(client, collection),
|
|
264
|
+
)
|
|
265
|
+
: shouldFetchStats
|
|
266
|
+
? activity.track(
|
|
267
|
+
`${collection} · stats preview`,
|
|
268
|
+
() => fetchStatsCollection(client, collection),
|
|
269
|
+
)
|
|
270
|
+
: activity.track(
|
|
271
|
+
`${collection} · SELECT * (LIMIT 200)`,
|
|
272
|
+
() => client.query(`SELECT * FROM ${collection} LIMIT 200`),
|
|
273
|
+
)
|
|
274
|
+
job
|
|
275
|
+
.then((r) => { results[tab.id] = r })
|
|
276
|
+
.catch((e) => { errors[tab.id] = (e as Error).message })
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
$effect(() => {
|
|
281
|
+
if (secureStore.locked) return
|
|
282
|
+
const client = connection.client
|
|
283
|
+
if (!client) return
|
|
284
|
+
for (const tab of tabs.tabs) {
|
|
285
|
+
if (tab.kind !== 'collection') continue
|
|
286
|
+
if (catalogMetadata[tab.id] !== undefined || catalogErrors[tab.id]) continue
|
|
287
|
+
catalogMetadata[tab.id] = null
|
|
288
|
+
activity.track(`catalog · ${tab.key}`, () => fetchCollectionCatalog(client, tab.key))
|
|
289
|
+
.then((m) => { catalogMetadata[tab.id] = m })
|
|
290
|
+
.catch((e) => { catalogErrors[tab.id] = (e as Error).message })
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
$effect(() => {
|
|
295
|
+
if (!metadataOpen) return
|
|
296
|
+
if (secureStore.locked) return
|
|
297
|
+
const client = connection.client
|
|
298
|
+
if (!client) return
|
|
299
|
+
for (const tab of tabs.tabs) {
|
|
300
|
+
if (tab.kind !== 'collection') continue
|
|
301
|
+
if (metadata[tab.id] !== undefined || metadataErrors[tab.id]) continue
|
|
302
|
+
metadata[tab.id] = null
|
|
303
|
+
activity.track(`metadata · ${tab.key}`, () => client.collection(tab.key))
|
|
304
|
+
.then((m) => { metadata[tab.id] = m })
|
|
305
|
+
.catch((e) => { metadataErrors[tab.id] = (e as Error).message })
|
|
306
|
+
}
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
function closeTab(e: MouseEvent, id: string) {
|
|
310
|
+
e.stopPropagation()
|
|
311
|
+
const tab = tabs.tabs.find((t) => t.id === id)
|
|
312
|
+
if (tab?.kind === 'query' && queryTabs.isDirty(id)) {
|
|
313
|
+
const ok = typeof window !== 'undefined'
|
|
314
|
+
? window.confirm('Discard unsaved query in this tab?')
|
|
315
|
+
: true
|
|
316
|
+
if (!ok) return
|
|
317
|
+
}
|
|
318
|
+
// If we're closing the collection that owns the current URL, the
|
|
319
|
+
// /c/[collection] route would just re-open the tab on every render.
|
|
320
|
+
// Navigate away so the URL stays in sync with what's open.
|
|
321
|
+
const closingUrlOwner = tab?.kind === 'collection' && tab.key === router.collection
|
|
322
|
+
tabs.close(id, previousActiveId)
|
|
323
|
+
queryTabs.remove(id)
|
|
324
|
+
delete results[id]
|
|
325
|
+
delete errors[id]
|
|
326
|
+
delete resultKeys[id]
|
|
327
|
+
delete catalogMetadata[id]
|
|
328
|
+
delete catalogErrors[id]
|
|
329
|
+
delete metadata[id]
|
|
330
|
+
delete metadataErrors[id]
|
|
331
|
+
delete historyOpen[id]
|
|
332
|
+
if (closingUrlOwner) {
|
|
333
|
+
const next = tabs.tabs.find((t) => t.kind === 'collection')
|
|
334
|
+
router.go(
|
|
335
|
+
next
|
|
336
|
+
? { view: 'collection', collection: next.key, subpage: next.subpage ?? defaultSubpage(next.capability) }
|
|
337
|
+
: { view: 'collections' },
|
|
338
|
+
undefined,
|
|
339
|
+
true,
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function openNewQuery() {
|
|
345
|
+
const label = queryTabs.nextLabel()
|
|
346
|
+
const tab = tabs.open(
|
|
347
|
+
{ kind: 'query', label, key: label },
|
|
348
|
+
true,
|
|
349
|
+
)
|
|
350
|
+
queryTabs.ensure(tab.id)
|
|
351
|
+
menuOpen = false
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
onMount(() => {
|
|
355
|
+
const onNewQuery = () => openNewQuery()
|
|
356
|
+
window.addEventListener('red:new-query', onNewQuery)
|
|
357
|
+
return () => window.removeEventListener('red:new-query', onNewQuery)
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
function pickRenderer(tab: Tab, result: QueryResult) {
|
|
361
|
+
const cap = subpageCapability(tab.subpage, tab.capability ?? (result.capability as Capability | undefined))
|
|
362
|
+
return registry.pick(cap, result, tab.overrideCapability)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function openLiveChanges() {
|
|
366
|
+
tabs.open({ kind: 'live-changes', label: 'live · global', key: 'global' })
|
|
367
|
+
menuOpen = false
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function onOverrideChange(tab: Tab, e: Event) {
|
|
371
|
+
const value = (e.currentTarget as HTMLSelectElement).value as '' | Capability
|
|
372
|
+
tabs.setOverride(tab.id, value === '' ? undefined : value)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function defaultCapabilityLabel(tab: Tab, result: QueryResult | null | undefined): string {
|
|
376
|
+
if (tab.subpage) return tab.subpage
|
|
377
|
+
if (tab.capability) return tab.capability
|
|
378
|
+
if (result?.capability) return result.capability
|
|
379
|
+
return 'auto'
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function actionAllowed(v: NonNullable<CollectionMetadata['actions']>[string]): { allowed: boolean; reason?: string } {
|
|
383
|
+
if (typeof v === 'boolean') return { allowed: v }
|
|
384
|
+
return { allowed: v.allowed, reason: v.reason }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function objectKeys(v: unknown): string[] {
|
|
388
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? Object.keys(v as Record<string, unknown>) : []
|
|
389
|
+
}
|
|
390
|
+
</script>
|
|
391
|
+
|
|
392
|
+
<div class="h-full flex flex-col">
|
|
393
|
+
<div class="border-b border-line-1 flex items-stretch min-h-[36px] bg-bg-0">
|
|
394
|
+
{#if tabs.tabs.length === 0}
|
|
395
|
+
<div class="px-3 py-2 flex items-center gap-2 text-fg-3">
|
|
396
|
+
<Activity class="size-3.5" />
|
|
397
|
+
<span class="type-label">Results</span>
|
|
398
|
+
</div>
|
|
399
|
+
{:else}
|
|
400
|
+
<div class="flex items-stretch overflow-x-auto">
|
|
401
|
+
{#each tabs.tabs as tab (tab.id)}
|
|
402
|
+
{@const isActive = tab.id === tabs.activeId}
|
|
403
|
+
{@const cls = [
|
|
404
|
+
'group flex items-center gap-2 px-3 py-1.5 text-[12px] font-mono border-r border-line-1 whitespace-nowrap no-underline',
|
|
405
|
+
isActive
|
|
406
|
+
? 'bg-bg-1 text-fg-1 border-b-2 border-b-accent -mb-px'
|
|
407
|
+
: 'text-fg-3 hover:text-fg-1 hover:bg-bg-1/40',
|
|
408
|
+
].join(' ')}
|
|
409
|
+
{#if tab.kind === 'collection'}
|
|
410
|
+
{@const tabTarget = { view: 'collection' as const, collection: tab.key, subpage: tab.subpage ?? defaultSubpage(tab.capability) }}
|
|
411
|
+
<a href={router.href(tabTarget)} onclick={(e) => router.go(tabTarget, e)} class={cls} title={tab.label}>
|
|
412
|
+
<span class="truncate max-w-[180px]">{tab.label}</span>
|
|
413
|
+
<span
|
|
414
|
+
class="opacity-60 group-hover:opacity-100 hover:bg-bg-2 rounded p-0.5"
|
|
415
|
+
role="button"
|
|
416
|
+
tabindex="-1"
|
|
417
|
+
aria-label="Close tab"
|
|
418
|
+
onclick={(e) => closeTab(e as unknown as MouseEvent, tab.id)}
|
|
419
|
+
onkeydown={(e) => { if (e.key === 'Enter') closeTab(e as unknown as MouseEvent, tab.id) }}
|
|
420
|
+
>
|
|
421
|
+
<X class="size-3" />
|
|
422
|
+
</span>
|
|
423
|
+
</a>
|
|
424
|
+
{:else}
|
|
425
|
+
<button type="button" class={cls} onclick={() => tabs.focus(tab.id)} title={tab.label}>
|
|
426
|
+
<span class="truncate max-w-[180px]">{tab.label}</span>
|
|
427
|
+
<span
|
|
428
|
+
class="opacity-60 group-hover:opacity-100 hover:bg-bg-2 rounded p-0.5"
|
|
429
|
+
role="button"
|
|
430
|
+
tabindex="-1"
|
|
431
|
+
aria-label="Close tab"
|
|
432
|
+
onclick={(e) => closeTab(e as unknown as MouseEvent, tab.id)}
|
|
433
|
+
onkeydown={(e) => { if (e.key === 'Enter') closeTab(e as unknown as MouseEvent, tab.id) }}
|
|
434
|
+
>
|
|
435
|
+
<X class="size-3" />
|
|
436
|
+
</span>
|
|
437
|
+
</button>
|
|
438
|
+
{/if}
|
|
439
|
+
{/each}
|
|
440
|
+
</div>
|
|
441
|
+
{/if}
|
|
442
|
+
|
|
443
|
+
{#if tabs.active && (tabs.active.kind === 'collection' || tabs.active.kind === 'query')}
|
|
444
|
+
{@const activeTab = tabs.active}
|
|
445
|
+
{@const activeResult = results[activeTab.id]}
|
|
446
|
+
<div class="ml-auto flex items-center gap-2 px-2 text-[11px] font-mono text-fg-3">
|
|
447
|
+
{#if activeTab.kind === 'collection'}
|
|
448
|
+
<button
|
|
449
|
+
type="button"
|
|
450
|
+
onclick={() => (metadataOpen = !metadataOpen)}
|
|
451
|
+
title={metadataOpen ? 'Hide collection metadata' : 'Show collection metadata'}
|
|
452
|
+
aria-pressed={metadataOpen}
|
|
453
|
+
class={[
|
|
454
|
+
'inline-flex items-center gap-1 h-6 px-1.5 rounded border transition-colors cursor-pointer',
|
|
455
|
+
metadataOpen
|
|
456
|
+
? 'border-accent/40 bg-accent/10 text-accent'
|
|
457
|
+
: 'border-line-1 text-fg-3 hover:text-fg-1 hover:border-line-2',
|
|
458
|
+
].join(' ')}
|
|
459
|
+
>
|
|
460
|
+
<Info class="size-3" />
|
|
461
|
+
<span>meta</span>
|
|
462
|
+
</button>
|
|
463
|
+
{#if connection.capabilities.vcs}
|
|
464
|
+
<!-- VCS history is server-gated (#22): absent when the connected
|
|
465
|
+
server doesn't support version control, not shown-but-broken. -->
|
|
466
|
+
<button
|
|
467
|
+
type="button"
|
|
468
|
+
onclick={() => (historyOpen = { ...historyOpen, [activeTab.id]: !(historyOpen[activeTab.id] ?? false) })}
|
|
469
|
+
title={historyOpen[activeTab.id] ? 'Hide version history' : 'Show version history and diffs'}
|
|
470
|
+
aria-pressed={historyOpen[activeTab.id] ?? false}
|
|
471
|
+
class={[
|
|
472
|
+
'inline-flex items-center gap-1 h-6 px-1.5 rounded border transition-colors cursor-pointer',
|
|
473
|
+
historyOpen[activeTab.id]
|
|
474
|
+
? 'border-accent/40 bg-accent/10 text-accent'
|
|
475
|
+
: 'border-line-1 text-fg-3 hover:text-fg-1 hover:border-line-2',
|
|
476
|
+
].join(' ')}
|
|
477
|
+
>
|
|
478
|
+
<GitBranch class="size-3" />
|
|
479
|
+
<span>history</span>
|
|
480
|
+
</button>
|
|
481
|
+
{/if}
|
|
482
|
+
{/if}
|
|
483
|
+
<button
|
|
484
|
+
type="button"
|
|
485
|
+
onclick={() => tabs.setShowSystem(activeTab.id, !activeTab.showSystemColumns)}
|
|
486
|
+
title={activeTab.showSystemColumns ? 'Hide reddb system columns (rid, collection, kind, tenant, created_at, updated_at)' : 'Show reddb system columns'}
|
|
487
|
+
aria-pressed={activeTab.showSystemColumns ?? false}
|
|
488
|
+
class={[
|
|
489
|
+
'inline-flex items-center gap-1 h-6 px-1.5 rounded border transition-colors cursor-pointer',
|
|
490
|
+
activeTab.showSystemColumns
|
|
491
|
+
? 'border-accent/40 bg-accent/10 text-accent'
|
|
492
|
+
: 'border-line-1 text-fg-3 hover:text-fg-1 hover:border-line-2',
|
|
493
|
+
].join(' ')}
|
|
494
|
+
>
|
|
495
|
+
{#if activeTab.showSystemColumns}
|
|
496
|
+
<Eye class="size-3" />
|
|
497
|
+
{:else}
|
|
498
|
+
<EyeOff class="size-3" />
|
|
499
|
+
{/if}
|
|
500
|
+
<span>sys</span>
|
|
501
|
+
</button>
|
|
502
|
+
<span>{defaultCapabilityLabel(activeTab, activeResult)}</span>
|
|
503
|
+
<span class="text-fg-3">· override:</span>
|
|
504
|
+
<select
|
|
505
|
+
class="bg-bg-1 text-fg-1 border border-line-1 rounded px-1 py-0.5"
|
|
506
|
+
aria-label="Renderer override"
|
|
507
|
+
value={activeTab.overrideCapability ?? ''}
|
|
508
|
+
onchange={(e) => onOverrideChange(activeTab, e)}
|
|
509
|
+
>
|
|
510
|
+
{#each OVERRIDE_CHOICES as choice (choice.value)}
|
|
511
|
+
<option value={choice.value}>{choice.label}</option>
|
|
512
|
+
{/each}
|
|
513
|
+
</select>
|
|
514
|
+
</div>
|
|
515
|
+
{/if}
|
|
516
|
+
|
|
517
|
+
<div class="relative {tabs.active && (tabs.active.kind === 'collection' || tabs.active.kind === 'query') ? '' : 'ml-auto'} flex items-stretch">
|
|
518
|
+
<button
|
|
519
|
+
type="button"
|
|
520
|
+
class="px-2 inline-flex items-center text-fg-3 hover:text-fg-1"
|
|
521
|
+
aria-label="Open tab menu"
|
|
522
|
+
onclick={() => (menuOpen = !menuOpen)}
|
|
523
|
+
>
|
|
524
|
+
<Plus class="size-3.5" />
|
|
525
|
+
</button>
|
|
526
|
+
{#if menuOpen}
|
|
527
|
+
<div
|
|
528
|
+
role="menu"
|
|
529
|
+
class="absolute right-0 top-full mt-1 z-10 min-w-[220px] rounded border border-line-1 bg-bg-1 shadow-lg p-1 text-[12px] font-mono"
|
|
530
|
+
>
|
|
531
|
+
<button
|
|
532
|
+
type="button"
|
|
533
|
+
class="w-full text-left px-2 py-1 rounded hover:bg-bg-2 text-fg-1 flex items-center gap-2"
|
|
534
|
+
onclick={openNewQuery}
|
|
535
|
+
>
|
|
536
|
+
<FileCode2 class="size-3.5 text-fg-3" />
|
|
537
|
+
<span class="flex-1">New query</span>
|
|
538
|
+
<span class="text-fg-3 text-[10px]">⌘T</span>
|
|
539
|
+
</button>
|
|
540
|
+
<button
|
|
541
|
+
type="button"
|
|
542
|
+
class="w-full text-left px-2 py-1 rounded hover:bg-bg-2 text-fg-1"
|
|
543
|
+
onclick={openLiveChanges}
|
|
544
|
+
>
|
|
545
|
+
Live changes (global)
|
|
546
|
+
</button>
|
|
547
|
+
</div>
|
|
548
|
+
{/if}
|
|
549
|
+
</div>
|
|
550
|
+
</div>
|
|
551
|
+
|
|
552
|
+
<div class="flex-1 overflow-hidden bg-bg-0">
|
|
553
|
+
{#if !tabs.active}
|
|
554
|
+
<div class="h-full grid place-items-center p-6">
|
|
555
|
+
{#if connection.connected}
|
|
556
|
+
<div class="text-center text-fg-3 text-[12px] font-mono">
|
|
557
|
+
Connected to <span class="text-fg-1">{connection.active.url}</span>.
|
|
558
|
+
<br />
|
|
559
|
+
Pick a collection from the left to open a tab.
|
|
560
|
+
</div>
|
|
561
|
+
{:else}
|
|
562
|
+
<div class="text-center max-w-md">
|
|
563
|
+
<Database class="size-8 text-fg-3 mx-auto mb-3" strokeWidth={1.4} />
|
|
564
|
+
<h2 class="type-h2 m-0 mb-2">Workspace</h2>
|
|
565
|
+
<p class="text-fg-2 text-[13px] m-0">
|
|
566
|
+
Connect to a reddb instance from the topbar to start browsing
|
|
567
|
+
collections and running queries.
|
|
568
|
+
</p>
|
|
569
|
+
</div>
|
|
570
|
+
{/if}
|
|
571
|
+
</div>
|
|
572
|
+
{:else}
|
|
573
|
+
{@const tab = tabs.active}
|
|
574
|
+
{#if tab.kind === 'live-changes'}
|
|
575
|
+
<LiveChanges collection={undefined} />
|
|
576
|
+
{:else if tab.kind === 'query'}
|
|
577
|
+
<QueryEditor tabId={tab.id} fallbackLabel={tab.key} />
|
|
578
|
+
{:else if tab.kind === 'welcome'}
|
|
579
|
+
<div class="h-full grid place-items-center text-fg-3 text-[12px] font-mono">
|
|
580
|
+
Workspace
|
|
581
|
+
</div>
|
|
582
|
+
{:else}
|
|
583
|
+
{@const result = results[tab.id]}
|
|
584
|
+
{@const err = errors[tab.id]}
|
|
585
|
+
{#if err}
|
|
586
|
+
<div class="p-4 text-[12px] font-mono text-warn">
|
|
587
|
+
<div class="font-semibold mb-1">Query failed</div>
|
|
588
|
+
<pre class="whitespace-pre-wrap">{err}</pre>
|
|
589
|
+
</div>
|
|
590
|
+
{:else if !result}
|
|
591
|
+
<div class="h-full grid place-items-center text-fg-3 text-[12px] font-mono">
|
|
592
|
+
Loading {tab.label}…
|
|
593
|
+
</div>
|
|
594
|
+
{:else}
|
|
595
|
+
{@const renderer = pickRenderer(tab, result)}
|
|
596
|
+
{@const Renderer = renderer.component}
|
|
597
|
+
{@const catalog = catalogMetadata[tab.id]}
|
|
598
|
+
{@const catalogErr = catalogErrors[tab.id]}
|
|
599
|
+
<div class="h-full flex flex-col">
|
|
600
|
+
<div class="border-b border-line-1 bg-bg-1/35 px-3 py-2">
|
|
601
|
+
{#if catalogErr}
|
|
602
|
+
<div class="flex items-center gap-2 text-[11px] font-mono text-fg-3">
|
|
603
|
+
<Info class="size-3.5 text-warn" />
|
|
604
|
+
<span>Catalog metadata unavailable.</span>
|
|
605
|
+
<span class="truncate">{catalogErr}</span>
|
|
606
|
+
</div>
|
|
607
|
+
{:else if !catalog}
|
|
608
|
+
<div class="flex items-center gap-2 text-[11px] font-mono text-fg-3">
|
|
609
|
+
<Database class="size-3.5" />
|
|
610
|
+
<span>Loading collection catalog…</span>
|
|
611
|
+
</div>
|
|
612
|
+
{:else}
|
|
613
|
+
<div class="flex flex-wrap items-center gap-x-4 gap-y-2">
|
|
614
|
+
<div class="min-w-[180px] flex-1">
|
|
615
|
+
<div class="flex items-center gap-2">
|
|
616
|
+
<span class="rounded border border-accent/30 bg-accent/10 px-1.5 py-0.5 font-mono text-[10px] uppercase tracking-[0.08em] text-accent">
|
|
617
|
+
{catalog.model ?? 'collection'}
|
|
618
|
+
</span>
|
|
619
|
+
<span class="truncate font-mono text-[12px] text-fg-0">{catalog.name}</span>
|
|
620
|
+
</div>
|
|
621
|
+
<div class="mt-1 flex flex-wrap gap-1">
|
|
622
|
+
{#each collectionCatalogBadges(catalog) as badge}
|
|
623
|
+
<span class="rounded border border-line-1 bg-bg-0/70 px-1.5 py-0.5 font-mono text-[10px] text-fg-2">{badge}</span>
|
|
624
|
+
{/each}
|
|
625
|
+
{#if collectionCatalogBadges(catalog).length === 0}
|
|
626
|
+
<span class="font-mono text-[10px] text-fg-3">No type-specific catalog fields.</span>
|
|
627
|
+
{/if}
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
|
|
631
|
+
<div class="flex flex-wrap items-center gap-2 font-mono text-[11px]">
|
|
632
|
+
<div class="inline-flex min-w-[94px] items-center gap-1.5 rounded border border-line-1 bg-bg-0/70 px-2 py-1">
|
|
633
|
+
<Rows3 class="size-3.5 text-fg-3" />
|
|
634
|
+
<span class="text-fg-0">{formatCount(catalog.entities)}</span>
|
|
635
|
+
<span class="text-fg-3">entities</span>
|
|
636
|
+
</div>
|
|
637
|
+
<div class="inline-flex min-w-[92px] items-center gap-1.5 rounded border border-line-1 bg-bg-0/70 px-2 py-1">
|
|
638
|
+
<HardDrive class="size-3.5 text-fg-3" />
|
|
639
|
+
<span class="text-fg-0">{formatBytes(catalog.onDiskBytes)}</span>
|
|
640
|
+
<span class="text-fg-3">disk</span>
|
|
641
|
+
</div>
|
|
642
|
+
<div class="inline-flex min-w-[92px] items-center gap-1.5 rounded border border-line-1 bg-bg-0/70 px-2 py-1">
|
|
643
|
+
<Activity class="size-3.5 text-fg-3" />
|
|
644
|
+
<span class="text-fg-0">{formatBytes(catalog.inMemoryBytes)}</span>
|
|
645
|
+
<span class="text-fg-3">mem</span>
|
|
646
|
+
</div>
|
|
647
|
+
<div class="inline-flex min-w-[78px] items-center gap-1.5 rounded border border-line-1 bg-bg-0/70 px-2 py-1">
|
|
648
|
+
<Layers3 class="size-3.5 text-fg-3" />
|
|
649
|
+
<span class="text-fg-0">{formatCount(catalog.segments)}</span>
|
|
650
|
+
<span class="text-fg-3">seg</span>
|
|
651
|
+
</div>
|
|
652
|
+
<div class="inline-flex min-w-[78px] items-center gap-1.5 rounded border border-line-1 bg-bg-0/70 px-2 py-1">
|
|
653
|
+
<ScanLine class="size-3.5 text-fg-3" />
|
|
654
|
+
<span class="text-fg-0">{formatCount(catalog.indices)}</span>
|
|
655
|
+
<span class="text-fg-3">idx</span>
|
|
656
|
+
</div>
|
|
657
|
+
</div>
|
|
658
|
+
</div>
|
|
659
|
+
{/if}
|
|
660
|
+
</div>
|
|
661
|
+
|
|
662
|
+
{#if metadataOpen}
|
|
663
|
+
{@const meta = metadata[tab.id]}
|
|
664
|
+
{@const metaErr = metadataErrors[tab.id]}
|
|
665
|
+
<div class="border-b border-line-1 bg-bg-1/30 px-3 py-2 text-[11px] font-mono">
|
|
666
|
+
{#if metaErr}
|
|
667
|
+
<div class="flex items-start gap-2 text-fg-3">
|
|
668
|
+
<Info class="mt-0.5 size-3.5 text-warn" />
|
|
669
|
+
<div>
|
|
670
|
+
<div class="text-warn">Collection metadata unavailable.</div>
|
|
671
|
+
<div class="mt-0.5 break-words">{metaErr}</div>
|
|
672
|
+
</div>
|
|
673
|
+
</div>
|
|
674
|
+
{:else if !meta}
|
|
675
|
+
<div class="text-fg-3">Loading metadata…</div>
|
|
676
|
+
{:else}
|
|
677
|
+
<div class="grid grid-cols-[minmax(160px,0.8fr)_1.2fr] gap-3">
|
|
678
|
+
<div class="grid grid-cols-[78px_1fr] gap-x-2 gap-y-1">
|
|
679
|
+
<span class="text-fg-3">name</span>
|
|
680
|
+
<span class="text-fg-0 truncate">{meta.name ?? tab.key}</span>
|
|
681
|
+
<span class="text-fg-3">kind</span>
|
|
682
|
+
<span class="text-accent">{meta.kind ?? meta.capability ?? tab.capability ?? 'unknown'}</span>
|
|
683
|
+
<span class="text-fg-3">tenant</span>
|
|
684
|
+
<span class="text-fg-1">{meta.tenant ?? 'not exposed'}</span>
|
|
685
|
+
<span class="text-fg-3">indexes</span>
|
|
686
|
+
<span class="text-fg-1">{meta.indexes?.length ?? 0}</span>
|
|
687
|
+
</div>
|
|
688
|
+
|
|
689
|
+
<div class="grid gap-1">
|
|
690
|
+
<div class="flex flex-wrap gap-1">
|
|
691
|
+
{#if meta.capability}
|
|
692
|
+
<span class="rounded border border-accent/30 bg-accent/10 px-1.5 py-0.5 text-accent">{meta.capability}</span>
|
|
693
|
+
{/if}
|
|
694
|
+
{#each meta.capabilities ?? [] as cap}
|
|
695
|
+
<span class="rounded border border-line-1 px-1.5 py-0.5 text-fg-2">{cap}</span>
|
|
696
|
+
{/each}
|
|
697
|
+
{#if objectKeys(meta.schema).length > 0}
|
|
698
|
+
<span class="rounded border border-line-1 px-1.5 py-0.5 text-fg-2">schema {objectKeys(meta.schema).length}</span>
|
|
699
|
+
{/if}
|
|
700
|
+
{#if objectKeys(meta.retention).length > 0}
|
|
701
|
+
<span class="rounded border border-line-1 px-1.5 py-0.5 text-fg-2">retention</span>
|
|
702
|
+
{/if}
|
|
703
|
+
</div>
|
|
704
|
+
|
|
705
|
+
{#if meta.actions && objectKeys(meta.actions).length > 0}
|
|
706
|
+
<div class="mt-1 flex flex-wrap gap-1">
|
|
707
|
+
{#each Object.entries(meta.actions) as [action, value]}
|
|
708
|
+
{@const state = actionAllowed(value)}
|
|
709
|
+
<span
|
|
710
|
+
class={[
|
|
711
|
+
'rounded border px-1.5 py-0.5',
|
|
712
|
+
state.allowed ? 'border-ok/30 bg-ok/10 text-ok' : 'border-danger/30 bg-danger/10 text-danger',
|
|
713
|
+
].join(' ')}
|
|
714
|
+
title={state.reason}
|
|
715
|
+
>
|
|
716
|
+
{action}: {state.allowed ? 'allow' : 'deny'}
|
|
717
|
+
</span>
|
|
718
|
+
{/each}
|
|
719
|
+
</div>
|
|
720
|
+
{:else}
|
|
721
|
+
<div class="mt-1 text-fg-3">Action grants not exposed.</div>
|
|
722
|
+
{/if}
|
|
723
|
+
</div>
|
|
724
|
+
</div>
|
|
725
|
+
{/if}
|
|
726
|
+
</div>
|
|
727
|
+
{/if}
|
|
728
|
+
{#if historyOpen[tab.id]}
|
|
729
|
+
<div class="h-[320px] min-h-[240px] border-b border-line-1">
|
|
730
|
+
<CollectionHistory client={connection.client ?? undefined} collection={tab.key} />
|
|
731
|
+
</div>
|
|
732
|
+
{/if}
|
|
733
|
+
<div class="flex-1 min-h-0">
|
|
734
|
+
<Renderer
|
|
735
|
+
{result}
|
|
736
|
+
collection={tab.kind === 'collection' ? tab.key : undefined}
|
|
737
|
+
client={connection.client ?? undefined}
|
|
738
|
+
subpage={tab.kind === 'collection' ? tab.subpage : undefined}
|
|
739
|
+
showSystem={tab.showSystemColumns ?? false}
|
|
740
|
+
/>
|
|
741
|
+
</div>
|
|
742
|
+
</div>
|
|
743
|
+
{/if}
|
|
744
|
+
{/if}
|
|
745
|
+
{/if}
|
|
746
|
+
</div>
|
|
747
|
+
</div>
|