@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,266 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { CollectionVcsState, QueryResult, RedClient, VcsCommit, VcsDiff } from '#reddb'
|
|
3
|
+
import { GitCommitHorizontal, GitCompareArrows, LoaderCircle, RadioTower, RefreshCw, ShieldCheck } from 'lucide-svelte'
|
|
4
|
+
import DiffRenderer from '$lib/renderers/DiffRenderer.svelte'
|
|
5
|
+
import LiveChanges from '$lib/LiveChanges.svelte'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
client?: RedClient
|
|
9
|
+
collection: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { client, collection }: Props = $props()
|
|
13
|
+
|
|
14
|
+
let mode = $state<'commits' | 'diff' | 'live'>('commits')
|
|
15
|
+
let vcs = $state<CollectionVcsState | null>(null)
|
|
16
|
+
let commits = $state<VcsCommit[]>([])
|
|
17
|
+
let from = $state('')
|
|
18
|
+
let to = $state('')
|
|
19
|
+
let diff = $state<VcsDiff | null>(null)
|
|
20
|
+
let loading = $state(false)
|
|
21
|
+
let diffLoading = $state(false)
|
|
22
|
+
let error = $state<string | null>(null)
|
|
23
|
+
let loadedKey = ''
|
|
24
|
+
|
|
25
|
+
const canDiff = $derived(from.length > 0 && to.length > 0 && from !== to)
|
|
26
|
+
|
|
27
|
+
function shortHash(hash: string): string {
|
|
28
|
+
return hash.slice(0, 10)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatTime(ms: number | undefined): string {
|
|
32
|
+
if (!ms) return '-'
|
|
33
|
+
return new Date(ms).toISOString().replace('T', ' ').replace('Z', '')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function diffResult(d: VcsDiff): QueryResult {
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
query: `/repo/commits/${d.from}/diff/${d.to}?collection=${collection}`,
|
|
40
|
+
capability: 'diff',
|
|
41
|
+
record_count: d.entries.length,
|
|
42
|
+
result: {
|
|
43
|
+
columns: ['collection', 'entity_id', 'change', 'before', 'after'],
|
|
44
|
+
records: d.entries.map((entry, index) => ({
|
|
45
|
+
values: {
|
|
46
|
+
id: `${entry.collection}:${entry.entity_id}:${index}`,
|
|
47
|
+
collection: entry.collection,
|
|
48
|
+
entity_id: entry.entity_id,
|
|
49
|
+
change: entry.change,
|
|
50
|
+
before: entry.before ?? null,
|
|
51
|
+
after: entry.after ?? null,
|
|
52
|
+
},
|
|
53
|
+
})),
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function load() {
|
|
59
|
+
if (!client) return
|
|
60
|
+
loading = true
|
|
61
|
+
error = null
|
|
62
|
+
diff = null
|
|
63
|
+
try {
|
|
64
|
+
const [vcsState, log] = await Promise.all([
|
|
65
|
+
client.collectionVcs(collection),
|
|
66
|
+
client.commits({ limit: 40 }),
|
|
67
|
+
])
|
|
68
|
+
vcs = vcsState
|
|
69
|
+
commits = log
|
|
70
|
+
if (log.length >= 2) {
|
|
71
|
+
to = log[0].hash
|
|
72
|
+
from = log[1].hash
|
|
73
|
+
} else if (log.length === 1) {
|
|
74
|
+
to = log[0].hash
|
|
75
|
+
from = ''
|
|
76
|
+
} else {
|
|
77
|
+
to = ''
|
|
78
|
+
from = ''
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
error = (e as Error).message
|
|
82
|
+
} finally {
|
|
83
|
+
loading = false
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function enableVersioning() {
|
|
88
|
+
if (!client) return
|
|
89
|
+
loading = true
|
|
90
|
+
error = null
|
|
91
|
+
try {
|
|
92
|
+
vcs = await client.setCollectionVcs(collection, true)
|
|
93
|
+
commits = await client.commits({ limit: 40 })
|
|
94
|
+
} catch (e) {
|
|
95
|
+
error = (e as Error).message
|
|
96
|
+
} finally {
|
|
97
|
+
loading = false
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function runDiff() {
|
|
102
|
+
if (!client || !canDiff) return
|
|
103
|
+
diffLoading = true
|
|
104
|
+
error = null
|
|
105
|
+
try {
|
|
106
|
+
diff = await client.commitDiff(from, to, { collection })
|
|
107
|
+
mode = 'diff'
|
|
108
|
+
} catch (e) {
|
|
109
|
+
error = (e as Error).message
|
|
110
|
+
} finally {
|
|
111
|
+
diffLoading = false
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
$effect(() => {
|
|
116
|
+
const key = `${client?.baseUrl ?? 'none'}:${collection}`
|
|
117
|
+
if (key === loadedKey) return
|
|
118
|
+
loadedKey = key
|
|
119
|
+
void load()
|
|
120
|
+
})
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
<div class="h-full min-h-0 flex flex-col bg-bg-0">
|
|
124
|
+
<div class="border-b border-line-1 px-3 py-1.5 flex items-center gap-2 font-mono text-[11px]">
|
|
125
|
+
<div class="inline-flex overflow-hidden rounded border border-line-2">
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
class={['inline-flex h-7 items-center gap-1 px-2', mode === 'commits' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0'].join(' ')}
|
|
129
|
+
onclick={() => (mode = 'commits')}
|
|
130
|
+
>
|
|
131
|
+
<GitCommitHorizontal class="size-3" />
|
|
132
|
+
commits
|
|
133
|
+
</button>
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
class={['inline-flex h-7 items-center gap-1 border-l border-line-2 px-2', mode === 'diff' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0'].join(' ')}
|
|
137
|
+
onclick={() => (mode = 'diff')}
|
|
138
|
+
>
|
|
139
|
+
<GitCompareArrows class="size-3" />
|
|
140
|
+
diff
|
|
141
|
+
</button>
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
class={['inline-flex h-7 items-center gap-1 border-l border-line-2 px-2', mode === 'live' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0'].join(' ')}
|
|
145
|
+
onclick={() => (mode = 'live')}
|
|
146
|
+
>
|
|
147
|
+
<RadioTower class="size-3" />
|
|
148
|
+
live
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<span class={vcs?.versioned ? 'text-ok' : 'text-fg-3'}>
|
|
153
|
+
{vcs?.versioned ? 'versioned' : 'not versioned'}
|
|
154
|
+
</span>
|
|
155
|
+
<span class="text-fg-3">{collection}</span>
|
|
156
|
+
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
class="ml-auto inline-flex h-7 items-center gap-1 rounded border border-line-1 px-2 text-fg-3 hover:bg-bg-2 hover:text-fg-1"
|
|
160
|
+
onclick={() => void load()}
|
|
161
|
+
disabled={loading}
|
|
162
|
+
>
|
|
163
|
+
{#if loading}
|
|
164
|
+
<LoaderCircle class="size-3 animate-spin" />
|
|
165
|
+
{:else}
|
|
166
|
+
<RefreshCw class="size-3" />
|
|
167
|
+
{/if}
|
|
168
|
+
refresh
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{#if error}
|
|
173
|
+
<div class="border-b border-line-1 px-3 py-1.5 font-mono text-[11px] text-warn">{error}</div>
|
|
174
|
+
{/if}
|
|
175
|
+
|
|
176
|
+
{#if !client}
|
|
177
|
+
<div class="p-4 font-mono text-[12px] text-fg-3">No active RedDB connection.</div>
|
|
178
|
+
{:else if loading && !vcs}
|
|
179
|
+
<div class="p-4 font-mono text-[12px] text-fg-3">Loading version history…</div>
|
|
180
|
+
{:else if vcs && !vcs.versioned}
|
|
181
|
+
<div class="p-4 font-mono text-[12px] text-fg-3">
|
|
182
|
+
<div class="mb-2 text-fg-1">This collection is a normal collection with versioning disabled.</div>
|
|
183
|
+
<button
|
|
184
|
+
type="button"
|
|
185
|
+
class="inline-flex h-7 items-center gap-1 rounded border border-accent/40 bg-accent px-2 text-white hover:bg-accent/90"
|
|
186
|
+
onclick={() => void enableVersioning()}
|
|
187
|
+
>
|
|
188
|
+
<ShieldCheck class="size-3" />
|
|
189
|
+
enable versioning
|
|
190
|
+
</button>
|
|
191
|
+
</div>
|
|
192
|
+
{:else if mode === 'live'}
|
|
193
|
+
<div class="flex-1 min-h-0">
|
|
194
|
+
<LiveChanges {collection} />
|
|
195
|
+
</div>
|
|
196
|
+
{:else if mode === 'diff'}
|
|
197
|
+
<div class="border-b border-line-1 px-3 py-1.5 flex flex-wrap items-center gap-2 font-mono text-[11px]">
|
|
198
|
+
<label class="inline-flex items-center gap-1 text-fg-3">
|
|
199
|
+
<span>from</span>
|
|
200
|
+
<select bind:value={from} class="h-7 rounded border border-line-1 bg-bg-0 px-2 text-fg-1">
|
|
201
|
+
<option value="">select commit</option>
|
|
202
|
+
{#each commits as commit (commit.hash)}
|
|
203
|
+
<option value={commit.hash}>{shortHash(commit.hash)} · {commit.message}</option>
|
|
204
|
+
{/each}
|
|
205
|
+
</select>
|
|
206
|
+
</label>
|
|
207
|
+
<label class="inline-flex items-center gap-1 text-fg-3">
|
|
208
|
+
<span>to</span>
|
|
209
|
+
<select bind:value={to} class="h-7 rounded border border-line-1 bg-bg-0 px-2 text-fg-1">
|
|
210
|
+
<option value="">select commit</option>
|
|
211
|
+
{#each commits as commit (commit.hash)}
|
|
212
|
+
<option value={commit.hash}>{shortHash(commit.hash)} · {commit.message}</option>
|
|
213
|
+
{/each}
|
|
214
|
+
</select>
|
|
215
|
+
</label>
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
class="inline-flex h-7 items-center gap-1 rounded border border-line-1 px-2 text-fg-2 hover:bg-bg-2 disabled:text-fg-3"
|
|
219
|
+
disabled={!canDiff || diffLoading}
|
|
220
|
+
onclick={() => void runDiff()}
|
|
221
|
+
>
|
|
222
|
+
{#if diffLoading}<LoaderCircle class="size-3 animate-spin" />{:else}<GitCompareArrows class="size-3" />{/if}
|
|
223
|
+
run diff
|
|
224
|
+
</button>
|
|
225
|
+
{#if diff}
|
|
226
|
+
<span class="ml-auto text-fg-3">
|
|
227
|
+
+{diff.added} -{diff.removed} ~{diff.modified}
|
|
228
|
+
</span>
|
|
229
|
+
{/if}
|
|
230
|
+
</div>
|
|
231
|
+
<div class="flex-1 min-h-0">
|
|
232
|
+
{#if diff}
|
|
233
|
+
<DiffRenderer result={diffResult(diff)} {collection} />
|
|
234
|
+
{:else}
|
|
235
|
+
<div class="p-4 font-mono text-[12px] text-fg-3">Choose two commits to compare this collection.</div>
|
|
236
|
+
{/if}
|
|
237
|
+
</div>
|
|
238
|
+
{:else}
|
|
239
|
+
<div class="flex-1 overflow-auto">
|
|
240
|
+
{#if commits.length === 0}
|
|
241
|
+
<div class="p-4 font-mono text-[12px] text-fg-3">No commits yet.</div>
|
|
242
|
+
{:else}
|
|
243
|
+
<table class="w-full border-collapse font-mono text-[12px]">
|
|
244
|
+
<thead class="sticky top-0 bg-bg-0 text-fg-3">
|
|
245
|
+
<tr class="border-b border-line-1">
|
|
246
|
+
<th class="px-3 py-1.5 text-left font-normal">commit</th>
|
|
247
|
+
<th class="px-3 py-1.5 text-left font-normal">message</th>
|
|
248
|
+
<th class="px-3 py-1.5 text-left font-normal">author</th>
|
|
249
|
+
<th class="px-3 py-1.5 text-left font-normal">time</th>
|
|
250
|
+
</tr>
|
|
251
|
+
</thead>
|
|
252
|
+
<tbody>
|
|
253
|
+
{#each commits as commit (commit.hash)}
|
|
254
|
+
<tr class="border-b border-line-1/60 hover:bg-bg-1/50">
|
|
255
|
+
<td class="px-3 py-1.5 text-accent">{shortHash(commit.hash)}</td>
|
|
256
|
+
<td class="px-3 py-1.5 text-fg-1">{commit.message}</td>
|
|
257
|
+
<td class="px-3 py-1.5 text-fg-3">{commit.author?.name ?? '-'}</td>
|
|
258
|
+
<td class="px-3 py-1.5 text-fg-3">{formatTime(commit.timestamp_ms)}</td>
|
|
259
|
+
</tr>
|
|
260
|
+
{/each}
|
|
261
|
+
</tbody>
|
|
262
|
+
</table>
|
|
263
|
+
{/if}
|
|
264
|
+
</div>
|
|
265
|
+
{/if}
|
|
266
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import SchemaTree from './SchemaTree.svelte'
|
|
3
|
+
import ResultsPane from './ResultsPane.svelte'
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<div class="h-full grid grid-cols-[260px_1fr] bg-bg-0 text-fg-1">
|
|
7
|
+
<aside class="row-start-1 col-start-1 border-r border-line-1 bg-bg-0 overflow-y-auto">
|
|
8
|
+
<SchemaTree />
|
|
9
|
+
</aside>
|
|
10
|
+
|
|
11
|
+
<section class="row-start-1 col-start-2 bg-bg-0 overflow-hidden">
|
|
12
|
+
<ResultsPane />
|
|
13
|
+
</section>
|
|
14
|
+
</div>
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte'
|
|
3
|
+
import { Kbd } from '@reddb-io/ui-kit'
|
|
4
|
+
import { useRouter, type RouteTarget } from './router.svelte'
|
|
5
|
+
import { theme } from './theme.svelte'
|
|
6
|
+
|
|
7
|
+
const router = useRouter()
|
|
8
|
+
|
|
9
|
+
type Group = 'navigate' | 'data' | 'actions' | 'view'
|
|
10
|
+
|
|
11
|
+
interface Command {
|
|
12
|
+
id: string
|
|
13
|
+
group: Group
|
|
14
|
+
label: string
|
|
15
|
+
hint?: string
|
|
16
|
+
shortcut?: string
|
|
17
|
+
run: () => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let open = $state(false)
|
|
21
|
+
let query = $state('')
|
|
22
|
+
let active = $state(0)
|
|
23
|
+
let inputEl: HTMLInputElement | undefined = $state()
|
|
24
|
+
|
|
25
|
+
function fire(name: string) {
|
|
26
|
+
window.dispatchEvent(new CustomEvent(name))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const commands: Command[] = [
|
|
30
|
+
{ id: 'query', group: 'navigate', label: 'Open query', hint: '/query', run: () => navigate({ view: 'query' }) },
|
|
31
|
+
{ id: 'collections', group: 'navigate', label: 'Open collections', hint: '/collections', run: () => navigate({ view: 'collections' }) },
|
|
32
|
+
{ id: 'topology', group: 'navigate', label: 'Open cluster', hint: '/cluster', run: () => navigate({ view: 'cluster' }) },
|
|
33
|
+
{ id: 'security', group: 'navigate', label: 'Open security', hint: '/security', run: () => navigate({ view: 'security' }) },
|
|
34
|
+
|
|
35
|
+
{ id: 'new-query', group: 'data', label: 'New query', shortcut: '⌘T', run: () => { fire('red:new-query'); close() } },
|
|
36
|
+
|
|
37
|
+
{ id: 'open-connect', group: 'actions', label: 'Open Connect dropdown', shortcut: '⌘⇧C', run: () => { fire('red:open-connect'); close() } },
|
|
38
|
+
{ id: 'apply-pending', group: 'actions', label: 'Apply pending changes', run: () => { fire('red:apply-pending'); close() } },
|
|
39
|
+
{ id: 'discard-pending', group: 'actions', label: 'Discard pending changes', run: () => { fire('red:discard-pending'); close() } },
|
|
40
|
+
|
|
41
|
+
{ id: 'toggle-theme', group: 'view', label: 'Toggle theme (light / dark)', hint: 'theme', run: () => { theme.toggle(); close() } },
|
|
42
|
+
{ id: 'show-shortcuts', group: 'view', label: 'Show keyboard shortcuts', shortcut: '?', run: () => { close(); fire('red:open-shortcuts') } },
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
const filtered = $derived(
|
|
46
|
+
query.trim() === ''
|
|
47
|
+
? commands
|
|
48
|
+
: commands.filter((c) => c.label.toLowerCase().includes(query.toLowerCase()))
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
function navigate(target: RouteTarget) {
|
|
52
|
+
router.go(target)
|
|
53
|
+
close()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function close() {
|
|
57
|
+
open = false
|
|
58
|
+
query = ''
|
|
59
|
+
active = 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isTypingTarget(el: EventTarget | null): boolean {
|
|
63
|
+
if (!(el instanceof HTMLElement)) return false
|
|
64
|
+
if (el.isContentEditable) return true
|
|
65
|
+
const tag = el.tagName
|
|
66
|
+
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function onKey(e: KeyboardEvent) {
|
|
70
|
+
const mod = e.metaKey || e.ctrlKey
|
|
71
|
+
const k = e.key.toLowerCase()
|
|
72
|
+
|
|
73
|
+
if (mod && k === 'k') {
|
|
74
|
+
e.preventDefault()
|
|
75
|
+
open = !open
|
|
76
|
+
if (open) setTimeout(() => inputEl?.focus(), 0)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Global accelerators wired here so they fire regardless of focus.
|
|
81
|
+
if (mod && e.shiftKey && k === 'c') {
|
|
82
|
+
e.preventDefault()
|
|
83
|
+
fire('red:open-connect')
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
if (mod && !e.shiftKey && k === 't' && !isTypingTarget(e.target)) {
|
|
87
|
+
e.preventDefault()
|
|
88
|
+
fire('red:new-query')
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!open) return
|
|
93
|
+
if (e.key === 'Escape') { close(); return }
|
|
94
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); active = Math.min(active + 1, filtered.length - 1) }
|
|
95
|
+
if (e.key === 'ArrowUp') { e.preventDefault(); active = Math.max(active - 1, 0) }
|
|
96
|
+
if (e.key === 'Enter') {
|
|
97
|
+
e.preventDefault()
|
|
98
|
+
filtered[active]?.run()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
onMount(() => {
|
|
103
|
+
window.addEventListener('keydown', onKey)
|
|
104
|
+
const onOpen = () => {
|
|
105
|
+
open = true
|
|
106
|
+
setTimeout(() => inputEl?.focus(), 0)
|
|
107
|
+
}
|
|
108
|
+
window.addEventListener('red:open-palette', onOpen)
|
|
109
|
+
return () => {
|
|
110
|
+
window.removeEventListener('keydown', onKey)
|
|
111
|
+
window.removeEventListener('red:open-palette', onOpen)
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const groupOrder: Group[] = ['navigate', 'data', 'actions', 'view']
|
|
116
|
+
const groupLabel: Record<Group, string> = {
|
|
117
|
+
navigate: 'Navigate',
|
|
118
|
+
data: 'Data',
|
|
119
|
+
actions: 'Actions',
|
|
120
|
+
view: 'View',
|
|
121
|
+
}
|
|
122
|
+
</script>
|
|
123
|
+
|
|
124
|
+
{#if open}
|
|
125
|
+
<div class="scrim" onclick={close} onkeydown={null} role="presentation"></div>
|
|
126
|
+
<div class="palette" role="dialog" aria-modal="true">
|
|
127
|
+
<div class="input-wrap">
|
|
128
|
+
<span class="search-icon">⌕</span>
|
|
129
|
+
<input
|
|
130
|
+
bind:this={inputEl}
|
|
131
|
+
bind:value={query}
|
|
132
|
+
oninput={() => (active = 0)}
|
|
133
|
+
placeholder="Search or run a command…"
|
|
134
|
+
spellcheck="false"
|
|
135
|
+
/>
|
|
136
|
+
<Kbd>esc</Kbd>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div class="list">
|
|
140
|
+
{#each groupOrder as g}
|
|
141
|
+
{@const items = filtered.filter((c) => c.group === g)}
|
|
142
|
+
{#if items.length}
|
|
143
|
+
<div class="group-label">{groupLabel[g]}</div>
|
|
144
|
+
{#each items as cmd}
|
|
145
|
+
{@const idx = filtered.indexOf(cmd)}
|
|
146
|
+
<button
|
|
147
|
+
class="row"
|
|
148
|
+
class:active={idx === active}
|
|
149
|
+
onmouseenter={() => (active = idx)}
|
|
150
|
+
onclick={() => cmd.run()}
|
|
151
|
+
>
|
|
152
|
+
<span class="label">{cmd.label}</span>
|
|
153
|
+
{#if cmd.hint}<span class="hint">{cmd.hint}</span>{/if}
|
|
154
|
+
{#if cmd.shortcut}<Kbd>{cmd.shortcut}</Kbd>{/if}
|
|
155
|
+
</button>
|
|
156
|
+
{/each}
|
|
157
|
+
{/if}
|
|
158
|
+
{/each}
|
|
159
|
+
{#if filtered.length === 0}
|
|
160
|
+
<div class="empty">No commands match "{query}"</div>
|
|
161
|
+
{/if}
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<footer>
|
|
165
|
+
<span><Kbd>↑</Kbd> <Kbd>↓</Kbd> navigate</span>
|
|
166
|
+
<span><Kbd>↵</Kbd> run</span>
|
|
167
|
+
<span><Kbd>⌘</Kbd><Kbd>K</Kbd> toggle</span>
|
|
168
|
+
</footer>
|
|
169
|
+
</div>
|
|
170
|
+
{/if}
|
|
171
|
+
|
|
172
|
+
<style>
|
|
173
|
+
.scrim {
|
|
174
|
+
position: fixed; inset: 0; z-index: 100;
|
|
175
|
+
background: rgba(0, 0, 0, 0.5);
|
|
176
|
+
backdrop-filter: blur(4px);
|
|
177
|
+
animation: fade 140ms var(--ease-out);
|
|
178
|
+
}
|
|
179
|
+
.palette {
|
|
180
|
+
position: fixed; z-index: 101;
|
|
181
|
+
top: 18%; left: 50%; transform: translateX(-50%);
|
|
182
|
+
width: min(640px, 92vw);
|
|
183
|
+
background: var(--bg-1);
|
|
184
|
+
border: 1px solid var(--line-2);
|
|
185
|
+
border-radius: var(--r-lg);
|
|
186
|
+
box-shadow: var(--shadow-lg);
|
|
187
|
+
overflow: hidden;
|
|
188
|
+
animation: pop 180ms var(--ease-snap);
|
|
189
|
+
}
|
|
190
|
+
@keyframes fade { from { opacity: 0 } to { opacity: 1 } }
|
|
191
|
+
@keyframes pop { from { opacity: 0; transform: translate(-50%, -8px) scale(0.98); } to { opacity: 1; transform: translateX(-50%) scale(1); } }
|
|
192
|
+
|
|
193
|
+
.input-wrap {
|
|
194
|
+
display: flex; align-items: center; gap: 10px;
|
|
195
|
+
padding: 12px 14px;
|
|
196
|
+
border-bottom: 1px solid var(--line-1);
|
|
197
|
+
}
|
|
198
|
+
.search-icon { color: var(--fg-3); font-size: 18px; }
|
|
199
|
+
.input-wrap input {
|
|
200
|
+
flex: 1; background: transparent; border: 0; outline: 0;
|
|
201
|
+
color: var(--fg-0); font-size: 14px;
|
|
202
|
+
}
|
|
203
|
+
.input-wrap input::placeholder { color: var(--fg-3); }
|
|
204
|
+
|
|
205
|
+
.list { max-height: 360px; overflow-y: auto; padding: 6px; }
|
|
206
|
+
.group-label {
|
|
207
|
+
padding: 10px 8px 4px;
|
|
208
|
+
font-family: var(--font-mono);
|
|
209
|
+
font-size: 10px;
|
|
210
|
+
text-transform: uppercase;
|
|
211
|
+
letter-spacing: 0.08em;
|
|
212
|
+
color: var(--fg-3);
|
|
213
|
+
}
|
|
214
|
+
.row {
|
|
215
|
+
display: flex; align-items: center; gap: 10px;
|
|
216
|
+
width: 100%; padding: 8px 10px;
|
|
217
|
+
background: transparent; border: 0; cursor: pointer;
|
|
218
|
+
border-radius: var(--r-md);
|
|
219
|
+
text-align: left;
|
|
220
|
+
color: var(--fg-1);
|
|
221
|
+
font-size: 13px;
|
|
222
|
+
}
|
|
223
|
+
.row.active { background: var(--bg-2); color: var(--fg-0); }
|
|
224
|
+
.label { flex: 1; }
|
|
225
|
+
.hint { color: var(--fg-3); font-family: var(--font-mono); font-size: 11px; }
|
|
226
|
+
|
|
227
|
+
.empty { padding: 24px; text-align: center; color: var(--fg-3); font-size: 13px; }
|
|
228
|
+
|
|
229
|
+
footer {
|
|
230
|
+
display: flex; gap: 14px; justify-content: flex-end;
|
|
231
|
+
padding: 8px 14px;
|
|
232
|
+
border-top: 1px solid var(--line-1);
|
|
233
|
+
background: var(--bg-0);
|
|
234
|
+
font-size: 11px;
|
|
235
|
+
color: var(--fg-3);
|
|
236
|
+
}
|
|
237
|
+
</style>
|