@bakery-framework/plugin-db-explorer 2.0.0-alpha.5 → 2.0.0-alpha.7
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/package.json +4 -4
- package/src/access.ts +188 -0
- package/src/client/api.ts +279 -0
- package/src/client/bulk.ts +357 -0
- package/src/client/cell.ts +139 -0
- package/src/client/confirm.ts +203 -0
- package/src/client/csv-commit.ts +185 -0
- package/src/client/csv-map.ts +274 -0
- package/src/client/csv-model.ts +420 -0
- package/src/client/csv-pick.ts +54 -0
- package/src/client/csv-preview.ts +89 -0
- package/src/client/csv.ts +104 -0
- package/src/client/dom.ts +164 -0
- package/src/client/edit-session.ts +219 -0
- package/src/client/editors.ts +283 -0
- package/src/client/filter-builder.ts +198 -0
- package/src/client/fk.ts +269 -0
- package/src/client/grid-body.ts +106 -0
- package/src/client/grid-header.ts +65 -0
- package/src/client/grid-rowbar.ts +64 -0
- package/src/client/grid.ts +468 -0
- package/src/client/meta.ts +185 -0
- package/src/client/page.ts +332 -0
- package/src/client/panel.ts +296 -0
- package/src/client/relations.ts +205 -0
- package/src/client/save.ts +205 -0
- package/src/client/sidebar.ts +110 -0
- package/src/client/state.ts +218 -0
- package/src/client/statusbar.ts +130 -0
- package/src/client/structure.ts +234 -0
- package/src/client/tabs.ts +219 -0
- package/src/client/tabstrip.ts +127 -0
- package/src/client.ts +376 -160
- package/src/endpoints/common.ts +122 -0
- package/src/endpoints/graph.ts +148 -0
- package/src/endpoints/import.ts +89 -0
- package/src/endpoints/read.ts +173 -0
- package/src/endpoints/rows.ts +435 -0
- package/src/identity.ts +391 -0
- package/src/index.ts +40 -45
- package/src/policy.ts +45 -0
- package/src/preview.ts +53 -0
- package/src/setup.ts +64 -81
- package/src/shared/coerce.ts +399 -0
- package/src/shared/csv.ts +186 -0
- package/src/shared/filters.ts +200 -0
- package/src/shared/plan.ts +173 -0
- package/src/shell.ts +187 -0
- package/src/validate.ts +295 -0
- package/src/credential.ts +0 -26
- package/src/endpoints.ts +0 -48
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open tables as tabs, with VS Code's preview semantics.
|
|
3
|
+
*
|
|
4
|
+
* **Pure — no DOM.** Every transition is a function from a state to a state, so
|
|
5
|
+
* the rules below are asserted in `tabs.test.ts` rather than inferred from a
|
|
6
|
+
* click handler. `tabstrip.ts` is the view over this.
|
|
7
|
+
*
|
|
8
|
+
* The behaviour is Supabase Studio's, which is VS Code's:
|
|
9
|
+
*
|
|
10
|
+
* - A **single click** opens a *preview* tab — rendered italic, and **replaced
|
|
11
|
+
* in place** by the next single click. Browsing twelve tables therefore
|
|
12
|
+
* leaves one tab open, not twelve, which is the whole point.
|
|
13
|
+
* - **Double-clicking**, or **editing**, makes it permanent. Investment is what
|
|
14
|
+
* promotes a tab; merely looking at it is not.
|
|
15
|
+
* - A table that is already open is *selected* rather than reopened, and its
|
|
16
|
+
* own view state — page, sort, filters — is left exactly as it was. That is
|
|
17
|
+
* the property that makes tabs worth having: switching back restores.
|
|
18
|
+
*
|
|
19
|
+
* There is at most one preview tab, which is what makes "replace the preview"
|
|
20
|
+
* unambiguous.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { ViewState } from './state'
|
|
24
|
+
import { decodeView, encodeView } from './state'
|
|
25
|
+
|
|
26
|
+
export interface Tab {
|
|
27
|
+
view: ViewState
|
|
28
|
+
/** Italic, and replaceable by the next single click. */
|
|
29
|
+
preview: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TabsState {
|
|
33
|
+
tabs: Tab[]
|
|
34
|
+
/** Index into `tabs`, or `-1` for the new-tab page when nothing is open. */
|
|
35
|
+
active: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createTabs(): TabsState {
|
|
39
|
+
return { tabs: [], active: -1 }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function activeTab(state: TabsState): Tab | null {
|
|
43
|
+
return state.tabs[state.active] ?? null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function activeView(state: TabsState): ViewState | null {
|
|
47
|
+
return activeTab(state)?.view ?? null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function indexOfTable(state: TabsState, table: string): number {
|
|
51
|
+
return state.tabs.findIndex(tab => tab.view.table === table)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function previewIndex(state: TabsState): number {
|
|
55
|
+
return state.tabs.findIndex(tab => tab.preview)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Single click: open as preview.
|
|
60
|
+
*
|
|
61
|
+
* Three cases in order, and the order is the behaviour. An already-open table
|
|
62
|
+
* wins over everything — reopening it would throw away the page and filters the
|
|
63
|
+
* user left it on, which is precisely what tabs exist to preserve.
|
|
64
|
+
*/
|
|
65
|
+
export function openPreview(state: TabsState, view: ViewState): TabsState {
|
|
66
|
+
const existing = indexOfTable(state, view.table)
|
|
67
|
+
if (existing >= 0) return { ...state, active: existing }
|
|
68
|
+
|
|
69
|
+
const slot = previewIndex(state)
|
|
70
|
+
const tab: Tab = { view, preview: true }
|
|
71
|
+
if (slot >= 0) {
|
|
72
|
+
const tabs = [...state.tabs]
|
|
73
|
+
tabs[slot] = tab
|
|
74
|
+
return { tabs, active: slot }
|
|
75
|
+
}
|
|
76
|
+
return { tabs: [...state.tabs, tab], active: state.tabs.length }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Double click, or any explicit "open and keep": a permanent tab.
|
|
81
|
+
*
|
|
82
|
+
* An already-open preview tab for the same table is promoted where it stands
|
|
83
|
+
* rather than moved, so the strip does not reorder under the cursor that just
|
|
84
|
+
* double-clicked it.
|
|
85
|
+
*/
|
|
86
|
+
export function openPermanent(state: TabsState, view: ViewState): TabsState {
|
|
87
|
+
const existing = indexOfTable(state, view.table)
|
|
88
|
+
if (existing >= 0) {
|
|
89
|
+
const tabs = [...state.tabs]
|
|
90
|
+
tabs[existing] = { ...tabs[existing]!, preview: false }
|
|
91
|
+
return { tabs, active: existing }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const slot = previewIndex(state)
|
|
95
|
+
const tab: Tab = { view, preview: false }
|
|
96
|
+
if (slot >= 0) {
|
|
97
|
+
const tabs = [...state.tabs]
|
|
98
|
+
tabs[slot] = tab
|
|
99
|
+
return { tabs, active: slot }
|
|
100
|
+
}
|
|
101
|
+
return { tabs: [...state.tabs, tab], active: state.tabs.length }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function selectTab(state: TabsState, index: number): TabsState {
|
|
105
|
+
if (index < 0 || index >= state.tabs.length) return state
|
|
106
|
+
return { ...state, active: index }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Close one tab.
|
|
111
|
+
*
|
|
112
|
+
* Closing the active tab activates whatever slid into its slot — the tab to the
|
|
113
|
+
* right — and the last tab when there is nothing to the right. Closing anything
|
|
114
|
+
* left of the active one shifts the active index down so the *same* tab stays
|
|
115
|
+
* selected, which is the bug the naive version has.
|
|
116
|
+
*/
|
|
117
|
+
export function closeTab(state: TabsState, index: number): TabsState {
|
|
118
|
+
if (index < 0 || index >= state.tabs.length) return state
|
|
119
|
+
const tabs = state.tabs.filter((_tab, at) => at !== index)
|
|
120
|
+
if (!tabs.length) return { tabs, active: -1 }
|
|
121
|
+
|
|
122
|
+
let active = state.active
|
|
123
|
+
if (index < state.active) active = state.active - 1
|
|
124
|
+
else if (index === state.active) active = Math.min(index, tabs.length - 1)
|
|
125
|
+
return { tabs, active }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** What a real edit does to a preview tab. Idempotent, and a no-op when none. */
|
|
129
|
+
export function promoteActive(state: TabsState): TabsState {
|
|
130
|
+
const tab = activeTab(state)
|
|
131
|
+
if (!tab?.preview) return state
|
|
132
|
+
const tabs = [...state.tabs]
|
|
133
|
+
tabs[state.active] = { ...tab, preview: false }
|
|
134
|
+
return { ...state, tabs }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* A new view for the active tab, keeping its preview flag.
|
|
139
|
+
*
|
|
140
|
+
* Paging and sorting inside a preview tab deliberately do **not** promote it:
|
|
141
|
+
* looking harder at a table is still looking. Only `promoteActive` promotes.
|
|
142
|
+
*/
|
|
143
|
+
export function replaceActiveView(
|
|
144
|
+
state: TabsState,
|
|
145
|
+
view: ViewState,
|
|
146
|
+
): TabsState {
|
|
147
|
+
const tab = activeTab(state)
|
|
148
|
+
if (!tab) return openPermanent(state, view)
|
|
149
|
+
const tabs = [...state.tabs]
|
|
150
|
+
tabs[state.active] = { ...tab, view }
|
|
151
|
+
return { ...state, tabs }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Drop tabs whose table no longer exists.
|
|
156
|
+
*
|
|
157
|
+
* A link can name anything, and a schema can change between two visits. A tab
|
|
158
|
+
* pointing at a table that is gone would render "Pick a table" forever with no
|
|
159
|
+
* way to tell why.
|
|
160
|
+
*/
|
|
161
|
+
export function pruneTabs(
|
|
162
|
+
state: TabsState,
|
|
163
|
+
known: ReadonlySet<string>,
|
|
164
|
+
): TabsState {
|
|
165
|
+
const keep = state.tabs.filter(tab => known.has(tab.view.table))
|
|
166
|
+
if (keep.length === state.tabs.length) return state
|
|
167
|
+
const wanted = activeTab(state)
|
|
168
|
+
const active = wanted ? keep.indexOf(wanted) : -1
|
|
169
|
+
return { tabs: keep, active: active >= 0 ? active : keep.length - 1 }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The whole strip in a hash.
|
|
174
|
+
*
|
|
175
|
+
* `x` repeats, one per tab, each holding a whole encoded view — `URLSearchParams`
|
|
176
|
+
* escapes the inner `&` on the way in and unescapes it on the way out, so the
|
|
177
|
+
* nesting needs no encoding of its own. `a` is the active index and `w` the
|
|
178
|
+
* preview one; both are indexes rather than flags because there is at most one
|
|
179
|
+
* of each and an index is shorter than a marker on every tab.
|
|
180
|
+
*/
|
|
181
|
+
export function encodeTabs(state: TabsState): string {
|
|
182
|
+
const params = new URLSearchParams()
|
|
183
|
+
for (const tab of state.tabs) params.append('x', encodeView(tab.view))
|
|
184
|
+
if (state.active >= 0) params.set('a', String(state.active))
|
|
185
|
+
const preview = previewIndex(state)
|
|
186
|
+
if (preview >= 0) params.set('w', String(preview))
|
|
187
|
+
return params.toString()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function decodeTabs(hash: string): TabsState {
|
|
191
|
+
const params = new URLSearchParams(hash.replace(/^#/, ''))
|
|
192
|
+
const encoded = params.getAll('x')
|
|
193
|
+
|
|
194
|
+
// A pre-tabs link — `#t=parcels&p=2` — is one permanent tab. Those links are
|
|
195
|
+
// in chat logs and bookmarks, and silently rendering nothing for them would
|
|
196
|
+
// be the most visible possible regression.
|
|
197
|
+
if (!encoded.length) {
|
|
198
|
+
const single = decodeView(hash)
|
|
199
|
+
if (!single.table) return createTabs()
|
|
200
|
+
return { tabs: [{ view: single, preview: false }], active: 0 }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const tabs: Tab[] = encoded.map(part => ({
|
|
204
|
+
view: decodeView(part),
|
|
205
|
+
preview: false,
|
|
206
|
+
}))
|
|
207
|
+
const preview = readIndex(params.get('w'), tabs.length)
|
|
208
|
+
if (preview >= 0) tabs[preview]!.preview = true
|
|
209
|
+
|
|
210
|
+
const active = readIndex(params.get('a'), tabs.length)
|
|
211
|
+
return { tabs, active: active >= 0 ? active : tabs.length - 1 }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** An index from a hand-editable URL, or `-1`. Never out of range. */
|
|
215
|
+
function readIndex(raw: string | null, length: number): number {
|
|
216
|
+
if (raw === null) return -1
|
|
217
|
+
const value = Number.parseInt(raw, 10)
|
|
218
|
+
return Number.isFinite(value) && value >= 0 && value < length ? value : -1
|
|
219
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tab strip, and the view switcher under it.
|
|
3
|
+
*
|
|
4
|
+
* Two rows, and **exactly two** — the table tabs, then Data / Structure /
|
|
5
|
+
* Relations for whichever table is active. Beekeeper sells itself on not having
|
|
6
|
+
* "tabs within tabs", and it is right: one level of nesting is navigable and
|
|
7
|
+
* two is a maze. Nothing below this line gets its own strip.
|
|
8
|
+
*
|
|
9
|
+
* All the state lives in `tabs.ts`; this only renders it and reports clicks.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { box, button, each, el, on } from './dom'
|
|
13
|
+
import type { TableView } from './state'
|
|
14
|
+
import type { TabsState } from './tabs'
|
|
15
|
+
|
|
16
|
+
export interface TabStripContext {
|
|
17
|
+
tabs: TabsState
|
|
18
|
+
onSelect: (index: number) => void
|
|
19
|
+
/** Double-click on a preview tab: keep it. */
|
|
20
|
+
onPromote: (index: number) => void
|
|
21
|
+
onClose: (index: number) => void
|
|
22
|
+
/** The `+` button — back to the table picker. */
|
|
23
|
+
onNew: () => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function renderTabStrip(ctx: TabStripContext): HTMLElement {
|
|
27
|
+
const strip = box('tabstrip')
|
|
28
|
+
strip.setAttribute('role', 'tablist')
|
|
29
|
+
each(strip, ctx.tabs.tabs, (_tab, index) => tabNode(ctx, index))
|
|
30
|
+
strip.appendChild(
|
|
31
|
+
button('+', ctx.onNew, {
|
|
32
|
+
class: 'tab-new',
|
|
33
|
+
title: 'open another table',
|
|
34
|
+
attrs: { 'aria-label': 'open another table' },
|
|
35
|
+
}),
|
|
36
|
+
)
|
|
37
|
+
return strip
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function tabNode(ctx: TabStripContext, index: number): HTMLElement {
|
|
41
|
+
const tab = ctx.tabs.tabs[index]!
|
|
42
|
+
const node = box('tab')
|
|
43
|
+
const active = index === ctx.tabs.active
|
|
44
|
+
node.classList.toggle('active', active)
|
|
45
|
+
// Italic is the whole visual language of a preview tab, borrowed from VS
|
|
46
|
+
// Code because that is where the user learned it.
|
|
47
|
+
node.classList.toggle('preview', tab.preview)
|
|
48
|
+
node.setAttribute('role', 'tab')
|
|
49
|
+
node.setAttribute('aria-selected', active ? 'true' : 'false')
|
|
50
|
+
|
|
51
|
+
const label = button(tab.view.table, () => ctx.onSelect(index), {
|
|
52
|
+
class: 'tab-label',
|
|
53
|
+
title: tab.preview ? `${tab.view.table} — preview` : tab.view.table,
|
|
54
|
+
})
|
|
55
|
+
on(label, 'dblclick', () => ctx.onPromote(index))
|
|
56
|
+
node.appendChild(label)
|
|
57
|
+
node.appendChild(closeButton(ctx, index, tab.view.table))
|
|
58
|
+
|
|
59
|
+
// Middle-click closes, which is what every browser and every editor has
|
|
60
|
+
// trained people to expect. `auxclick` rather than `mousedown` so a middle
|
|
61
|
+
// *drag* that ends elsewhere does not close anything.
|
|
62
|
+
on(node, 'auxclick', event => {
|
|
63
|
+
if ((event as MouseEvent).button !== 1) return
|
|
64
|
+
event.preventDefault()
|
|
65
|
+
ctx.onClose(index)
|
|
66
|
+
})
|
|
67
|
+
return node
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function closeButton(
|
|
71
|
+
ctx: TabStripContext,
|
|
72
|
+
index: number,
|
|
73
|
+
table: string,
|
|
74
|
+
): HTMLElement {
|
|
75
|
+
return button('×', () => ctx.onClose(index), {
|
|
76
|
+
class: 'tab-close',
|
|
77
|
+
title: `close ${table}`,
|
|
78
|
+
attrs: { 'aria-label': `close ${table}` },
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ------------------------------------------------------------ the view switch
|
|
83
|
+
|
|
84
|
+
const VIEWS: readonly { value: TableView; label: string }[] = [
|
|
85
|
+
{ value: 'data', label: 'Data' },
|
|
86
|
+
{ value: 'structure', label: 'Structure' },
|
|
87
|
+
{ value: 'relations', label: 'Relations' },
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
export interface ViewTabsContext {
|
|
91
|
+
current: TableView
|
|
92
|
+
onSelect: (view: TableView) => void
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Data / Structure / Relations.
|
|
97
|
+
*
|
|
98
|
+
* Structure is separate from data rather than folded into a header panel,
|
|
99
|
+
* which is what DBeaver, DataGrip and Devart all do and what makes "what is
|
|
100
|
+
* this column" answerable without disturbing the grid you were reading.
|
|
101
|
+
*/
|
|
102
|
+
export function renderViewTabs(ctx: ViewTabsContext): HTMLElement {
|
|
103
|
+
const strip = box('viewtabs')
|
|
104
|
+
strip.setAttribute('role', 'tablist')
|
|
105
|
+
each(strip, VIEWS, entry => {
|
|
106
|
+
const active = entry.value === ctx.current
|
|
107
|
+
const node = button(entry.label, () => ctx.onSelect(entry.value), {
|
|
108
|
+
class: active ? 'viewtab active' : 'viewtab',
|
|
109
|
+
attrs: { role: 'tab', 'aria-selected': active ? 'true' : 'false' },
|
|
110
|
+
})
|
|
111
|
+
return node
|
|
112
|
+
})
|
|
113
|
+
return strip
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The `+` page: no table open, so say what to do rather than showing nothing. */
|
|
117
|
+
export function renderNewTabPage(): HTMLElement {
|
|
118
|
+
const node = box('newtab')
|
|
119
|
+
node.appendChild(el('h2', { text: 'No table open' }))
|
|
120
|
+
node.appendChild(
|
|
121
|
+
el('p', {
|
|
122
|
+
class: 'note',
|
|
123
|
+
text: 'Click a table to preview it. Double-click to keep it open.',
|
|
124
|
+
}),
|
|
125
|
+
)
|
|
126
|
+
return node
|
|
127
|
+
}
|