@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,286 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { QueryResult } from '#reddb'
|
|
3
|
+
import { connection } from '$lib/connections.svelte'
|
|
4
|
+
import { activity } from '$lib/activity.svelte'
|
|
5
|
+
import { Braces, ClipboardList, Loader2, Plus, Radio, Search, Send, X } from 'lucide-svelte'
|
|
6
|
+
import {
|
|
7
|
+
extractQueueEvents,
|
|
8
|
+
extractQueueMessages,
|
|
9
|
+
queueCommandPlan,
|
|
10
|
+
queuePushQuery,
|
|
11
|
+
queueSummary,
|
|
12
|
+
type QueueMessage,
|
|
13
|
+
} from './queue-render'
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
result: QueryResult
|
|
17
|
+
collection?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let { result, collection }: Props = $props()
|
|
21
|
+
|
|
22
|
+
let mode = $state<'events' | 'messages' | 'commands' | 'json'>('events')
|
|
23
|
+
let filter = $state('')
|
|
24
|
+
let selected = $state<QueueMessage | null>(null)
|
|
25
|
+
let composerOpen = $state(false)
|
|
26
|
+
let payload = $state('')
|
|
27
|
+
let inserting = $state(false)
|
|
28
|
+
let insertError = $state<string | null>(null)
|
|
29
|
+
let inserted = $state(0)
|
|
30
|
+
const payloadPlaceholder = '{"type":"email","to":"ops@example.com"}'
|
|
31
|
+
|
|
32
|
+
const messages = $derived(extractQueueMessages(result))
|
|
33
|
+
const summary = $derived(queueSummary(messages))
|
|
34
|
+
const events = $derived(extractQueueEvents(messages))
|
|
35
|
+
const commands = $derived(collection ? queueCommandPlan(collection) : [])
|
|
36
|
+
const filtered = $derived.by(() => {
|
|
37
|
+
const q = filter.trim().toLowerCase()
|
|
38
|
+
if (!q) return messages
|
|
39
|
+
return messages.filter((msg) =>
|
|
40
|
+
msg.id.toLowerCase().includes(q) ||
|
|
41
|
+
msg.state.toLowerCase().includes(q) ||
|
|
42
|
+
JSON.stringify(msg.message).toLowerCase().includes(q),
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
function display(v: unknown): string {
|
|
47
|
+
if (typeof v === 'string') return v
|
|
48
|
+
return JSON.stringify(v)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fmtTs(v: number | null): string {
|
|
52
|
+
if (v === null) return '-'
|
|
53
|
+
try {
|
|
54
|
+
return new Date(v).toISOString().replace('T', ' ').slice(0, 19)
|
|
55
|
+
} catch {
|
|
56
|
+
return String(v)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parsePayload(): unknown {
|
|
61
|
+
const text = payload.trim()
|
|
62
|
+
if (!text) return ''
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(text)
|
|
65
|
+
} catch {
|
|
66
|
+
return text
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function insertMessage() {
|
|
71
|
+
const client = connection.client
|
|
72
|
+
if (!client || !collection || !payload.trim()) return
|
|
73
|
+
inserting = true
|
|
74
|
+
insertError = null
|
|
75
|
+
try {
|
|
76
|
+
await activity.track(`${collection} · enqueue message`, () =>
|
|
77
|
+
client.query(queuePushQuery(collection, parsePayload())),
|
|
78
|
+
)
|
|
79
|
+
payload = ''
|
|
80
|
+
inserted += 1
|
|
81
|
+
} catch (e) {
|
|
82
|
+
insertError = (e as Error).message
|
|
83
|
+
} finally {
|
|
84
|
+
inserting = false
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<div class="flex h-full flex-col text-fg-1">
|
|
90
|
+
<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">
|
|
91
|
+
<div class="inline-flex border border-line-2 rounded overflow-hidden">
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
class={['inline-flex items-center gap-1 px-2 py-1 cursor-pointer transition-colors', mode === 'events' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0'].join(' ')}
|
|
95
|
+
onclick={() => (mode = 'events')}
|
|
96
|
+
aria-pressed={mode === 'events'}
|
|
97
|
+
>
|
|
98
|
+
<Radio class="size-3" />
|
|
99
|
+
events
|
|
100
|
+
</button>
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
class={['inline-flex items-center gap-1 px-2 py-1 cursor-pointer transition-colors border-l border-line-2', mode === 'messages' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0'].join(' ')}
|
|
104
|
+
onclick={() => (mode = 'messages')}
|
|
105
|
+
aria-pressed={mode === 'messages'}
|
|
106
|
+
>
|
|
107
|
+
<Send class="size-3" />
|
|
108
|
+
messages
|
|
109
|
+
</button>
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
class={['inline-flex items-center gap-1 px-2 py-1 cursor-pointer transition-colors border-l border-line-2', mode === 'commands' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0'].join(' ')}
|
|
113
|
+
onclick={() => (mode = 'commands')}
|
|
114
|
+
aria-pressed={mode === 'commands'}
|
|
115
|
+
>
|
|
116
|
+
<ClipboardList class="size-3" />
|
|
117
|
+
commands
|
|
118
|
+
</button>
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
class={['inline-flex items-center gap-1 px-2 py-1 cursor-pointer transition-colors border-l border-line-2', mode === 'json' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0'].join(' ')}
|
|
122
|
+
onclick={() => (mode = 'json')}
|
|
123
|
+
aria-pressed={mode === 'json'}
|
|
124
|
+
>
|
|
125
|
+
<Braces class="size-3" />
|
|
126
|
+
json
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
onclick={() => (composerOpen = !composerOpen)}
|
|
133
|
+
disabled={!collection}
|
|
134
|
+
class={['inline-flex items-center gap-1 h-6 px-2 rounded border cursor-pointer transition-colors disabled:opacity-40 disabled:cursor-not-allowed', composerOpen ? 'border-accent/40 bg-accent/10 text-accent' : 'border-line-1 text-fg-3 hover:text-fg-1 hover:border-line-2'].join(' ')}
|
|
135
|
+
aria-pressed={composerOpen}
|
|
136
|
+
title={collection ? 'Insert a queue message' : 'Open this as a collection to insert messages'}
|
|
137
|
+
>
|
|
138
|
+
<Plus class="size-3" />
|
|
139
|
+
insert
|
|
140
|
+
</button>
|
|
141
|
+
|
|
142
|
+
<div class="inline-flex items-center gap-1.5 border border-line-1 rounded px-2 py-1 bg-bg-0">
|
|
143
|
+
<Search class="size-3 text-fg-3" />
|
|
144
|
+
<input type="text" bind:value={filter} placeholder="message/state…" class="bg-transparent text-fg-1 outline-none w-[180px] placeholder:text-fg-3" />
|
|
145
|
+
{#if filter}
|
|
146
|
+
<button type="button" onclick={() => (filter = '')} class="text-fg-3 hover:text-fg-1 cursor-pointer" aria-label="Clear queue filter">
|
|
147
|
+
<X class="size-3" />
|
|
148
|
+
</button>
|
|
149
|
+
{/if}
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div class="ml-auto flex items-center gap-3 text-fg-3">
|
|
153
|
+
<span>{filtered.length.toLocaleString()} messages</span>
|
|
154
|
+
{#if events.length}<span>{events.length.toLocaleString()} events</span>{/if}
|
|
155
|
+
<span>{summary.activeConsumers} active consumers</span>
|
|
156
|
+
{#if summary.sources.length}<span>from <span class="text-fg-2">{summary.sources.join(', ')}</span></span>{/if}
|
|
157
|
+
{#if summary.dlcs.length}<span>dlc <span class="text-fg-2">{summary.dlcs.join(', ')}</span></span>{/if}
|
|
158
|
+
{#if collection}<span class="text-fg-2">{collection}</span>{/if}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{#if composerOpen}
|
|
163
|
+
<div class="border-b border-line-1 bg-bg-1/20 px-3 py-2 text-[11px] font-mono">
|
|
164
|
+
<div class="flex gap-2">
|
|
165
|
+
<textarea
|
|
166
|
+
bind:value={payload}
|
|
167
|
+
rows="3"
|
|
168
|
+
placeholder={payloadPlaceholder}
|
|
169
|
+
class="flex-1 resize-none rounded border border-line-1 bg-bg-0 px-2 py-1 text-[12px] text-fg-1 outline-none placeholder:text-fg-3 focus:border-accent"
|
|
170
|
+
></textarea>
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
onclick={insertMessage}
|
|
174
|
+
disabled={inserting || !payload.trim() || !collection}
|
|
175
|
+
class="inline-flex h-7 items-center gap-1.5 rounded bg-accent px-3 text-white disabled:opacity-40 disabled:cursor-not-allowed"
|
|
176
|
+
>
|
|
177
|
+
{#if inserting}<Loader2 class="size-3 animate-spin" />{:else}<Send class="size-3" />{/if}
|
|
178
|
+
insert
|
|
179
|
+
</button>
|
|
180
|
+
</div>
|
|
181
|
+
{#if insertError}
|
|
182
|
+
<div class="mt-1.5 text-warn">{insertError}</div>
|
|
183
|
+
{:else if inserted > 0}
|
|
184
|
+
<div class="mt-1.5 text-ok">{inserted} inserted. Refresh the collection to reload from reddb.</div>
|
|
185
|
+
{/if}
|
|
186
|
+
</div>
|
|
187
|
+
{/if}
|
|
188
|
+
|
|
189
|
+
{#if mode === 'events'}
|
|
190
|
+
<div class="flex-1 min-h-0 overflow-auto">
|
|
191
|
+
{#if events.length === 0}
|
|
192
|
+
<div class="p-3 text-[12px] font-mono text-fg-3">No WITH EVENTS envelopes detected in this queue result.</div>
|
|
193
|
+
{:else}
|
|
194
|
+
<table class="w-full border-collapse text-[12px] font-mono">
|
|
195
|
+
<thead class="sticky top-0 bg-bg-1 z-10">
|
|
196
|
+
<tr class="border-b border-line-1 text-fg-3">
|
|
197
|
+
<th class="px-2 py-1.5 text-left font-normal">time</th>
|
|
198
|
+
<th class="px-2 py-1.5 text-left font-normal">source</th>
|
|
199
|
+
<th class="px-2 py-1.5 text-left font-normal">op</th>
|
|
200
|
+
<th class="px-2 py-1.5 text-left font-normal">entity</th>
|
|
201
|
+
<th class="px-2 py-1.5 text-right font-normal">lsn</th>
|
|
202
|
+
<th class="px-2 py-1.5 text-left font-normal">after</th>
|
|
203
|
+
</tr>
|
|
204
|
+
</thead>
|
|
205
|
+
<tbody>
|
|
206
|
+
{#each events as event (`${event.messageId}-${event.lsn ?? event.entityId}`)}
|
|
207
|
+
<tr class="border-b border-line-1/60 hover:bg-bg-1/40">
|
|
208
|
+
<td class="px-2 py-1 text-fg-2">{fmtTs(event.ts)}</td>
|
|
209
|
+
<td class="px-2 py-1 text-fg-0">{event.collection}</td>
|
|
210
|
+
<td class="px-2 py-1"><span class="rounded border border-accent/30 bg-accent/10 px-1.5 py-0.5 text-accent">{event.op}</span></td>
|
|
211
|
+
<td class="px-2 py-1 text-fg-1">{event.entityId}</td>
|
|
212
|
+
<td class="px-2 py-1 text-right text-fg-3">{event.lsn ?? '-'}</td>
|
|
213
|
+
<td class="px-2 py-1 text-fg-2 truncate max-w-[520px]">{display(event.after)}</td>
|
|
214
|
+
</tr>
|
|
215
|
+
{/each}
|
|
216
|
+
</tbody>
|
|
217
|
+
</table>
|
|
218
|
+
{/if}
|
|
219
|
+
</div>
|
|
220
|
+
{:else if mode === 'commands'}
|
|
221
|
+
<div class="flex-1 min-h-0 overflow-auto p-3 text-[12px] font-mono">
|
|
222
|
+
<div class="grid gap-2">
|
|
223
|
+
{#each commands as command (command.query)}
|
|
224
|
+
<div class="rounded border border-line-1 bg-bg-1 px-3 py-2">
|
|
225
|
+
<div class="mb-1 flex items-center gap-2">
|
|
226
|
+
<span class="text-fg-0">{command.label}</span>
|
|
227
|
+
<span class={['rounded border px-1.5 py-0.5 text-[10px]', command.mutates ? 'border-warn/30 bg-warn/10 text-warn' : 'border-ok/30 bg-ok/10 text-ok'].join(' ')}>
|
|
228
|
+
{command.mutates ? 'mutates' : 'read-only'}
|
|
229
|
+
</span>
|
|
230
|
+
</div>
|
|
231
|
+
<pre class="m-0 overflow-x-auto rounded bg-bg-0 px-2 py-1 text-fg-1">{command.query}</pre>
|
|
232
|
+
<div class="mt-1 text-fg-3">{command.note}</div>
|
|
233
|
+
</div>
|
|
234
|
+
{/each}
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
{:else}
|
|
238
|
+
<div class="flex-1 min-h-0 grid grid-cols-[minmax(360px,0.9fr)_1.1fr]">
|
|
239
|
+
<div class="overflow-auto border-r border-line-1 text-[12px] font-mono">
|
|
240
|
+
{#if filtered.length === 0}
|
|
241
|
+
<div class="p-3 text-fg-3">No queue messages in this result.</div>
|
|
242
|
+
{:else}
|
|
243
|
+
{#each filtered as msg (msg.id)}
|
|
244
|
+
<button
|
|
245
|
+
type="button"
|
|
246
|
+
onclick={() => (selected = msg)}
|
|
247
|
+
class={['w-full text-left px-3 py-2 border-b border-line-1/60 hover:bg-bg-1/50 cursor-pointer', selected === msg ? 'bg-bg-1 text-fg-0' : 'text-fg-2'].join(' ')}
|
|
248
|
+
>
|
|
249
|
+
<div class="flex items-center gap-2">
|
|
250
|
+
<span class="text-accent">{msg.id}</span>
|
|
251
|
+
<span class="rounded border border-line-1 px-1 py-0.5 text-[10px] text-fg-3">{msg.state}</span>
|
|
252
|
+
{#if msg.consumer}<span class="ml-auto text-fg-3">{msg.consumer}</span>{/if}
|
|
253
|
+
</div>
|
|
254
|
+
<div class="mt-1 truncate text-fg-1">{display(msg.message)}</div>
|
|
255
|
+
</button>
|
|
256
|
+
{/each}
|
|
257
|
+
{/if}
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div class="overflow-auto p-3 text-[12px] font-mono">
|
|
261
|
+
{#if mode === 'json'}
|
|
262
|
+
<pre class="whitespace-pre-wrap text-fg-1">{JSON.stringify(filtered.map((m) => m.values), null, 2)}</pre>
|
|
263
|
+
{:else if selected}
|
|
264
|
+
<div class="grid grid-cols-[120px_1fr] gap-x-3 gap-y-1.5">
|
|
265
|
+
<span class="text-fg-3">id</span><span class="text-fg-0">{selected.id}</span>
|
|
266
|
+
<span class="text-fg-3">state</span><span class="text-fg-0">{selected.state}</span>
|
|
267
|
+
<span class="text-fg-3">consumer</span><span class="text-fg-0">{selected.consumer ?? 'none'}</span>
|
|
268
|
+
<span class="text-fg-3">dlc</span><span class="text-fg-0">{selected.dlc ?? 'none'}</span>
|
|
269
|
+
<span class="text-fg-3">message</span><pre class="m-0 whitespace-pre-wrap text-fg-1">{display(selected.message)}</pre>
|
|
270
|
+
</div>
|
|
271
|
+
{:else}
|
|
272
|
+
<div class="text-fg-3">Select a message to inspect payload, delivery state, consumer, and DLC link.</div>
|
|
273
|
+
{/if}
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
{/if}
|
|
277
|
+
|
|
278
|
+
<div class="border-t border-line-1 px-3 py-1.5 text-[10px] font-mono text-fg-3 flex items-center gap-3">
|
|
279
|
+
{#each summary.states as [state, count] (state)}
|
|
280
|
+
<span><span class="text-fg-2">{state}</span> {count.toLocaleString()}</span>
|
|
281
|
+
{/each}
|
|
282
|
+
{#each summary.operations as [op, count] (op)}
|
|
283
|
+
<span><span class="text-fg-2">{op}</span> {count.toLocaleString()}</span>
|
|
284
|
+
{/each}
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { QueryResult } from '#reddb'
|
|
3
|
+
import { Gauge, List, Table2 } from 'lucide-svelte'
|
|
4
|
+
import { extractStats, formatMetric } from './stats-render'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
result: QueryResult
|
|
8
|
+
collection?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { result, collection }: Props = $props()
|
|
12
|
+
|
|
13
|
+
let mode = $state<'kpi' | 'gauge' | 'table'>('kpi')
|
|
14
|
+
const metrics = $derived(extractStats(result))
|
|
15
|
+
const max = $derived(Math.max(1, ...metrics.map((m) => Math.abs(m.value))))
|
|
16
|
+
|
|
17
|
+
function percent(value: number): number {
|
|
18
|
+
if (value >= 0 && value <= 100) return value
|
|
19
|
+
return Math.min(100, Math.abs(value) / max * 100)
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<div class="flex h-full flex-col text-fg-1">
|
|
24
|
+
{#if metrics.length === 0}
|
|
25
|
+
<div class="flex-1 grid place-items-center text-fg-3 text-[12px] font-mono p-6">
|
|
26
|
+
No numeric statistics in this result.
|
|
27
|
+
</div>
|
|
28
|
+
{:else}
|
|
29
|
+
<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">
|
|
30
|
+
<div class="inline-flex border border-line-2 rounded overflow-hidden">
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
class={['inline-flex items-center gap-1 px-2 py-1 cursor-pointer transition-colors', mode === 'kpi' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0'].join(' ')}
|
|
34
|
+
onclick={() => (mode = 'kpi')}
|
|
35
|
+
aria-pressed={mode === 'kpi'}
|
|
36
|
+
>
|
|
37
|
+
<List class="size-3" />
|
|
38
|
+
kpis
|
|
39
|
+
</button>
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
class={['inline-flex items-center gap-1 px-2 py-1 cursor-pointer transition-colors border-l border-line-2', mode === 'gauge' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0'].join(' ')}
|
|
43
|
+
onclick={() => (mode = 'gauge')}
|
|
44
|
+
aria-pressed={mode === 'gauge'}
|
|
45
|
+
>
|
|
46
|
+
<Gauge class="size-3" />
|
|
47
|
+
gauges
|
|
48
|
+
</button>
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
class={['inline-flex items-center gap-1 px-2 py-1 cursor-pointer transition-colors border-l border-line-2', mode === 'table' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0'].join(' ')}
|
|
52
|
+
onclick={() => (mode = 'table')}
|
|
53
|
+
aria-pressed={mode === 'table'}
|
|
54
|
+
>
|
|
55
|
+
<Table2 class="size-3" />
|
|
56
|
+
table
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="ml-auto text-fg-3">
|
|
60
|
+
{metrics.length.toLocaleString()} metrics
|
|
61
|
+
{#if collection}· <span class="text-fg-2">{collection}</span>{/if}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{#if mode === 'kpi'}
|
|
66
|
+
<div class="flex-1 overflow-auto p-3">
|
|
67
|
+
<div class="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-2">
|
|
68
|
+
{#each metrics as metric, i (`${metric.key}-${i}`)}
|
|
69
|
+
<div class="rounded border border-line-1 bg-bg-1 px-3 py-2">
|
|
70
|
+
<div class="type-label truncate">{metric.label}</div>
|
|
71
|
+
<div class="mt-1 font-mono text-[20px] font-semibold text-fg-0">{formatMetric(metric.value, metric.unit)}</div>
|
|
72
|
+
</div>
|
|
73
|
+
{/each}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
{:else if mode === 'gauge'}
|
|
77
|
+
<div class="flex-1 overflow-auto p-3">
|
|
78
|
+
<div class="grid grid-cols-[repeat(auto-fill,minmax(180px,1fr))] gap-2">
|
|
79
|
+
{#each metrics as metric, i (`${metric.key}-${i}`)}
|
|
80
|
+
{@const p = percent(metric.value)}
|
|
81
|
+
<div class="rounded border border-line-1 bg-bg-1 px-3 py-3 text-center">
|
|
82
|
+
<svg viewBox="0 0 120 70" class="mx-auto h-[82px] w-[150px]" aria-label="{metric.label} gauge">
|
|
83
|
+
<path d="M 15 60 A 45 45 0 0 1 105 60" fill="none" stroke="var(--color-line-2)" stroke-width="10" stroke-linecap="round" />
|
|
84
|
+
<path
|
|
85
|
+
d="M 15 60 A 45 45 0 0 1 105 60"
|
|
86
|
+
fill="none"
|
|
87
|
+
stroke="var(--color-accent)"
|
|
88
|
+
stroke-width="10"
|
|
89
|
+
stroke-linecap="round"
|
|
90
|
+
pathLength="100"
|
|
91
|
+
stroke-dasharray={`${p} 100`}
|
|
92
|
+
/>
|
|
93
|
+
<text x="60" y="58" text-anchor="middle" class="fill-fg-0 font-mono text-[13px] font-semibold">{formatMetric(metric.value, metric.unit)}</text>
|
|
94
|
+
</svg>
|
|
95
|
+
<div class="mt-1 truncate font-mono text-[11px] text-fg-2">{metric.label}</div>
|
|
96
|
+
</div>
|
|
97
|
+
{/each}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
{:else}
|
|
101
|
+
<div class="flex-1 overflow-auto">
|
|
102
|
+
<table class="w-full border-collapse text-[12px] font-mono">
|
|
103
|
+
<thead class="sticky top-0 bg-bg-1 z-10">
|
|
104
|
+
<tr class="border-b border-line-1 text-fg-3">
|
|
105
|
+
<th class="px-2 py-1.5 text-left font-normal">metric</th>
|
|
106
|
+
<th class="px-2 py-1.5 text-right font-normal">value</th>
|
|
107
|
+
<th class="px-2 py-1.5 text-left font-normal">unit</th>
|
|
108
|
+
</tr>
|
|
109
|
+
</thead>
|
|
110
|
+
<tbody>
|
|
111
|
+
{#each metrics as metric, i (`${metric.key}-${i}`)}
|
|
112
|
+
<tr class="border-b border-line-1/60 hover:bg-bg-1/40">
|
|
113
|
+
<td class="px-2 py-1 text-fg-0">{metric.label}</td>
|
|
114
|
+
<td class="px-2 py-1 text-right text-accent">{metric.value.toLocaleString()}</td>
|
|
115
|
+
<td class="px-2 py-1 text-fg-3">{metric.unit || '-'}</td>
|
|
116
|
+
</tr>
|
|
117
|
+
{/each}
|
|
118
|
+
</tbody>
|
|
119
|
+
</table>
|
|
120
|
+
</div>
|
|
121
|
+
{/if}
|
|
122
|
+
{/if}
|
|
123
|
+
</div>
|