@bakery-framework/plugin-dashboard 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/client/dashboard.ts +2 -65
- package/src/client/parts/effects.ts +2 -2
- package/src/client/parts/stats.ts +105 -23
- package/src/client/parts/utils.ts +13 -40
- package/src/components/DBBrowser.tsx +41 -352
- package/src/setup.ts +28 -14
- package/src/shell.tsx +98 -22
- package/src/client/parts/database.ts +0 -1176
- package/src/endpoints/database.ts +0 -211
|
@@ -1,362 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The Database tab is a signpost, not a browser.
|
|
3
|
+
*
|
|
4
|
+
* This panel used to be the grid editor and a raw SQL prompt, backed by
|
|
5
|
+
* `POST /api/_dashboard/query` and `POST /api/_dashboard/execute-action` and
|
|
6
|
+
* gated by a single environment flag. Both endpoints are gone.
|
|
7
|
+
* `@bakery-framework/plugin-db-explorer` does the same work at `/_db` with an
|
|
8
|
+
* access level per caller instead, so the console points at it rather than
|
|
9
|
+
* shipping a second, weaker copy.
|
|
10
|
+
*
|
|
11
|
+
* Rendered **only when nothing serves `/_db`**. With the explorer mounted the
|
|
12
|
+
* nav entry links straight to it and this panel never appears — a tab whose
|
|
13
|
+
* whole content is "go there" is a click of ceremony in front of going there.
|
|
14
|
+
* Without it, this is what explains where the editor went.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately static: no fetch, no client module, nothing to escape. It is
|
|
17
|
+
* plain markup so that a tab which now only links somewhere cannot grow a
|
|
18
|
+
* data path back by accident.
|
|
19
|
+
*/
|
|
9
20
|
export function renderDatabaseBrowser() {
|
|
10
|
-
const driverName = driverNames[connection.driver] || 'Database'
|
|
11
21
|
return (
|
|
12
22
|
<div id="panel-database" class="panel">
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
`}</style>
|
|
21
|
-
<div class="db-mobile-toggle">
|
|
22
|
-
<h2 class="db-browser-mobile-title">
|
|
23
|
-
<iconify-icon icon="lucide:database"></iconify-icon>
|
|
24
|
-
Database Explorer
|
|
25
|
-
</h2>
|
|
26
|
-
<button
|
|
27
|
-
type="button"
|
|
28
|
-
class="btn btn-secondary"
|
|
29
|
-
onclick="document.getElementById('db-sidebar').classList.toggle('mobile-open')">
|
|
30
|
-
<iconify-icon icon="lucide:menu"></iconify-icon>
|
|
31
|
-
<span>Tables Menu</span>
|
|
32
|
-
</button>
|
|
33
|
-
</div>
|
|
34
|
-
<div class="db-container">
|
|
35
|
-
{/* Left Sidebar: Discovered Tables & Schema Details */}
|
|
36
|
-
<div id="db-sidebar" class="sidebar glass-effect">
|
|
37
|
-
<div class="sidebar-scroll-container">
|
|
38
|
-
<div class="sidebar-search">
|
|
39
|
-
<input
|
|
40
|
-
type="text"
|
|
41
|
-
id="db-table-search"
|
|
42
|
-
class="search-input"
|
|
43
|
-
placeholder="Search tables..."
|
|
44
|
-
oninput="filterTablesList()"
|
|
45
|
-
/>
|
|
46
|
-
</div>
|
|
47
|
-
<div class="sidebar-title">
|
|
48
|
-
<span>DISCOVERED TABLES</span>
|
|
49
|
-
<span id="tables-count">(0)</span>
|
|
50
|
-
</div>
|
|
51
|
-
<ul class="table-list" id="tables-list">
|
|
52
|
-
<li class="results-empty">Scanning schema...</li>
|
|
53
|
-
</ul>
|
|
23
|
+
<div class="browser-card glass-effect">
|
|
24
|
+
<div class="browser-header">
|
|
25
|
+
<div class="table-info">
|
|
26
|
+
<h2>
|
|
27
|
+
<iconify-icon icon="lucide:database"></iconify-icon>
|
|
28
|
+
<span>Database</span>
|
|
29
|
+
</h2>
|
|
54
30
|
</div>
|
|
55
31
|
</div>
|
|
56
32
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
0 rows
|
|
69
|
-
</span>
|
|
70
|
-
</div>
|
|
71
|
-
<div class="actions-group">
|
|
72
|
-
<button
|
|
73
|
-
type="button"
|
|
74
|
-
class="btn btn-primary"
|
|
75
|
-
onclick="openInsertModal()">
|
|
76
|
-
<iconify-icon icon="lucide:plus"></iconify-icon>
|
|
77
|
-
<span>Add Row</span>
|
|
78
|
-
</button>
|
|
79
|
-
<button
|
|
80
|
-
type="button"
|
|
81
|
-
class="btn btn-secondary"
|
|
82
|
-
onclick="openImportModal()">
|
|
83
|
-
<iconify-icon icon="lucide:file-up"></iconify-icon>
|
|
84
|
-
<span>Import CSV</span>
|
|
85
|
-
</button>
|
|
86
|
-
<div class="export-dropdown-wrapper">
|
|
87
|
-
<button
|
|
88
|
-
type="button"
|
|
89
|
-
class="btn btn-secondary"
|
|
90
|
-
onclick="toggleExportMenu()">
|
|
91
|
-
<iconify-icon icon="lucide:download"></iconify-icon>
|
|
92
|
-
<span>Export ▾</span>
|
|
93
|
-
</button>
|
|
94
|
-
<div id="export-menu" class="export-menu">
|
|
95
|
-
<button type="button" onclick="exportToCSV()">
|
|
96
|
-
Export to CSV
|
|
97
|
-
</button>
|
|
98
|
-
<button type="button" onclick="exportToJSON()">
|
|
99
|
-
Export to JSON
|
|
100
|
-
</button>
|
|
101
|
-
</div>
|
|
102
|
-
</div>
|
|
103
|
-
<button
|
|
104
|
-
type="button"
|
|
105
|
-
class="btn btn-secondary btn-danger"
|
|
106
|
-
onclick="truncateCurrentTable()">
|
|
107
|
-
<iconify-icon icon="lucide:alert-triangle"></iconify-icon>
|
|
108
|
-
<span>Truncate</span>
|
|
109
|
-
</button>
|
|
110
|
-
</div>
|
|
111
|
-
</div>
|
|
112
|
-
|
|
113
|
-
{/* Filter Query Builder Bar */}
|
|
114
|
-
<div class="filter-builder-card glass-effect">
|
|
115
|
-
<div class="filter-builder-row">
|
|
116
|
-
<span class="filter-label">
|
|
117
|
-
<iconify-icon icon="lucide:filter"></iconify-icon>
|
|
118
|
-
<span>Filters</span>
|
|
119
|
-
</span>
|
|
120
|
-
<select id="filter-col-select" class="filter-select">
|
|
121
|
-
<option value="">-- Choose Column --</option>
|
|
122
|
-
</select>
|
|
123
|
-
<select id="filter-op-select" class="filter-select">
|
|
124
|
-
<option value="like">contains</option>
|
|
125
|
-
<option value="=">equals</option>
|
|
126
|
-
<option value=">">></option>
|
|
127
|
-
<option value="<"><</option>
|
|
128
|
-
<option value="is_null">is null</option>
|
|
129
|
-
<option value="is_not_null">is not null</option>
|
|
130
|
-
</select>
|
|
131
|
-
<input
|
|
132
|
-
type="text"
|
|
133
|
-
id="filter-val-input"
|
|
134
|
-
class="filter-input"
|
|
135
|
-
placeholder="Filter value..."
|
|
136
|
-
/>
|
|
137
|
-
<button
|
|
138
|
-
type="button"
|
|
139
|
-
class="btn btn-secondary"
|
|
140
|
-
onclick="addActiveFilter()">
|
|
141
|
-
Apply
|
|
142
|
-
</button>
|
|
143
|
-
<button
|
|
144
|
-
type="button"
|
|
145
|
-
class="btn btn-secondary"
|
|
146
|
-
onclick="clearActiveFilters()">
|
|
147
|
-
Clear All
|
|
148
|
-
</button>
|
|
149
|
-
</div>
|
|
150
|
-
<div id="active-filters-list" class="active-filters-list">
|
|
151
|
-
{/* Dynamically generated filter chips */}
|
|
152
|
-
</div>
|
|
153
|
-
</div>
|
|
154
|
-
|
|
155
|
-
{/* Pagination Controls */}
|
|
156
|
-
<div class="pagination-bar">
|
|
157
|
-
<div class="page-size-selector">
|
|
158
|
-
<span>Show</span>
|
|
159
|
-
<select
|
|
160
|
-
id="db-page-size"
|
|
161
|
-
class="filter-select"
|
|
162
|
-
onchange="changePageSize()">
|
|
163
|
-
<option value="10">10 rows</option>
|
|
164
|
-
<option value="25">25 rows</option>
|
|
165
|
-
<option value="50" selected>
|
|
166
|
-
50 rows
|
|
167
|
-
</option>
|
|
168
|
-
<option value="100">100 rows</option>
|
|
169
|
-
<option value="500">500 rows</option>
|
|
170
|
-
</select>
|
|
171
|
-
</div>
|
|
172
|
-
<div class="page-nav">
|
|
173
|
-
<button
|
|
174
|
-
type="button"
|
|
175
|
-
id="btn-page-prev"
|
|
176
|
-
class="btn btn-secondary"
|
|
177
|
-
onclick="prevPage()">
|
|
178
|
-
<iconify-icon icon="lucide:chevron-left"></iconify-icon>
|
|
179
|
-
<span>Prev</span>
|
|
180
|
-
</button>
|
|
181
|
-
<span id="db-page-info">Page 1 of 1</span>
|
|
182
|
-
<button
|
|
183
|
-
type="button"
|
|
184
|
-
id="btn-page-next"
|
|
185
|
-
class="btn btn-secondary"
|
|
186
|
-
onclick="nextPage()">
|
|
187
|
-
<span>Next</span>
|
|
188
|
-
<iconify-icon icon="lucide:chevron-right"></iconify-icon>
|
|
189
|
-
</button>
|
|
190
|
-
</div>
|
|
191
|
-
<div class="rows-meta">
|
|
192
|
-
<span id="db-rows-meta">0 rows matching filters</span>
|
|
193
|
-
</div>
|
|
194
|
-
</div>
|
|
195
|
-
|
|
196
|
-
{/* Interactive Data Table Grid */}
|
|
197
|
-
<div class="results-card glass-effect">
|
|
198
|
-
<div id="browser-grid-body">
|
|
199
|
-
<div class="results-empty">
|
|
200
|
-
<span>Loading table data...</span>
|
|
201
|
-
</div>
|
|
202
|
-
</div>
|
|
203
|
-
</div>
|
|
204
|
-
</div>
|
|
205
|
-
|
|
206
|
-
{/* SQL Terminal Console */}
|
|
207
|
-
<div class="editor-card glass-effect">
|
|
208
|
-
<div class="sql-terminal-header">
|
|
209
|
-
<h2>{driverName} SQL Terminal</h2>
|
|
210
|
-
<span class="console-mode-badge">Read/Write Mode Enabled</span>
|
|
211
|
-
</div>
|
|
212
|
-
<textarea
|
|
213
|
-
class="sql-textarea"
|
|
214
|
-
id="sql-query"
|
|
215
|
-
placeholder="SELECT * FROM users LIMIT 10;"
|
|
216
|
-
/>
|
|
217
|
-
<div class="actions-row">
|
|
218
|
-
<span class="sql-terminal-help">
|
|
219
|
-
Supports SELECT, UPDATE, INSERT, DELETE, and administrative
|
|
220
|
-
statements.
|
|
221
|
-
</span>
|
|
222
|
-
<button
|
|
223
|
-
type="button"
|
|
224
|
-
class="btn sql-terminal-run"
|
|
225
|
-
onclick="runQuery()">
|
|
226
|
-
<iconify-icon icon="lucide:play"></iconify-icon>
|
|
227
|
-
<span>Run SQL Command</span>
|
|
228
|
-
</button>
|
|
229
|
-
</div>
|
|
230
|
-
</div>
|
|
231
|
-
|
|
232
|
-
{/* SQL Terminal Output */}
|
|
233
|
-
<div class="results-card glass-effect" id="sql-console-results-card">
|
|
234
|
-
<div class="results-header">
|
|
235
|
-
<span>SQL TERMINAL OUTPUT</span>
|
|
236
|
-
<span id="results-meta"></span>
|
|
237
|
-
</div>
|
|
238
|
-
<div id="results-body">
|
|
239
|
-
<div class="results-empty">
|
|
240
|
-
<span>
|
|
241
|
-
No SQL query executed yet. Write a query above and click "Run
|
|
242
|
-
SQL Command".
|
|
243
|
-
</span>
|
|
244
|
-
</div>
|
|
245
|
-
</div>
|
|
246
|
-
</div>
|
|
247
|
-
</div>
|
|
248
|
-
</div>
|
|
249
|
-
|
|
250
|
-
{/* Dynamic Insert Modal */}
|
|
251
|
-
<div id="modal-insert" class="modal-overlay">
|
|
252
|
-
<div class="modal-card">
|
|
253
|
-
<div class="modal-header">
|
|
254
|
-
<h3>Add New Row</h3>
|
|
255
|
-
<button
|
|
256
|
-
type="button"
|
|
257
|
-
class="modal-close"
|
|
258
|
-
onclick="closeInsertModal()">
|
|
259
|
-
×
|
|
260
|
-
</button>
|
|
261
|
-
</div>
|
|
262
|
-
<form id="insert-row-form" onsubmit="submitInsertRow(event)">
|
|
263
|
-
<div id="insert-fields-container" class="modal-fields-container">
|
|
264
|
-
{/* Dynamically generated form fields */}
|
|
265
|
-
</div>
|
|
266
|
-
<div class="modal-actions">
|
|
267
|
-
<button
|
|
268
|
-
type="button"
|
|
269
|
-
class="btn btn-secondary"
|
|
270
|
-
onclick="closeInsertModal()">
|
|
271
|
-
Cancel
|
|
272
|
-
</button>
|
|
273
|
-
<button type="submit" class="btn btn-primary">
|
|
274
|
-
Add Row
|
|
275
|
-
</button>
|
|
276
|
-
</div>
|
|
277
|
-
</form>
|
|
278
|
-
</div>
|
|
279
|
-
</div>
|
|
280
|
-
|
|
281
|
-
{/* Dynamic Edit Modal */}
|
|
282
|
-
<div id="modal-edit" class="modal-overlay">
|
|
283
|
-
<div class="modal-card">
|
|
284
|
-
<div class="modal-header">
|
|
285
|
-
<h3>Edit Row Details</h3>
|
|
286
|
-
<button
|
|
287
|
-
type="button"
|
|
288
|
-
class="modal-close"
|
|
289
|
-
onclick="closeEditModal()">
|
|
290
|
-
×
|
|
291
|
-
</button>
|
|
292
|
-
</div>
|
|
293
|
-
<form id="edit-row-form" onsubmit="submitEditRow(event)">
|
|
294
|
-
<input type="hidden" id="edit-row-rowid" />
|
|
295
|
-
<div id="edit-fields-container" class="modal-fields-container">
|
|
296
|
-
{/* Dynamically generated form fields */}
|
|
297
|
-
</div>
|
|
298
|
-
<div class="modal-actions">
|
|
299
|
-
<button
|
|
300
|
-
type="button"
|
|
301
|
-
class="btn btn-secondary"
|
|
302
|
-
onclick="closeEditModal()">
|
|
303
|
-
Cancel
|
|
304
|
-
</button>
|
|
305
|
-
<button type="submit" class="btn btn-primary">
|
|
306
|
-
Save Changes
|
|
307
|
-
</button>
|
|
308
|
-
</div>
|
|
309
|
-
</form>
|
|
310
|
-
</div>
|
|
311
|
-
</div>
|
|
312
|
-
|
|
313
|
-
{/* CSV Import Modal */}
|
|
314
|
-
<div id="modal-import" class="modal-overlay">
|
|
315
|
-
<div class="modal-card">
|
|
316
|
-
<div class="modal-header">
|
|
317
|
-
<h3>
|
|
318
|
-
<iconify-icon icon="lucide:file-up"></iconify-icon>
|
|
319
|
-
<span>Bulk Import CSV Data</span>
|
|
320
|
-
</h3>
|
|
321
|
-
<button
|
|
322
|
-
type="button"
|
|
323
|
-
class="modal-close"
|
|
324
|
-
onclick="closeImportModal()">
|
|
325
|
-
×
|
|
326
|
-
</button>
|
|
327
|
-
</div>
|
|
328
|
-
<div class="modal-fields-container">
|
|
329
|
-
<p>
|
|
330
|
-
Paste CSV data below. The first row must contain column headers
|
|
331
|
-
matching the table schema. Data values are automatically parsed.
|
|
332
|
-
</p>
|
|
333
|
-
<textarea
|
|
334
|
-
id="csv-import-textarea"
|
|
335
|
-
class="sql-textarea"
|
|
336
|
-
placeholder="username,email alice,alice@example.com bob,bob@example.com"></textarea>
|
|
337
|
-
<div class="import-upload-block">
|
|
338
|
-
<span>Or upload a .csv file:</span>
|
|
339
|
-
<input
|
|
340
|
-
type="file"
|
|
341
|
-
id="csv-file-input"
|
|
342
|
-
accept=".csv"
|
|
343
|
-
onchange="handleCsvFileSelect(event)"
|
|
344
|
-
/>
|
|
345
|
-
</div>
|
|
346
|
-
</div>
|
|
33
|
+
<div class="modal-fields-container">
|
|
34
|
+
<p>
|
|
35
|
+
The database browser and editor moved to the Database Explorer
|
|
36
|
+
plugin, served at <code>/_db</code>. It browses tables, filters and
|
|
37
|
+
edits rows, and does not accept raw SQL.
|
|
38
|
+
</p>
|
|
39
|
+
<p>
|
|
40
|
+
This console no longer reads or writes table data. If{' '}
|
|
41
|
+
<code>/_db</code> is not reachable, the explorer plugin is not
|
|
42
|
+
registered in your <code>server.config.ts</code>.
|
|
43
|
+
</p>
|
|
347
44
|
<div class="modal-actions">
|
|
348
|
-
<
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
Cancel
|
|
353
|
-
</button>
|
|
354
|
-
<button
|
|
355
|
-
type="button"
|
|
356
|
-
class="btn btn-primary"
|
|
357
|
-
onclick="submitImportCsv()">
|
|
358
|
-
Import Rows
|
|
359
|
-
</button>
|
|
45
|
+
<a class="btn btn-primary" href="/_db">
|
|
46
|
+
<iconify-icon icon="lucide:external-link"></iconify-icon>
|
|
47
|
+
<span>Open Database Explorer</span>
|
|
48
|
+
</a>
|
|
360
49
|
</div>
|
|
361
50
|
</div>
|
|
362
51
|
</div>
|
package/src/setup.ts
CHANGED
|
@@ -26,12 +26,6 @@ import {
|
|
|
26
26
|
// the tree, allow-listed by name in `tests/conventions.test.ts`.
|
|
27
27
|
import { setupAnalytics } from '@bakery-framework/plugin-analytics/setup'
|
|
28
28
|
import { isAnalyticsAuthorized } from '@bakery-framework/plugin-analytics/stats'
|
|
29
|
-
import {
|
|
30
|
-
handleExecuteAction,
|
|
31
|
-
handleQuery,
|
|
32
|
-
handleSchema,
|
|
33
|
-
handleTableData,
|
|
34
|
-
} from './endpoints/database'
|
|
35
29
|
import {
|
|
36
30
|
handleDeleteSession,
|
|
37
31
|
handleGetSessions,
|
|
@@ -209,9 +203,14 @@ async function checkAuthMiddleware(req: Request, path: string) {
|
|
|
209
203
|
*
|
|
210
204
|
* Necessary but **not sufficient on its own**. `SAFE_METHODS` exempts GET, and
|
|
211
205
|
* `processBody` reads a GET's query string as the body, so a plain
|
|
212
|
-
* `<img src="/api/_dashboard/
|
|
213
|
-
*
|
|
214
|
-
*
|
|
206
|
+
* `<img src="/api/_dashboard/sessions/delete?id=victim">` sails past this
|
|
207
|
+
* guard. The mutating keys in the table below are method-qualified for exactly
|
|
208
|
+
* that reason; neither half closes the hole alone.
|
|
209
|
+
*
|
|
210
|
+
* The example used to be `execute-action?action=truncate`, which was the worst
|
|
211
|
+
* of them — a link that emptied a table. That endpoint is gone with the grid
|
|
212
|
+
* editor, and the two session routes are what is left to protect. The hazard
|
|
213
|
+
* is unchanged; only the blast radius shrank.
|
|
215
214
|
*/
|
|
216
215
|
function checkCsrfMiddleware(req: Request, url: URL): DashboardResponse | null {
|
|
217
216
|
const reason = checkCsrf(req, url)
|
|
@@ -241,10 +240,6 @@ const dashboardRoutes = {
|
|
|
241
240
|
'/api/_dashboard/sessions': (_req, url) => handleGetSessions(url),
|
|
242
241
|
'POST /api/_dashboard/sessions/delete': req => handleDeleteSession(req),
|
|
243
242
|
'POST /api/_dashboard/sessions/update': req => handleUpdateSession(req),
|
|
244
|
-
'/api/_dashboard/schema': () => handleSchema(),
|
|
245
|
-
'/api/_dashboard/table-data': (_req, url) => handleTableData(url),
|
|
246
|
-
'POST /api/_dashboard/query': req => handleQuery(req),
|
|
247
|
-
'POST /api/_dashboard/execute-action': req => handleExecuteAction(req),
|
|
248
243
|
} satisfies PluginRouteTable
|
|
249
244
|
|
|
250
245
|
const dispatchDashboardRoute = routeTable(dashboardRoutes)
|
|
@@ -279,5 +274,24 @@ export async function handleDashboardRequest(
|
|
|
279
274
|
const csrfBlock = checkCsrfMiddleware(req, url)
|
|
280
275
|
if (csrfBlock) return csrfBlock
|
|
281
276
|
|
|
282
|
-
|
|
277
|
+
const result = await dispatchDashboardRoute(req, url)
|
|
278
|
+
if (result) return result
|
|
279
|
+
|
|
280
|
+
// `dispatch` answers `null` for a path no key matched, and a handler
|
|
281
|
+
// returning `null` means "not mine" — which core turns into **204 No
|
|
282
|
+
// Content**. Every unmatched request under this namespace therefore answered
|
|
283
|
+
// 204: a status that reads as success and carries nothing.
|
|
284
|
+
//
|
|
285
|
+
// Retiring the write endpoints is what made that matter. A script still
|
|
286
|
+
// posting to `/api/_dashboard/execute-action` was told 204, took it for
|
|
287
|
+
// success, and silently changed nothing. For a route that has been deleted
|
|
288
|
+
// that is the worst available answer.
|
|
289
|
+
//
|
|
290
|
+
// **Only under `/api/`, and that boundary is load-bearing.** The stylesheet
|
|
291
|
+
// is served by `mountRoutes`, not by a key in the table above, so `null` is
|
|
292
|
+
// precisely how `/_dashboard/style.css` *falls through* to the static
|
|
293
|
+
// pipeline. A blanket 404 here claims it and the console loads unstyled —
|
|
294
|
+
// which is what happened when this was first written without the condition.
|
|
295
|
+
if (path.startsWith('/api/')) return response.error('Not Found', 404)
|
|
296
|
+
return null
|
|
283
297
|
}
|
package/src/shell.tsx
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { getFrameworkVersion } from '@bakery-framework/core'
|
|
2
|
+
import { Bakery } from '@bakery-framework/core/core/bakery'
|
|
3
|
+
import { Try } from '@bakery-framework/core/utils/common'
|
|
1
4
|
import { renderDatabaseBrowser } from './components/DBBrowser'
|
|
2
5
|
import { renderLogsPanel } from './components/LogsPanel'
|
|
3
6
|
import { renderSessionsPanel } from './components/SessionsPanel'
|
|
@@ -10,25 +13,89 @@ import { renderTopPagesPanel } from './components/TopPagesPanel'
|
|
|
10
13
|
* Keeping that contract lets the chrome be replaced without touching the ~3k
|
|
11
14
|
* lines of panel client code.
|
|
12
15
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
16
|
+
/**
|
|
17
|
+
* A path nothing should claim, used as the control below.
|
|
18
|
+
*/
|
|
19
|
+
const NOT_A_ROUTE = '/__bakery_probe_no_handler_serves_this__'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Is anything serving `/_db`?
|
|
23
|
+
*
|
|
24
|
+
* Asked **behaviourally**, never by importing db-explorer: a plugin-to-plugin
|
|
25
|
+
* import is a package-graph edge, and `tests/conventions.test.ts` allows
|
|
26
|
+
* exactly one of those (this package → analytics). It is also the better
|
|
27
|
+
* question — the console wants to know whether that path leads somewhere, not
|
|
28
|
+
* which package put it there, so an application serving its own explorer at
|
|
29
|
+
* `/_db` gets the link too.
|
|
30
|
+
*
|
|
31
|
+
* **The control probe is what makes this work.** `StaticHandler.canHandle()`
|
|
32
|
+
* returns `true` unconditionally — it is the priority-0 fallback and claims
|
|
33
|
+
* every path — so "does some handler claim `/_db`" is always yes. A handler
|
|
34
|
+
* that claims `/_db` *and declines a path nobody serves* is claiming a
|
|
35
|
+
* namespace rather than catching everything.
|
|
36
|
+
*
|
|
37
|
+
* `canHandle` signatures vary across handlers and some read the request, so a
|
|
38
|
+
* throw here means "not the one we are looking for" rather than an error.
|
|
39
|
+
*/
|
|
40
|
+
function explorerIsMounted(): boolean {
|
|
41
|
+
for (const handler of Bakery.handlers.fetch.list()) {
|
|
42
|
+
const claims = (path: string) =>
|
|
43
|
+
Try.return(() => (handler as any).canHandle?.(path) === true, false)
|
|
44
|
+
if (claims('/_db') && !claims(NOT_A_ROUTE)) return true
|
|
45
|
+
}
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface NavEntry {
|
|
50
|
+
id: string
|
|
51
|
+
label: string
|
|
52
|
+
/** Present when this entry leaves the console rather than switching a tab. */
|
|
53
|
+
href?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function navSections(explorerMounted: boolean): {
|
|
57
|
+
group: string
|
|
58
|
+
items: NavEntry[]
|
|
59
|
+
}[] {
|
|
60
|
+
return [
|
|
61
|
+
{
|
|
62
|
+
group: 'Observability',
|
|
63
|
+
items: [
|
|
64
|
+
{ id: 'stats', label: 'Overview' },
|
|
65
|
+
{ id: 'top-pages', label: 'Traffic' },
|
|
66
|
+
{ id: 'logs', label: 'Logs' },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
group: 'Data',
|
|
71
|
+
items: [
|
|
72
|
+
// The console does not browse the database any more, so Database is a
|
|
73
|
+
// way *out* of it when the explorer is mounted — a link, not a tab.
|
|
74
|
+
// Without it the entry stays a tab showing the panel that explains
|
|
75
|
+
// where the editor went and how to get it back; an entry that silently
|
|
76
|
+
// navigates to a 404 would be worse than either.
|
|
77
|
+
explorerMounted
|
|
78
|
+
? { id: 'database', label: 'Database', href: '/_db' }
|
|
79
|
+
: { id: 'database', label: 'Database' },
|
|
80
|
+
{ id: 'sessions', label: 'Sessions' },
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function NavItem({ id, label, href }: NavEntry) {
|
|
87
|
+
if (href) {
|
|
88
|
+
return (
|
|
89
|
+
<a class="tab-btn" href={href}>
|
|
90
|
+
<span class="nav-dot"></span>
|
|
91
|
+
<span>{label}</span>
|
|
92
|
+
<span class="nav-external" aria-hidden="true">
|
|
93
|
+
↗
|
|
94
|
+
</span>
|
|
95
|
+
</a>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
32
99
|
return (
|
|
33
100
|
<button
|
|
34
101
|
type="button"
|
|
@@ -41,6 +108,9 @@ function NavItem({ id, label }: { id: string; label: string }) {
|
|
|
41
108
|
}
|
|
42
109
|
|
|
43
110
|
export default function Dashboard() {
|
|
111
|
+
const explorerMounted = explorerIsMounted()
|
|
112
|
+
const NAV = navSections(explorerMounted)
|
|
113
|
+
|
|
44
114
|
return (
|
|
45
115
|
<html lang="en">
|
|
46
116
|
<head>
|
|
@@ -62,14 +132,20 @@ export default function Dashboard() {
|
|
|
62
132
|
<nav class="rail-group">
|
|
63
133
|
<div class="rail-group-label">{section.group}</div>
|
|
64
134
|
{section.items.map(item => (
|
|
65
|
-
<NavItem id={item.id} label={item.label} />
|
|
135
|
+
<NavItem id={item.id} label={item.label} href={item.href} />
|
|
66
136
|
))}
|
|
67
137
|
</nav>
|
|
68
138
|
))}
|
|
69
139
|
|
|
70
140
|
<div class="rail-foot">
|
|
71
141
|
<span>Bakery</span>
|
|
72
|
-
|
|
142
|
+
{/* Was the literal `v3`, which was never any version of anything
|
|
143
|
+
— nothing filled the id, and the framework was on 1.x when it
|
|
144
|
+
was written. `getFrameworkVersion()` reads core's own
|
|
145
|
+
manifest, which is the number this label claims to be; the
|
|
146
|
+
app's version is a different question and `BAKERY_VERSION`
|
|
147
|
+
answers that one despite its name. */}
|
|
148
|
+
<span id="rail-version">v{getFrameworkVersion()}</span>
|
|
73
149
|
</div>
|
|
74
150
|
</aside>
|
|
75
151
|
|
|
@@ -125,7 +201,7 @@ export default function Dashboard() {
|
|
|
125
201
|
{renderStatsPanel()}
|
|
126
202
|
{renderTopPagesPanel()}
|
|
127
203
|
{renderSessionsPanel()}
|
|
128
|
-
{renderDatabaseBrowser()}
|
|
204
|
+
{explorerMounted ? null : renderDatabaseBrowser()}
|
|
129
205
|
{renderLogsPanel()}
|
|
130
206
|
</main>
|
|
131
207
|
</div>
|