@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,440 @@
|
|
|
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 { pendingChanges } from '$lib/pending-changes.svelte'
|
|
6
|
+
import { Columns3, Filter, Loader2, Play, RotateCw, X } from 'lucide-svelte'
|
|
7
|
+
import {
|
|
8
|
+
formatCell,
|
|
9
|
+
identityKind,
|
|
10
|
+
inferPkColumn,
|
|
11
|
+
isNull,
|
|
12
|
+
isSystemField,
|
|
13
|
+
visibleColumns,
|
|
14
|
+
visibleRows,
|
|
15
|
+
} from './table-render'
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
result: QueryResult
|
|
19
|
+
collection?: string
|
|
20
|
+
showSystem?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let { result, collection, showSystem = false }: Props = $props()
|
|
24
|
+
|
|
25
|
+
let overrideResult = $state<QueryResult | null>(null)
|
|
26
|
+
const activeResult = $derived(overrideResult ?? result)
|
|
27
|
+
const allRows = $derived(visibleRows(activeResult))
|
|
28
|
+
const columns = $derived(visibleColumns(activeResult, { showSystem }))
|
|
29
|
+
const pkCol = $derived(inferPkColumn(columns))
|
|
30
|
+
const queryableColumns = $derived(columns.filter((c) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(c)))
|
|
31
|
+
|
|
32
|
+
let selectOpen = $state(false)
|
|
33
|
+
let selectedColumns = $state<Record<string, boolean>>({})
|
|
34
|
+
let selectLimit = $state(200)
|
|
35
|
+
let selecting = $state(false)
|
|
36
|
+
let selectError = $state<string | null>(null)
|
|
37
|
+
|
|
38
|
+
$effect(() => {
|
|
39
|
+
const next = { ...selectedColumns }
|
|
40
|
+
let changed = false
|
|
41
|
+
for (const col of queryableColumns) {
|
|
42
|
+
if (next[col] === undefined) {
|
|
43
|
+
next[col] = true
|
|
44
|
+
changed = true
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const col of Object.keys(next)) {
|
|
48
|
+
if (!queryableColumns.includes(col)) {
|
|
49
|
+
delete next[col]
|
|
50
|
+
changed = true
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (changed) selectedColumns = next
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// ─── filters ────────────────────────────────────────────────────────────
|
|
57
|
+
// Per-column substring filter, case-insensitive. The toolbar shows a
|
|
58
|
+
// single-input "filter X" affordance with column picker; the header
|
|
59
|
+
// also lets the user click a column name to focus the filter on it.
|
|
60
|
+
let filters = $state<Record<string, string>>({})
|
|
61
|
+
let filtersOpen = $state(false)
|
|
62
|
+
|
|
63
|
+
const filteredRows = $derived.by(() => {
|
|
64
|
+
const active = Object.entries(filters).filter(([, v]) => v.trim().length > 0)
|
|
65
|
+
if (active.length === 0) return allRows
|
|
66
|
+
return allRows.filter((row) =>
|
|
67
|
+
active.every(([col, term]) => {
|
|
68
|
+
const v = row[col]
|
|
69
|
+
if (v === null || v === undefined) return false
|
|
70
|
+
return String(v).toLowerCase().includes(term.toLowerCase())
|
|
71
|
+
}),
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const rows = $derived(filteredRows)
|
|
76
|
+
const activeFilterCount = $derived(Object.values(filters).filter((v) => v.trim().length > 0).length)
|
|
77
|
+
|
|
78
|
+
function clearFilter(col: string) {
|
|
79
|
+
const next = { ...filters }
|
|
80
|
+
delete next[col]
|
|
81
|
+
filters = next
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function clearAllFilters() {
|
|
85
|
+
filters = {}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function safeCollectionName(): string | null {
|
|
89
|
+
if (!collection) return null
|
|
90
|
+
const safe = collection.replace(/[^A-Za-z0-9_./-]/g, '')
|
|
91
|
+
return safe.length > 0 ? safe : null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function selectedProjection(): string {
|
|
95
|
+
const cols = queryableColumns.filter((c) => selectedColumns[c])
|
|
96
|
+
return cols.length === 0 || cols.length === queryableColumns.length ? '*' : cols.join(', ')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function selectSql(): string | null {
|
|
100
|
+
const safe = safeCollectionName()
|
|
101
|
+
if (!safe) return null
|
|
102
|
+
return `SELECT ${selectedProjection()} FROM ${safe} LIMIT ${selectLimit}`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function runSelect() {
|
|
106
|
+
const client = connection.client
|
|
107
|
+
const sql = selectSql()
|
|
108
|
+
if (!client || !sql) return
|
|
109
|
+
selecting = true
|
|
110
|
+
selectError = null
|
|
111
|
+
try {
|
|
112
|
+
const r = await activity.track(`${collection} · ${sql}`, () => client.query(sql))
|
|
113
|
+
if (!r.ok) {
|
|
114
|
+
selectError = r.error ?? 'query failed'
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
overrideResult = r
|
|
118
|
+
filters = {}
|
|
119
|
+
editing = null
|
|
120
|
+
} catch (e) {
|
|
121
|
+
selectError = (e as Error).message
|
|
122
|
+
} finally {
|
|
123
|
+
selecting = false
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function resetSelect() {
|
|
128
|
+
overrideResult = null
|
|
129
|
+
filters = {}
|
|
130
|
+
selectError = null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let editing = $state<{ row: number; col: string } | null>(null)
|
|
134
|
+
let editValue = $state('')
|
|
135
|
+
|
|
136
|
+
function startEdit(row: number, col: string, value: unknown) {
|
|
137
|
+
// Read-only server (#23): cells aren't editable — the mutation entry point
|
|
138
|
+
// is absent rather than letting a doomed edit stage and fail at commit.
|
|
139
|
+
if (editing || !collection || connection.readOnly) return
|
|
140
|
+
editing = { row, col }
|
|
141
|
+
const staged = pendingChanges.find(collection, row, col)
|
|
142
|
+
editValue = staged ? staged.newValue : String(value ?? '')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function commit() {
|
|
146
|
+
if (!editing || !collection) return
|
|
147
|
+
const { row, col } = editing
|
|
148
|
+
const original = rows[row]?.[col]
|
|
149
|
+
const next = editValue
|
|
150
|
+
if (String(original ?? '') !== next) {
|
|
151
|
+
pendingChanges.stage(collection, row, col, original, next)
|
|
152
|
+
} else {
|
|
153
|
+
// Reverting to original value — drop any prior staged change for it.
|
|
154
|
+
const existing = pendingChanges.find(collection, row, col)
|
|
155
|
+
if (existing) pendingChanges.unstage(existing.id)
|
|
156
|
+
}
|
|
157
|
+
editing = null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function cancel() {
|
|
161
|
+
editing = null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function focusOnMount(node: HTMLInputElement) {
|
|
165
|
+
node.focus()
|
|
166
|
+
node.select()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function cellDisplay(row: number, col: string, v: unknown): { value: unknown; staged: boolean } {
|
|
170
|
+
if (!collection) return { value: v, staged: false }
|
|
171
|
+
const staged = pendingChanges.find(collection, row, col)
|
|
172
|
+
if (staged) return { value: staged.newValue, staged: true }
|
|
173
|
+
return { value: v, staged: false }
|
|
174
|
+
}
|
|
175
|
+
</script>
|
|
176
|
+
|
|
177
|
+
<div class="flex h-full flex-col text-fg-1">
|
|
178
|
+
{#if !activeResult.ok}
|
|
179
|
+
<div class="flex-1 grid place-items-center text-fg-3 text-[12px] font-mono p-6">
|
|
180
|
+
{activeResult.error ?? 'Query failed.'}
|
|
181
|
+
</div>
|
|
182
|
+
{:else}
|
|
183
|
+
<!-- Toolbar: SELECT + per-column filters -->
|
|
184
|
+
<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">
|
|
185
|
+
{#if collection}
|
|
186
|
+
<button
|
|
187
|
+
type="button"
|
|
188
|
+
onclick={() => (selectOpen = !selectOpen)}
|
|
189
|
+
aria-pressed={selectOpen}
|
|
190
|
+
title="Build a SELECT for this collection"
|
|
191
|
+
class={[
|
|
192
|
+
'inline-flex items-center gap-1.5 h-6 px-2 rounded border transition-colors cursor-pointer',
|
|
193
|
+
selectOpen
|
|
194
|
+
? 'border-accent/40 bg-accent/10 text-accent'
|
|
195
|
+
: 'border-line-1 text-fg-3 hover:text-fg-1 hover:border-line-2',
|
|
196
|
+
].join(' ')}
|
|
197
|
+
>
|
|
198
|
+
<Columns3 class="size-3" />
|
|
199
|
+
<span>select</span>
|
|
200
|
+
</button>
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
onclick={runSelect}
|
|
204
|
+
disabled={selecting || !connection.connected}
|
|
205
|
+
class="inline-flex items-center gap-1 h-6 px-2 rounded border border-line-1 text-fg-3 hover:text-fg-1 hover:border-line-2 cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
|
|
206
|
+
title="Run the current SELECT against reddb"
|
|
207
|
+
>
|
|
208
|
+
{#if selecting}<Loader2 class="size-3 animate-spin" />{:else}<Play class="size-3" />{/if}
|
|
209
|
+
run
|
|
210
|
+
</button>
|
|
211
|
+
{#if overrideResult}
|
|
212
|
+
<button
|
|
213
|
+
type="button"
|
|
214
|
+
onclick={resetSelect}
|
|
215
|
+
class="inline-flex items-center gap-1 h-6 px-2 rounded border border-line-1 text-fg-3 hover:text-fg-1 hover:border-line-2 cursor-pointer"
|
|
216
|
+
title="Return to the original result"
|
|
217
|
+
>
|
|
218
|
+
<RotateCw class="size-3" />
|
|
219
|
+
reset
|
|
220
|
+
</button>
|
|
221
|
+
{/if}
|
|
222
|
+
<div class="h-4 w-px bg-line-1"></div>
|
|
223
|
+
{/if}
|
|
224
|
+
<button
|
|
225
|
+
type="button"
|
|
226
|
+
onclick={() => (filtersOpen = !filtersOpen)}
|
|
227
|
+
aria-pressed={filtersOpen}
|
|
228
|
+
title={filtersOpen ? 'Hide column filters' : 'Show column filters'}
|
|
229
|
+
class={[
|
|
230
|
+
'inline-flex items-center gap-1.5 h-6 px-2 rounded border transition-colors cursor-pointer',
|
|
231
|
+
filtersOpen
|
|
232
|
+
? 'border-accent/40 bg-accent/10 text-accent'
|
|
233
|
+
: 'border-line-1 text-fg-3 hover:text-fg-1 hover:border-line-2',
|
|
234
|
+
].join(' ')}
|
|
235
|
+
>
|
|
236
|
+
<Filter class="size-3" />
|
|
237
|
+
<span>filters</span>
|
|
238
|
+
{#if activeFilterCount > 0}
|
|
239
|
+
<span class="text-[9px] px-1 rounded bg-accent text-white">{activeFilterCount}</span>
|
|
240
|
+
{/if}
|
|
241
|
+
</button>
|
|
242
|
+
{#if activeFilterCount > 0}
|
|
243
|
+
<button
|
|
244
|
+
type="button"
|
|
245
|
+
onclick={clearAllFilters}
|
|
246
|
+
class="inline-flex items-center gap-1 h-6 px-2 rounded border border-line-1 text-fg-3 hover:text-fg-1 hover:border-line-2 cursor-pointer"
|
|
247
|
+
title="Clear all filters"
|
|
248
|
+
>
|
|
249
|
+
<X class="size-3" />
|
|
250
|
+
clear
|
|
251
|
+
</button>
|
|
252
|
+
{/if}
|
|
253
|
+
<div class="ml-auto text-fg-3">
|
|
254
|
+
{rows.length.toLocaleString()}{#if activeFilterCount > 0} of {allRows.length.toLocaleString()}{/if} rows
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
{#if selectOpen && collection}
|
|
259
|
+
<div class="border-b border-line-1 bg-bg-1/20 px-3 py-2 text-[11px] font-mono">
|
|
260
|
+
<div class="mb-2 flex items-center gap-2">
|
|
261
|
+
<span class="text-fg-3">columns:</span>
|
|
262
|
+
<button
|
|
263
|
+
type="button"
|
|
264
|
+
class="rounded border border-line-1 px-1.5 py-0.5 text-fg-3 hover:text-fg-1 hover:border-line-2"
|
|
265
|
+
onclick={() => {
|
|
266
|
+
const next: Record<string, boolean> = {}
|
|
267
|
+
for (const col of queryableColumns) next[col] = true
|
|
268
|
+
selectedColumns = next
|
|
269
|
+
}}
|
|
270
|
+
>
|
|
271
|
+
all
|
|
272
|
+
</button>
|
|
273
|
+
<button
|
|
274
|
+
type="button"
|
|
275
|
+
class="rounded border border-line-1 px-1.5 py-0.5 text-fg-3 hover:text-fg-1 hover:border-line-2"
|
|
276
|
+
onclick={() => (selectedColumns = {})}
|
|
277
|
+
>
|
|
278
|
+
none
|
|
279
|
+
</button>
|
|
280
|
+
<label class="ml-auto inline-flex items-center gap-1.5 text-fg-3">
|
|
281
|
+
limit
|
|
282
|
+
<select
|
|
283
|
+
class="bg-bg-1 text-fg-1 border border-line-1 rounded px-1 py-0.5"
|
|
284
|
+
value={selectLimit}
|
|
285
|
+
onchange={(e) => (selectLimit = Number((e.currentTarget as HTMLSelectElement).value))}
|
|
286
|
+
>
|
|
287
|
+
<option value={50}>50</option>
|
|
288
|
+
<option value={100}>100</option>
|
|
289
|
+
<option value={200}>200</option>
|
|
290
|
+
<option value={500}>500</option>
|
|
291
|
+
<option value={1000}>1k</option>
|
|
292
|
+
</select>
|
|
293
|
+
</label>
|
|
294
|
+
</div>
|
|
295
|
+
<div class="flex flex-wrap gap-1">
|
|
296
|
+
{#each queryableColumns as col (col)}
|
|
297
|
+
<label
|
|
298
|
+
class={[
|
|
299
|
+
'inline-flex h-5 items-center gap-1 rounded border px-1.5 cursor-pointer',
|
|
300
|
+
selectedColumns[col] ? 'border-accent/40 bg-accent/10 text-accent' : 'border-line-1 text-fg-3 hover:text-fg-1',
|
|
301
|
+
].join(' ')}
|
|
302
|
+
>
|
|
303
|
+
<input
|
|
304
|
+
type="checkbox"
|
|
305
|
+
class="sr-only"
|
|
306
|
+
checked={selectedColumns[col] ?? false}
|
|
307
|
+
onchange={(e) => (selectedColumns = { ...selectedColumns, [col]: (e.currentTarget as HTMLInputElement).checked })}
|
|
308
|
+
/>
|
|
309
|
+
{col}
|
|
310
|
+
</label>
|
|
311
|
+
{/each}
|
|
312
|
+
</div>
|
|
313
|
+
<div class="mt-2 flex items-center gap-2 text-fg-3">
|
|
314
|
+
<code class="min-w-0 flex-1 truncate rounded border border-line-1 bg-bg-0 px-2 py-1 text-fg-1">{selectSql() ?? 'No collection selected'}</code>
|
|
315
|
+
{#if selectError}<span class="text-warn">{selectError}</span>{/if}
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
{/if}
|
|
319
|
+
|
|
320
|
+
{#if !activeResult.result.records.length}
|
|
321
|
+
<div class="flex-1 grid place-items-center text-fg-3 text-[12px] font-mono p-6">
|
|
322
|
+
Empty result.
|
|
323
|
+
</div>
|
|
324
|
+
{:else}
|
|
325
|
+
|
|
326
|
+
<div class="flex-1 overflow-auto">
|
|
327
|
+
<table class="w-full border-collapse text-[12px] font-mono">
|
|
328
|
+
<thead class="sticky top-0 bg-bg-1 z-10">
|
|
329
|
+
<tr class="border-b border-line-1 text-fg-3">
|
|
330
|
+
<th class="px-2 py-1.5 text-right w-10 font-normal">#</th>
|
|
331
|
+
{#each columns as c}
|
|
332
|
+
{@const kind = identityKind(c)}
|
|
333
|
+
{@const sys = isSystemField(c)}
|
|
334
|
+
<th
|
|
335
|
+
class={[
|
|
336
|
+
'px-2 py-1.5 text-left font-normal whitespace-nowrap',
|
|
337
|
+
kind === 'rid' ? 'bg-accent/5 text-fg-0' : sys ? 'bg-bg-1 text-fg-2' : kind === 'id' ? 'text-fg-1' : '',
|
|
338
|
+
].join(' ')}
|
|
339
|
+
>
|
|
340
|
+
<span class={kind || sys ? 'font-semibold' : ''}>{c}</span>
|
|
341
|
+
{#if kind === 'rid'}
|
|
342
|
+
<span class="ml-1 text-[9px] px-1 py-0.5 rounded bg-accent text-white uppercase tracking-wider font-semibold" title="System-assigned row id — reddb's canonical address">sys</span>
|
|
343
|
+
{:else if sys}
|
|
344
|
+
<span class="ml-1 text-[9px] px-1 py-0.5 rounded border border-line-2 text-fg-3 uppercase tracking-wider" title="Reserved system field — written by reddb">sys</span>
|
|
345
|
+
{:else if kind === 'id'}
|
|
346
|
+
<span class="ml-1 text-[9px] px-1 py-0.5 rounded border border-line-2 text-fg-2 uppercase tracking-wider" title="User-defined identifier">pk</span>
|
|
347
|
+
{:else if c === pkCol}
|
|
348
|
+
<span class="ml-1 text-[10px] text-fg-3 uppercase tracking-wide" title="Inferred primary key">pk</span>
|
|
349
|
+
{/if}
|
|
350
|
+
</th>
|
|
351
|
+
{/each}
|
|
352
|
+
</tr>
|
|
353
|
+
{#if filtersOpen}
|
|
354
|
+
<tr class="border-b border-line-1 bg-bg-0">
|
|
355
|
+
<th class="px-2 py-1"></th>
|
|
356
|
+
{#each columns as c}
|
|
357
|
+
<th class="px-1 py-1">
|
|
358
|
+
<div class="relative">
|
|
359
|
+
<input
|
|
360
|
+
type="text"
|
|
361
|
+
bind:value={filters[c]}
|
|
362
|
+
placeholder="filter {c}…"
|
|
363
|
+
class="w-full h-6 pl-2 pr-5 bg-bg-1 text-fg-1 border border-line-1 rounded text-[11px] font-mono outline-none focus:border-accent placeholder:text-fg-3"
|
|
364
|
+
/>
|
|
365
|
+
{#if filters[c]?.length}
|
|
366
|
+
<button
|
|
367
|
+
type="button"
|
|
368
|
+
onclick={() => clearFilter(c)}
|
|
369
|
+
aria-label="Clear filter for {c}"
|
|
370
|
+
class="absolute right-1 top-1/2 -translate-y-1/2 text-fg-3 hover:text-fg-1 cursor-pointer"
|
|
371
|
+
>
|
|
372
|
+
<X class="size-3" />
|
|
373
|
+
</button>
|
|
374
|
+
{/if}
|
|
375
|
+
</div>
|
|
376
|
+
</th>
|
|
377
|
+
{/each}
|
|
378
|
+
</tr>
|
|
379
|
+
{/if}
|
|
380
|
+
</thead>
|
|
381
|
+
<tbody>
|
|
382
|
+
{#each rows as row, i (i)}
|
|
383
|
+
<tr class="border-b border-line-1/60 hover:bg-bg-1/40">
|
|
384
|
+
<td class="px-2 py-1 text-right text-fg-3 select-none">{i + 1}</td>
|
|
385
|
+
{#each columns as c}
|
|
386
|
+
{@const v = row[c]}
|
|
387
|
+
{@const display = cellDisplay(i, c, v)}
|
|
388
|
+
{@const isEditing = editing?.row === i && editing.col === c}
|
|
389
|
+
{@const kind = identityKind(c)}
|
|
390
|
+
{@const sys = isSystemField(c)}
|
|
391
|
+
<td
|
|
392
|
+
class={[
|
|
393
|
+
'px-2 py-1 max-w-[260px] truncate align-top',
|
|
394
|
+
kind === 'rid'
|
|
395
|
+
? 'bg-accent/5 text-accent font-semibold'
|
|
396
|
+
: sys
|
|
397
|
+
? 'bg-bg-1 text-fg-2 font-mono'
|
|
398
|
+
: kind === 'id'
|
|
399
|
+
? 'text-fg-0'
|
|
400
|
+
: '',
|
|
401
|
+
display.staged && !isEditing ? 'bg-warn/15 text-fg-1' : '',
|
|
402
|
+
].join(' ')}
|
|
403
|
+
ondblclick={() => startEdit(i, c, v)}
|
|
404
|
+
title={display.staged ? 'Staged change — open Pending Changes to commit' : undefined}
|
|
405
|
+
>
|
|
406
|
+
{#if isEditing}
|
|
407
|
+
<input
|
|
408
|
+
class="w-full bg-bg-2 text-fg-1 border border-accent/60 rounded px-1 py-0.5 outline-none"
|
|
409
|
+
bind:value={editValue}
|
|
410
|
+
onblur={commit}
|
|
411
|
+
use:focusOnMount
|
|
412
|
+
onkeydown={(e) => {
|
|
413
|
+
if (e.key === 'Enter') commit()
|
|
414
|
+
else if (e.key === 'Escape') cancel()
|
|
415
|
+
}}
|
|
416
|
+
/>
|
|
417
|
+
{:else if isNull(display.value)}
|
|
418
|
+
<span class="text-fg-3 italic">NULL</span>
|
|
419
|
+
{:else}
|
|
420
|
+
<span class="block truncate">{formatCell(display.value)}</span>
|
|
421
|
+
{/if}
|
|
422
|
+
</td>
|
|
423
|
+
{/each}
|
|
424
|
+
</tr>
|
|
425
|
+
{/each}
|
|
426
|
+
</tbody>
|
|
427
|
+
</table>
|
|
428
|
+
</div>
|
|
429
|
+
<div class="border-t border-line-1 px-3 py-1.5 text-[11px] font-mono text-fg-3 flex items-center gap-3">
|
|
430
|
+
<span>
|
|
431
|
+
{rows.length} of {activeResult.record_count} rows
|
|
432
|
+
{#if activeFilterCount > 0}· filtered{/if}
|
|
433
|
+
{#if collection}· <span class="text-fg-2">{collection}</span>{/if}
|
|
434
|
+
</span>
|
|
435
|
+
<span>· double-click a cell to stage edit</span>
|
|
436
|
+
{#if pkCol}<span>· pk <code class="text-fg-2">{pkCol}</code></span>{/if}
|
|
437
|
+
</div>
|
|
438
|
+
{/if}
|
|
439
|
+
{/if}
|
|
440
|
+
</div>
|