@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,253 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { QueryResult } from '#reddb'
|
|
3
|
+
import { Activity, BarChart3, LineChart, X } from 'lucide-svelte'
|
|
4
|
+
import {
|
|
5
|
+
ALL_METRICS,
|
|
6
|
+
bucketGeometry,
|
|
7
|
+
bucketSeries,
|
|
8
|
+
chartGeometry,
|
|
9
|
+
extractSeries,
|
|
10
|
+
type BucketPoint,
|
|
11
|
+
} from './hypertable-render'
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
result: QueryResult
|
|
15
|
+
collection?: string
|
|
16
|
+
showSystem?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let { result, collection }: Props = $props()
|
|
20
|
+
|
|
21
|
+
let metric = $state<string | undefined>(ALL_METRICS)
|
|
22
|
+
let chartMode = $state<'line' | 'rate'>('rate')
|
|
23
|
+
let bucketMs = $state(60_000)
|
|
24
|
+
let selectedBucket = $state<BucketPoint | null>(null)
|
|
25
|
+
|
|
26
|
+
const series = $derived(extractSeries(result, metric))
|
|
27
|
+
const lineGeom = $derived(series && chartMode === 'line' ? chartGeometry(series) : null)
|
|
28
|
+
const buckets = $derived(series ? bucketSeries(series, bucketMs) : [])
|
|
29
|
+
const barGeom = $derived(series && chartMode === 'rate' ? bucketGeometry(buckets) : null)
|
|
30
|
+
|
|
31
|
+
// ─── filter rows by selected bucket ─────────────────────────────────────
|
|
32
|
+
const visiblePoints = $derived.by(() => {
|
|
33
|
+
if (!series) return []
|
|
34
|
+
if (!selectedBucket) return series.points
|
|
35
|
+
const t0 = selectedBucket.t
|
|
36
|
+
const t1 = t0 + bucketMs
|
|
37
|
+
return series.points.filter((p) => p.t >= t0 && p.t < t1)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
function fmtT(t: number): string {
|
|
41
|
+
try {
|
|
42
|
+
return new Date(t).toISOString().replace('T', ' ').slice(0, 19)
|
|
43
|
+
} catch {
|
|
44
|
+
return String(t)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function renderLogValue(v: unknown): string {
|
|
49
|
+
if (v === null || v === undefined) return ''
|
|
50
|
+
if (typeof v === 'object') return JSON.stringify(v)
|
|
51
|
+
return String(v)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function logPayload(p: { raw: Record<string, unknown> }): string {
|
|
55
|
+
const parts: string[] = []
|
|
56
|
+
const metricName = series?.metricNameColumn ? p.raw[series.metricNameColumn] : undefined
|
|
57
|
+
if (metricName !== undefined) parts.push(String(metricName))
|
|
58
|
+
const tags = p.raw.tags
|
|
59
|
+
if (tags && typeof tags === 'object') {
|
|
60
|
+
const tagText = Object.entries(tags as Record<string, unknown>)
|
|
61
|
+
.map(([k, v]) => `${k}=${renderLogValue(v)}`)
|
|
62
|
+
.join(' ')
|
|
63
|
+
if (tagText) parts.push(tagText)
|
|
64
|
+
}
|
|
65
|
+
const event = p.raw.event ?? p.raw.action ?? p.raw.message ?? p.raw.payload
|
|
66
|
+
if (event !== undefined) parts.push(renderLogValue(event))
|
|
67
|
+
return parts.join(' · ')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function bucketLabel(opt: number): string {
|
|
71
|
+
if (opt === 1000) return '1s'
|
|
72
|
+
if (opt === 10_000) return '10s'
|
|
73
|
+
if (opt === 60_000) return '1min'
|
|
74
|
+
if (opt === 300_000) return '5min'
|
|
75
|
+
if (opt === 600_000) return '10min'
|
|
76
|
+
if (opt === 3_600_000) return '1hr'
|
|
77
|
+
return `${opt}ms`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function metricLabel(m: string): string {
|
|
81
|
+
return m === ALL_METRICS ? 'all writes' : m
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function activeMetricLabel(): string {
|
|
85
|
+
if (!series) return ''
|
|
86
|
+
if (!metric || metric === ALL_METRICS) return 'all writes'
|
|
87
|
+
return metric
|
|
88
|
+
}
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<div class="flex h-full flex-col text-fg-1">
|
|
92
|
+
{#if !series}
|
|
93
|
+
<div class="flex-1 grid place-items-center text-fg-3 text-[12px] font-mono p-6">
|
|
94
|
+
No timeseries shape (need a time column and a numeric metric).
|
|
95
|
+
</div>
|
|
96
|
+
{:else}
|
|
97
|
+
<!-- Toolbar -->
|
|
98
|
+
<div class="border-b border-line-1 px-3 py-1.5 flex items-center gap-2 text-[11px] font-mono bg-bg-1/40">
|
|
99
|
+
<div class="inline-flex border border-line-2 rounded overflow-hidden">
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
class={[
|
|
103
|
+
'inline-flex items-center gap-1 px-2 py-1 cursor-pointer transition-colors',
|
|
104
|
+
chartMode === 'rate' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0',
|
|
105
|
+
].join(' ')}
|
|
106
|
+
onclick={() => (chartMode = 'rate')}
|
|
107
|
+
aria-pressed={chartMode === 'rate'}
|
|
108
|
+
>
|
|
109
|
+
<BarChart3 class="size-3" />
|
|
110
|
+
Writes/{bucketLabel(bucketMs)}
|
|
111
|
+
</button>
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
class={[
|
|
115
|
+
'inline-flex items-center gap-1 px-2 py-1 cursor-pointer transition-colors border-l border-line-2',
|
|
116
|
+
chartMode === 'line' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0',
|
|
117
|
+
].join(' ')}
|
|
118
|
+
onclick={() => (chartMode = 'line')}
|
|
119
|
+
aria-pressed={chartMode === 'line'}
|
|
120
|
+
>
|
|
121
|
+
<LineChart class="size-3" />
|
|
122
|
+
Value
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<label class="inline-flex items-center gap-1.5 text-fg-3">
|
|
127
|
+
<Activity class="size-3" />
|
|
128
|
+
metric:
|
|
129
|
+
<select
|
|
130
|
+
class="bg-bg-1 text-fg-1 border border-line-1 rounded px-1.5 py-0.5"
|
|
131
|
+
bind:value={metric}
|
|
132
|
+
>
|
|
133
|
+
{#each series.metrics as m (m)}
|
|
134
|
+
<option value={m}>{metricLabel(m)}</option>
|
|
135
|
+
{/each}
|
|
136
|
+
</select>
|
|
137
|
+
</label>
|
|
138
|
+
|
|
139
|
+
{#if chartMode === 'rate'}
|
|
140
|
+
<label class="inline-flex items-center gap-1.5 text-fg-3">
|
|
141
|
+
bucket:
|
|
142
|
+
<select
|
|
143
|
+
class="bg-bg-1 text-fg-1 border border-line-1 rounded px-1.5 py-0.5"
|
|
144
|
+
value={bucketMs}
|
|
145
|
+
onchange={(e) => { bucketMs = Number((e.currentTarget as HTMLSelectElement).value); selectedBucket = null }}
|
|
146
|
+
>
|
|
147
|
+
<option value={1000}>1s</option>
|
|
148
|
+
<option value={10_000}>10s</option>
|
|
149
|
+
<option value={60_000}>1min</option>
|
|
150
|
+
<option value={300_000}>5min</option>
|
|
151
|
+
<option value={600_000}>10min</option>
|
|
152
|
+
<option value={3_600_000}>1hr</option>
|
|
153
|
+
</select>
|
|
154
|
+
</label>
|
|
155
|
+
{/if}
|
|
156
|
+
|
|
157
|
+
{#if selectedBucket}
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
onclick={() => (selectedBucket = null)}
|
|
161
|
+
class="inline-flex items-center gap-1 px-2 py-1 rounded border border-line-1 text-fg-3 hover:text-fg-1 cursor-pointer"
|
|
162
|
+
title="Show all rows again"
|
|
163
|
+
>
|
|
164
|
+
<X class="size-3" />
|
|
165
|
+
clear bucket
|
|
166
|
+
</button>
|
|
167
|
+
{/if}
|
|
168
|
+
|
|
169
|
+
<div class="ml-auto text-fg-3">
|
|
170
|
+
<span class="text-fg-2">{activeMetricLabel()}</span>
|
|
171
|
+
· {visiblePoints.length.toLocaleString()}{#if selectedBucket} of {series.points.length.toLocaleString()}{/if} inserts
|
|
172
|
+
{#if collection}· <span class="text-fg-2">{collection}</span>{/if}
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<!-- Chart -->
|
|
177
|
+
<div class="px-3 pt-2">
|
|
178
|
+
{#if chartMode === 'rate' && barGeom}
|
|
179
|
+
<svg
|
|
180
|
+
viewBox={`0 0 ${barGeom.width} ${barGeom.height}`}
|
|
181
|
+
class="block w-full h-[180px]"
|
|
182
|
+
aria-label="Writes per bucket"
|
|
183
|
+
>
|
|
184
|
+
{#each barGeom.bars as bar (bar.bucket.t)}
|
|
185
|
+
<rect
|
|
186
|
+
x={bar.x}
|
|
187
|
+
y={bar.y}
|
|
188
|
+
width={bar.w}
|
|
189
|
+
height={bar.h}
|
|
190
|
+
class="cursor-pointer transition-opacity"
|
|
191
|
+
fill="var(--color-accent)"
|
|
192
|
+
opacity={!selectedBucket || selectedBucket.t === bar.bucket.t ? 0.9 : 0.3}
|
|
193
|
+
role="button"
|
|
194
|
+
tabindex="0"
|
|
195
|
+
aria-label="Bucket starting at {fmtT(bar.bucket.t)} with {bar.bucket.count} writes"
|
|
196
|
+
onclick={() => (selectedBucket = selectedBucket?.t === bar.bucket.t ? null : bar.bucket)}
|
|
197
|
+
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectedBucket = selectedBucket?.t === bar.bucket.t ? null : bar.bucket } }}
|
|
198
|
+
>
|
|
199
|
+
<title>{fmtT(bar.bucket.t)} — {bar.bucket.count} writes (sum {bar.bucket.sum.toFixed(2)})</title>
|
|
200
|
+
</rect>
|
|
201
|
+
{/each}
|
|
202
|
+
<text x={barGeom.padding} y={barGeom.height - 2} class="text-[9px] fill-fg-3 font-mono">
|
|
203
|
+
{fmtT(barGeom.bars[0]?.bucket.t ?? 0)}
|
|
204
|
+
</text>
|
|
205
|
+
<text x={barGeom.width - barGeom.padding} y={barGeom.height - 2} text-anchor="end" class="text-[9px] fill-fg-3 font-mono">
|
|
206
|
+
peak {barGeom.maxCount}/{bucketLabel(bucketMs)}
|
|
207
|
+
</text>
|
|
208
|
+
</svg>
|
|
209
|
+
{:else if lineGeom}
|
|
210
|
+
<svg
|
|
211
|
+
viewBox={`0 0 ${lineGeom.width} ${lineGeom.height}`}
|
|
212
|
+
class="block w-full h-[180px] text-accent"
|
|
213
|
+
aria-label="Timeseries chart"
|
|
214
|
+
>
|
|
215
|
+
<path d={lineGeom.path} fill="none" stroke="currentColor" stroke-width="1.5" />
|
|
216
|
+
</svg>
|
|
217
|
+
{/if}
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<!-- Event log -->
|
|
221
|
+
<div class="flex-1 min-h-0 overflow-auto">
|
|
222
|
+
<table class="w-full border-collapse text-[12px] font-mono">
|
|
223
|
+
<thead class="sticky top-0 bg-bg-1 z-10">
|
|
224
|
+
<tr class="border-b border-line-1 text-fg-3">
|
|
225
|
+
<th class="px-2 py-1.5 text-left font-normal">time</th>
|
|
226
|
+
{#if series.metricNameColumn}
|
|
227
|
+
<th class="px-2 py-1.5 text-left font-normal">metric</th>
|
|
228
|
+
{/if}
|
|
229
|
+
<th class="px-2 py-1.5 text-right font-normal">value</th>
|
|
230
|
+
<th class="px-2 py-1.5 text-left font-normal">log</th>
|
|
231
|
+
</tr>
|
|
232
|
+
</thead>
|
|
233
|
+
<tbody>
|
|
234
|
+
{#each visiblePoints as p, i (i)}
|
|
235
|
+
<tr class="border-b border-line-1/60 hover:bg-bg-1/40">
|
|
236
|
+
<td class="px-2 py-1">{fmtT(p.t)}</td>
|
|
237
|
+
{#if series.metricNameColumn}
|
|
238
|
+
<td class="px-2 py-1 text-fg-2">{String(p.raw[series.metricNameColumn] ?? '')}</td>
|
|
239
|
+
{/if}
|
|
240
|
+
<td class="px-2 py-1 text-right text-accent">{p.v}</td>
|
|
241
|
+
<td class="px-2 py-1 text-fg-2">{logPayload(p)}</td>
|
|
242
|
+
</tr>
|
|
243
|
+
{/each}
|
|
244
|
+
</tbody>
|
|
245
|
+
</table>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<div class="border-t border-line-1 px-3 py-1.5 text-[11px] font-mono text-fg-3">
|
|
249
|
+
{visiblePoints.length} inserts{collection ? ' · ' : ''}{collection ?? ''}
|
|
250
|
+
{#if selectedBucket}· bucket {fmtT(selectedBucket.t)} (+{bucketLabel(bucketMs)}){/if}
|
|
251
|
+
</div>
|
|
252
|
+
{/if}
|
|
253
|
+
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { QueryResult } from '#reddb'
|
|
3
|
+
import { highlightJson, prettyJson, projectJson } from './json-render'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
result: QueryResult
|
|
7
|
+
collection?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { result, collection }: Props = $props()
|
|
11
|
+
const pretty = $derived(prettyJson(projectJson(result)))
|
|
12
|
+
const html = $derived(highlightJson(pretty))
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div class="flex h-full flex-col text-fg-1">
|
|
16
|
+
<div class="flex-1 min-h-0 overflow-auto">
|
|
17
|
+
<pre class="json text-[12px] font-mono p-3 leading-relaxed">{@html html}</pre>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="border-t border-line-1 px-3 py-1.5 text-[11px] font-mono text-fg-3">
|
|
20
|
+
{result.record_count} records · raw shape{collection ? ' · ' : ''}{collection ?? ''}
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<style>
|
|
25
|
+
.json :global(.k) { color: var(--color-accent); }
|
|
26
|
+
.json :global(.s) { color: var(--color-fg-1); }
|
|
27
|
+
.json :global(.n) { color: #7dd3fc; }
|
|
28
|
+
.json :global(.b) { color: #fbbf24; }
|
|
29
|
+
.json :global(.z) { color: var(--color-fg-3); font-style: italic; }
|
|
30
|
+
</style>
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { QueryResult } from '#reddb'
|
|
3
|
+
import { Braces, ListTree, Search, X } from 'lucide-svelte'
|
|
4
|
+
import { buildTree, extractKv, formatValue, materializeKvJson, type KvNode } from './kv-render'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
result: QueryResult
|
|
8
|
+
collection?: string
|
|
9
|
+
showSystem?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { result, collection }: Props = $props()
|
|
13
|
+
|
|
14
|
+
// ─── state ──────────────────────────────────────────────────────────────
|
|
15
|
+
let prefix = $state('')
|
|
16
|
+
let viewMode = $state<'tree' | 'json'>('tree')
|
|
17
|
+
let selected = $state<KvNode | null>(null)
|
|
18
|
+
|
|
19
|
+
// ─── data ───────────────────────────────────────────────────────────────
|
|
20
|
+
const entries = $derived(extractKv(result))
|
|
21
|
+
const filteredEntries = $derived(
|
|
22
|
+
prefix.trim() ? entries.filter((e) => e.key.startsWith(prefix.trim())) : entries,
|
|
23
|
+
)
|
|
24
|
+
const tree = $derived(buildTree(filteredEntries))
|
|
25
|
+
const jsonObject = $derived(materializeKvJson(entries, prefix.trim()))
|
|
26
|
+
|
|
27
|
+
function preview(v: unknown): string {
|
|
28
|
+
const s = formatValue(v)
|
|
29
|
+
return s.length > 60 ? s.slice(0, 59) + '…' : s
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<div class="flex h-full flex-col text-fg-1">
|
|
34
|
+
{#if entries.length === 0}
|
|
35
|
+
<div class="flex-1 grid place-items-center text-fg-3 text-[12px] font-mono p-6">
|
|
36
|
+
No KV shape — expected <code>key</code> and <code>value</code> columns
|
|
37
|
+
(also accepts <code>*_key</code>/<code>*_value</code>).
|
|
38
|
+
</div>
|
|
39
|
+
{:else}
|
|
40
|
+
<!-- Toolbar -->
|
|
41
|
+
<div class="border-b border-line-1 px-3 py-1.5 flex items-center gap-2 text-[11px] font-mono bg-bg-1/40">
|
|
42
|
+
<div class="inline-flex border border-line-2 rounded overflow-hidden">
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
class={[
|
|
46
|
+
'inline-flex items-center gap-1 px-2 py-1 cursor-pointer transition-colors',
|
|
47
|
+
viewMode === 'tree' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0',
|
|
48
|
+
].join(' ')}
|
|
49
|
+
onclick={() => (viewMode = 'tree')}
|
|
50
|
+
aria-pressed={viewMode === 'tree'}
|
|
51
|
+
>
|
|
52
|
+
<ListTree class="size-3" />
|
|
53
|
+
Tree
|
|
54
|
+
</button>
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
class={[
|
|
58
|
+
'inline-flex items-center gap-1 px-2 py-1 cursor-pointer transition-colors border-l border-line-2',
|
|
59
|
+
viewMode === 'json' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0',
|
|
60
|
+
].join(' ')}
|
|
61
|
+
onclick={() => (viewMode = 'json')}
|
|
62
|
+
aria-pressed={viewMode === 'json'}
|
|
63
|
+
>
|
|
64
|
+
<Braces class="size-3" />
|
|
65
|
+
JSON
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div class="inline-flex items-center gap-1.5 border border-line-1 rounded px-2 py-1 bg-bg-0">
|
|
70
|
+
<Search class="size-3 text-fg-3" />
|
|
71
|
+
<input
|
|
72
|
+
type="text"
|
|
73
|
+
bind:value={prefix}
|
|
74
|
+
placeholder="key prefix…"
|
|
75
|
+
class="bg-transparent text-fg-1 outline-none w-[220px] placeholder:text-fg-3"
|
|
76
|
+
/>
|
|
77
|
+
{#if prefix}
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
onclick={() => (prefix = '')}
|
|
81
|
+
class="text-fg-3 hover:text-fg-1 cursor-pointer"
|
|
82
|
+
aria-label="Clear prefix"
|
|
83
|
+
>
|
|
84
|
+
<X class="size-3" />
|
|
85
|
+
</button>
|
|
86
|
+
{/if}
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div class="ml-auto text-fg-3">
|
|
90
|
+
{filteredEntries.length.toLocaleString()}{#if prefix} of {entries.length.toLocaleString()}{/if} keys
|
|
91
|
+
{#if collection}· <span class="text-fg-2">{collection}</span>{/if}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<!-- Body -->
|
|
96
|
+
{#if viewMode === 'tree'}
|
|
97
|
+
<div class="flex-1 grid grid-cols-[280px_1fr] min-h-0">
|
|
98
|
+
<ul class="overflow-auto border-r border-line-1 text-[12px] font-mono py-1">
|
|
99
|
+
{#if tree.children.length === 0}
|
|
100
|
+
<li class="px-3 py-2 text-fg-3">No keys match prefix.</li>
|
|
101
|
+
{:else}
|
|
102
|
+
{#each tree.children as node (node.name)}
|
|
103
|
+
{@render renderNode(node, 0)}
|
|
104
|
+
{/each}
|
|
105
|
+
{/if}
|
|
106
|
+
</ul>
|
|
107
|
+
<div class="overflow-auto p-3 text-[12px] font-mono">
|
|
108
|
+
{#if selected}
|
|
109
|
+
<div class="text-fg-3 mb-2">{selected.fullKey ?? selected.name}</div>
|
|
110
|
+
<pre class="text-fg-1 whitespace-pre-wrap break-all">{formatValue(selected.value)}</pre>
|
|
111
|
+
{:else}
|
|
112
|
+
<div class="text-fg-3">Click a key to inspect.</div>
|
|
113
|
+
{/if}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
{:else}
|
|
117
|
+
<div class="flex-1 overflow-auto p-3 text-[12px] font-mono">
|
|
118
|
+
{#if Object.keys(jsonObject).length === 0}
|
|
119
|
+
<div class="text-fg-3">No keys match prefix.</div>
|
|
120
|
+
{:else}
|
|
121
|
+
<pre class="whitespace-pre text-fg-1">{JSON.stringify(jsonObject, null, 2)}</pre>
|
|
122
|
+
{/if}
|
|
123
|
+
</div>
|
|
124
|
+
{/if}
|
|
125
|
+
{/if}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{#snippet renderNode(node: KvNode, depth: number)}
|
|
129
|
+
<li>
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
class="w-full text-left px-2 py-0.5 hover:bg-bg-1/60"
|
|
133
|
+
class:text-accent={selected === node}
|
|
134
|
+
style:padding-left={`${8 + depth * 12}px`}
|
|
135
|
+
onclick={() => (selected = node)}
|
|
136
|
+
>
|
|
137
|
+
<span class="text-fg-1">{node.name}</span>
|
|
138
|
+
{#if node.fullKey !== null}
|
|
139
|
+
<span class="text-fg-3 ml-2">{preview(node.value)}</span>
|
|
140
|
+
{/if}
|
|
141
|
+
</button>
|
|
142
|
+
{#if node.children.length > 0}
|
|
143
|
+
<ul>
|
|
144
|
+
{#each node.children as child (child.name)}
|
|
145
|
+
{@render renderNode(child, depth + 1)}
|
|
146
|
+
{/each}
|
|
147
|
+
</ul>
|
|
148
|
+
{/if}
|
|
149
|
+
</li>
|
|
150
|
+
{/snippet}
|