dsh-db-studio 0.0.0-stage → 1.0.2

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/lib/client.js ADDED
@@ -0,0 +1,2313 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-db-studio",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ var React = require("react");
7
+ var ReactDOM = null;
8
+ try { ReactDOM = require("react-dom"); } catch (error) { ReactDOM = null; }
9
+ var h = React.createElement;
10
+ var NS = "dsh-db-studio";
11
+ var API = "/dsh-db-studio/api";
12
+
13
+ // ── copy ────────────────────────────────────────────────────────────
14
+ var zh = {
15
+ title: "数据库",
16
+ subtitle: "连接、浏览和查询数据库,也可以直接在对话里让助手操作数据",
17
+ connections: "连接",
18
+ newConnection: "新建连接",
19
+ refresh: "刷新",
20
+ emptyTitle: "还没有数据库连接",
21
+ emptyDesc: "连接 SQLite 文件或 PostgreSQL / MySQL 服务。也可以先用内置示例数据库体验。",
22
+ useDemo: "使用示例数据库",
23
+ demo: "示例",
24
+ readOnly: "只读",
25
+ tables: "表",
26
+ schema: "结构",
27
+ data: "数据",
28
+ sql: "SQL",
29
+ run: "运行",
30
+ running: "运行中…",
31
+ searchTable: "搜索表…",
32
+ noTables: "这个数据库里没有表",
33
+ rows: "行",
34
+ page: "第 {page} 页",
35
+ prev: "上一页",
36
+ next: "下一页",
37
+ pageSize: "每页",
38
+ loading: "加载中…",
39
+ noRows: "没有数据",
40
+ nullValue: "NULL",
41
+ test: "测试连接",
42
+ testing: "测试中…",
43
+ save: "保存",
44
+ saving: "保存中…",
45
+ cancel: "取消",
46
+ edit: "编辑",
47
+ remove: "删除",
48
+ removeTitle: "删除连接",
49
+ removeDesc: "删除后不影响数据库本身,只移除这里的连接配置。",
50
+ confirmRemove: "确认删除",
51
+ name: "名称",
52
+ kind: "类型",
53
+ file: "SQLite 文件路径",
54
+ fileHint: "支持 ~ 开头的路径;文件不存在时会新建。",
55
+ host: "主机",
56
+ port: "端口",
57
+ database: "数据库",
58
+ user: "用户名",
59
+ password: "密码",
60
+ passwordKeep: "已保存,留空表示不修改",
61
+ ssl: "使用 SSL",
62
+ advanced: "高级选项",
63
+ readonlyConnection: "只读连接(禁止写操作)",
64
+ testOk: "连接成功:{target}",
65
+ testFail: "连接失败:{error}",
66
+ saveFailed: "保存失败:{error}",
67
+ loadFailed: "加载失败:{error}",
68
+ queryFailed: "执行失败:{error}",
69
+ sqlHint: "只读 SQL 会自动走 db_query;INSERT / UPDATE / DELETE 等写操作会走 db_execute,并由你这次点击确认。",
70
+ sqlPlaceholder: "SELECT * FROM users LIMIT 20;",
71
+ result: "结果",
72
+ affected: "影响 {n} 行",
73
+ truncated: "结果已截断",
74
+ columns: "字段",
75
+ noConnection: "请选择一个连接",
76
+ selectTable: "从左侧选择一张表",
77
+ selectTableHint: "可以查看数据、结构和执行 SQL。",
78
+ driverMissing: "未安装 {driver} 驱动,使用该类型连接前需要安装依赖。",
79
+ demoNote: "示例数据库位于 {path}",
80
+ inChat: "在对话中操作",
81
+ inChatHint: "试试对助手说:「查一下示例数据库里最近 10 笔订单,按金额排序」或「把 users 表里杭州的用户城市改成杭州·西湖区」。",
82
+ sqlHistory: "最近查询",
83
+ clear: "清空",
84
+ copy: "复制",
85
+ copied: "已复制",
86
+ close: "关闭",
87
+ table: "表",
88
+ view: "视图",
89
+ pk: "主键",
90
+ notNull: "非空",
91
+ defaultLabel: "默认值",
92
+ dialect: "SQL 方言",
93
+ notEditableDemo: "示例连接可以直接浏览和修改数据;删除后也可以从空状态重新创建。",
94
+ kindSqlite: "SQLite",
95
+ kindPostgres: "PostgreSQL",
96
+ kindMysql: "MySQL",
97
+ tabsAria: "数据库视图切换",
98
+ closeDialog: "关闭对话框",
99
+ rowIndex: "#",
100
+ totalRows: "共 {n} 行",
101
+ serverError: "服务器返回错误",
102
+ needConnectionName: "请填写连接名称",
103
+ needDatabase: "请填写数据库名",
104
+ needFile: "请填写 SQLite 文件路径",
105
+ unsupportedAction: "这个操作在当前 DSH 版本不可用。",
106
+ selectAll: "全选",
107
+ clearAll: "清空",
108
+ done: "完成",
109
+ openPanel: "打开数据库面板",
110
+ selectDatabase: "选择数据库",
111
+ noConnections: "还没有数据库连接",
112
+ statusLabel: "数据库连接状态",
113
+ tableCount: "{n} 张表",
114
+ statusFailed: "连接失败",
115
+ statusChecking: "检查中",
116
+ noSelectionTitle: "还没有选择数据库",
117
+ noSelectionDesc: "在输入框旁的多选下拉里选择要在这个对话中使用的数据库。",
118
+ selectAllRecent: "全选最近使用",
119
+ openGlobal: "打开全局面板",
120
+ conversationEmptyTitle: "这个对话还没有数据库",
121
+ conversationEmptyDesc: "先新建一个连接,或使用内置示例数据库开始。",
122
+ selected: "已选",
123
+ databases: "数据库",
124
+ connectionCount: "{n} 个连接",
125
+ databaseCount: "{n} 个数据库",
126
+ connectedOf: "{ok}/{n} 已连接",
127
+ allConnected: "已连接",
128
+ schemaHint: "点击「结构」查看字段",
129
+ askInChat: "可以直接在对话里让助手查询或修改这些数据",
130
+ commandLabel: "数据库",
131
+ commandDesc: "打开数据库面板"
132
+ };
133
+ var en = {
134
+ title: "Database",
135
+ subtitle: "Connect, browse and query databases — or ask the assistant to work with the data in chat",
136
+ connections: "Connections",
137
+ newConnection: "New connection",
138
+ refresh: "Refresh",
139
+ emptyTitle: "No database connections yet",
140
+ emptyDesc: "Connect a SQLite file or a PostgreSQL / MySQL server. You can start with the built-in demo database.",
141
+ useDemo: "Use demo database",
142
+ demo: "Demo",
143
+ readOnly: "Read-only",
144
+ tables: "Tables",
145
+ schema: "Schema",
146
+ data: "Data",
147
+ sql: "SQL",
148
+ run: "Run",
149
+ running: "Running…",
150
+ searchTable: "Search tables…",
151
+ noTables: "No tables in this database",
152
+ rows: "rows",
153
+ page: "Page {page}",
154
+ prev: "Previous",
155
+ next: "Next",
156
+ pageSize: "Per page",
157
+ loading: "Loading…",
158
+ noRows: "No rows",
159
+ nullValue: "NULL",
160
+ test: "Test connection",
161
+ testing: "Testing…",
162
+ save: "Save",
163
+ saving: "Saving…",
164
+ cancel: "Cancel",
165
+ edit: "Edit",
166
+ remove: "Delete",
167
+ removeTitle: "Delete connection",
168
+ removeDesc: "The database itself is untouched; only this connection entry is removed.",
169
+ confirmRemove: "Delete",
170
+ name: "Name",
171
+ kind: "Kind",
172
+ file: "SQLite file path",
173
+ fileHint: "~ paths are supported; a missing file is created.",
174
+ host: "Host",
175
+ port: "Port",
176
+ database: "Database",
177
+ user: "User",
178
+ password: "Password",
179
+ passwordKeep: "Saved — leave blank to keep",
180
+ ssl: "Use SSL",
181
+ advanced: "Advanced",
182
+ readonlyConnection: "Read-only connection (writes disabled)",
183
+ testOk: "Connected: {target}",
184
+ testFail: "Connection failed: {error}",
185
+ saveFailed: "Could not save: {error}",
186
+ loadFailed: "Could not load: {error}",
187
+ queryFailed: "Query failed: {error}",
188
+ sqlHint: "Read-only SQL goes through db_query; INSERT / UPDATE / DELETE etc. go through db_execute and are confirmed by this click.",
189
+ sqlPlaceholder: "SELECT * FROM users LIMIT 20;",
190
+ result: "Result",
191
+ affected: "{n} rows affected",
192
+ truncated: "Result truncated",
193
+ columns: "Columns",
194
+ noConnection: "Choose a connection",
195
+ selectTable: "Choose a table on the left",
196
+ selectTableHint: "Browse data, inspect the schema, or run SQL.",
197
+ driverMissing: "The {driver} driver is not installed; install it before using this connection kind.",
198
+ demoNote: "Demo database: {path}",
199
+ inChat: "Work with data in chat",
200
+ inChatHint: "Try: “show me the last 10 orders in the demo database, sorted by total” or “change every Hangzhou user's city to Hangzhou-West Lake”.",
201
+ sqlHistory: "Recent queries",
202
+ clear: "Clear",
203
+ copy: "Copy",
204
+ copied: "Copied",
205
+ close: "Close",
206
+ table: "Table",
207
+ view: "View",
208
+ pk: "PK",
209
+ notNull: "NOT NULL",
210
+ defaultLabel: "default",
211
+ dialect: "SQL dialect",
212
+ notEditableDemo: "The demo connection can be browsed and modified. If deleted, recreate it from the empty state.",
213
+ kindSqlite: "SQLite",
214
+ kindPostgres: "PostgreSQL",
215
+ kindMysql: "MySQL",
216
+ tabsAria: "Database views",
217
+ closeDialog: "Close dialog",
218
+ rowIndex: "#",
219
+ totalRows: "{n} rows",
220
+ serverError: "Server error",
221
+ needConnectionName: "Enter a connection name",
222
+ needDatabase: "Enter a database name",
223
+ needFile: "Enter a SQLite file path",
224
+ unsupportedAction: "This action is not available in the current DSH version.",
225
+ selectAll: "Select all",
226
+ clearAll: "Clear",
227
+ done: "Done",
228
+ openPanel: "Open database panel",
229
+ selectDatabase: "Select databases",
230
+ noConnections: "No database connections yet",
231
+ statusLabel: "Database connection status",
232
+ tableCount: "{n} tables",
233
+ statusFailed: "Connection failed",
234
+ statusChecking: "Checking",
235
+ noSelectionTitle: "No databases selected",
236
+ noSelectionDesc: "Pick the databases this conversation should use in the multi-select beside the composer.",
237
+ selectAllRecent: "Select recently used",
238
+ openGlobal: "Open full panel",
239
+ conversationEmptyTitle: "No database in this conversation",
240
+ conversationEmptyDesc: "Create a connection, or start with the built-in demo database.",
241
+ selected: "Selected",
242
+ databases: "Databases",
243
+ connectionCount: "{n} connections",
244
+ databaseCount: "{n} databases",
245
+ connectedOf: "{ok}/{n} connected",
246
+ allConnected: "Connected",
247
+ schemaHint: "Use Schema to inspect columns",
248
+ askInChat: "Ask the assistant to query or change this data in chat",
249
+ commandLabel: "Database",
250
+ commandDesc: "Open the database panel"
251
+ };
252
+
253
+ function interpolate(text, params) {
254
+ return String(text).replace(/\{(\w+)\}/g, function (_, key) {
255
+ return params && params[key] !== undefined ? String(params[key]) : "{" + key + "}";
256
+ });
257
+ }
258
+
259
+ // ── API ─────────────────────────────────────────────────────────────
260
+ async function api(body) {
261
+ var response;
262
+ try {
263
+ response = await fetch(API, {
264
+ method: "POST",
265
+ headers: { "Content-Type": "application/json" },
266
+ body: JSON.stringify(body)
267
+ });
268
+ } catch (error) {
269
+ throw new Error(error && error.message ? error.message : "network error");
270
+ }
271
+ var payload = null;
272
+ try { payload = await response.json(); } catch (error) { payload = null; }
273
+ if (!response.ok) throw new Error((payload && payload.error) || response.statusText || "HTTP " + response.status);
274
+ if (!payload) throw new Error("empty response");
275
+ if (payload.ok === false) throw new Error(payload.error || "request failed");
276
+ return payload;
277
+ }
278
+
279
+ function fmtNumber(n) {
280
+ var value = Number(n);
281
+ return Number.isFinite(value) ? value.toLocaleString() : String(n);
282
+ }
283
+
284
+ function cellText(value) {
285
+ if (value === null || value === undefined) return null;
286
+ if (typeof value === "object") return JSON.stringify(value);
287
+ return String(value);
288
+ }
289
+
290
+ /** Captured in apply(); lets any seat reach the global DB panel. */
291
+ var APP_CTX = null;
292
+ function openGlobalPanel() {
293
+ var layout = APP_CTX && APP_CTX.get("layout");
294
+ if (!layout || typeof layout.selectPanel !== "function") return false;
295
+ try { layout.selectPanel("dbstudio"); return true; } catch (error) { return false; }
296
+ }
297
+
298
+ // ── per-session database state ──────────────────────────────────────
299
+ //
300
+ // The picker, the conversation tab and the status strip are three seats
301
+ // over one host-owned answer. They share this small store so a checkbox
302
+ // flips every surface at once; the host remains the authority and every
303
+ // mutation round-trips through /dsh-db-studio/api.
304
+ var SESSION_CACHE = new Map();
305
+ var sessionListeners = new Map();
306
+
307
+ function emptySessionEntry() {
308
+ return { state: null, error: null, loading: false, status: null, statusLoading: false };
309
+ }
310
+
311
+ function sessionEntry(sessionId) {
312
+ var entry = SESSION_CACHE.get(sessionId);
313
+ if (!entry) {
314
+ entry = emptySessionEntry();
315
+ SESSION_CACHE.set(sessionId, entry);
316
+ }
317
+ return entry;
318
+ }
319
+
320
+ function notifySession(sessionId) {
321
+ var entry = sessionEntry(sessionId);
322
+ var listeners = sessionListeners.get(sessionId);
323
+ if (!listeners) return;
324
+ listeners.forEach(function (listener) {
325
+ try { listener(entry); } catch (error) { /* a seat must not break the others */ }
326
+ });
327
+ }
328
+
329
+ function subscribeSession(sessionId, listener) {
330
+ var listeners = sessionListeners.get(sessionId);
331
+ if (!listeners) {
332
+ listeners = new Set();
333
+ sessionListeners.set(sessionId, listeners);
334
+ }
335
+ listeners.add(listener);
336
+ return function () {
337
+ listeners.delete(listener);
338
+ if (listeners.size === 0) sessionListeners.delete(sessionId);
339
+ };
340
+ }
341
+
342
+ async function loadSessionState(sessionId, force) {
343
+ if (!sessionId) return null;
344
+ var entry = sessionEntry(sessionId);
345
+ if (entry.loading && !force) return entry.state;
346
+ entry.loading = true;
347
+ notifySession(sessionId);
348
+ try {
349
+ var data = await api({ action: "session-state", sessionId: sessionId });
350
+ entry.state = data;
351
+ entry.error = null;
352
+ } catch (error) {
353
+ // An older host (one that predates per-conversation selection) does
354
+ // not know session-state/session-set. Fall back to the global
355
+ // overview so the picker, tab and status strip keep working; the
356
+ // selection is then page-local until the host is restarted.
357
+ try {
358
+ var overview = await api({ action: "overview" });
359
+ var all = overview.connections || [];
360
+ entry.state = Object.assign({}, overview, {
361
+ sessionId: sessionId,
362
+ legacy: true,
363
+ selectedIds: all.map(function (row) { return row.id; }),
364
+ selected: all,
365
+ });
366
+ entry.error = null;
367
+ } catch (fallbackError) {
368
+ entry.error = error.message;
369
+ }
370
+ } finally {
371
+ entry.loading = false;
372
+ notifySession(sessionId);
373
+ }
374
+ return entry.state;
375
+ }
376
+
377
+ async function setSessionSelection(sessionId, ids) {
378
+ var entry = sessionEntry(sessionId);
379
+ var previous = entry.state;
380
+ if (previous) {
381
+ var selected = ids
382
+ .map(function (id) {
383
+ return (previous.connections || []).find(function (row) { return row.id === id; });
384
+ })
385
+ .filter(Boolean);
386
+ entry.state = Object.assign({}, previous, { selectedIds: ids, selected: selected });
387
+ entry.status = null;
388
+ notifySession(sessionId);
389
+ }
390
+ if (previous && previous.legacy) {
391
+ // Host has no session store yet: keep the page-local selection and
392
+ // skip the round-trip that would fail with "unknown action".
393
+ return entry.state;
394
+ }
395
+ try {
396
+ var data = await api({ action: "session-set", sessionId: sessionId, ids: ids });
397
+ entry.state = data;
398
+ entry.error = null;
399
+ entry.status = null;
400
+ notifySession(sessionId);
401
+ return data;
402
+ } catch (error) {
403
+ entry.state = previous;
404
+ entry.error = error.message;
405
+ notifySession(sessionId);
406
+ throw error;
407
+ }
408
+ }
409
+
410
+ async function loadSessionStatus(sessionId) {
411
+ if (!sessionId) return null;
412
+ var entry = sessionEntry(sessionId);
413
+ if (entry.statusLoading) return entry.status;
414
+ var selected = entry.state ? entry.state.selected || [] : [];
415
+ entry.statusLoading = true;
416
+ notifySession(sessionId);
417
+ try {
418
+ if (entry.state && entry.state.legacy) {
419
+ // Legacy host: derive a light status from the tables action, which
420
+ // every version of the plugin route understands.
421
+ var rows = await Promise.all(selected.map(async function (connection) {
422
+ var started = Date.now();
423
+ try {
424
+ var data = await api({ action: "tables", id: connection.id });
425
+ return Object.assign({}, connection, {
426
+ ok: true,
427
+ latencyMs: Date.now() - started,
428
+ tableCount: (data.tables || []).length,
429
+ });
430
+ } catch (error) {
431
+ return Object.assign({}, connection, { ok: false, latencyMs: Date.now() - started, error: error.message });
432
+ }
433
+ }));
434
+ entry.status = { sessionId: sessionId, checkedAt: Date.now(), connections: rows };
435
+ } else {
436
+ var data = await api({ action: "session-status", sessionId: sessionId });
437
+ entry.status = data;
438
+ }
439
+ } catch (error) {
440
+ entry.status = { connections: [], error: error.message };
441
+ } finally {
442
+ entry.statusLoading = false;
443
+ notifySession(sessionId);
444
+ }
445
+ return entry.status;
446
+ }
447
+
448
+ function useSessionDb(sessionId) {
449
+ var [entry, setEntry] = React.useState(function () {
450
+ return Object.assign({}, sessionEntry(sessionId));
451
+ });
452
+ React.useEffect(function () {
453
+ var alive = true;
454
+ setEntry(Object.assign({}, sessionEntry(sessionId)));
455
+ var unsubscribe = subscribeSession(sessionId, function (next) {
456
+ if (alive) setEntry(Object.assign({}, next));
457
+ });
458
+ loadSessionState(sessionId);
459
+ return function () {
460
+ alive = false;
461
+ unsubscribe();
462
+ };
463
+ }, [sessionId]);
464
+ return entry;
465
+ }
466
+
467
+ function sessionKindLabel(kind) {
468
+ return KIND_LABEL[kind] || kind;
469
+ }
470
+
471
+ function connectionSubtitle(connection) {
472
+ if (!connection) return "";
473
+ return connection.kind === "sqlite"
474
+ ? (connection.file || "")
475
+ : (connection.host + ":" + connection.port + "/" + (connection.database || ""));
476
+ }
477
+
478
+ function isReadOnlySql(sql) {
479
+ return /^\s*\(?\s*(select|with|show|describe|desc|explain|pragma|values|table)\b/i.test(sql || "");
480
+ }
481
+
482
+ // ── icons ───────────────────────────────────────────────────────────
483
+ function Svg(props) {
484
+ return h("svg", {
485
+ width: props.size || 16,
486
+ height: props.size || 16,
487
+ viewBox: "0 0 24 24",
488
+ fill: "none",
489
+ stroke: "currentColor",
490
+ strokeWidth: props.strokeWidth || 1.7,
491
+ strokeLinecap: "round",
492
+ strokeLinejoin: "round",
493
+ "aria-hidden": "true",
494
+ focusable: "false",
495
+ style: props.style
496
+ }, props.children);
497
+ }
498
+ function DatabaseIcon(props) {
499
+ return h(Svg, props,
500
+ h("ellipse", { cx: 12, cy: 5.5, rx: 7.5, ry: 3 }),
501
+ h("path", { d: "M4.5 5.5v6c0 1.66 3.36 3 7.5 3s7.5-1.34 7.5-3v-6" }),
502
+ h("path", { d: "M4.5 11.5v6c0 1.66 3.36 3 7.5 3s7.5-1.34 7.5-3v-6" })
503
+ );
504
+ }
505
+ function TableIcon(props) {
506
+ return h(Svg, props,
507
+ h("rect", { x: 3.5, y: 4.5, width: 17, height: 15, rx: 2.5 }),
508
+ h("path", { d: "M3.5 9.5h17M9.5 9.5v10M15 9.5v10" })
509
+ );
510
+ }
511
+ function PlusIcon(props) { return h(Svg, props, h("path", { d: "M12 5v14M5 12h14" })); }
512
+ function RefreshIcon(props) { return h(Svg, props, h("path", { d: "M20 11a8 8 0 1 0-2.3 5.7" }), h("path", { d: "M20 4v7h-7" })); }
513
+ function TrashIcon(props) { return h(Svg, props, h("path", { d: "M4 7h16M9 7V4.5h6V7M6.5 7l.8 13h9.4l.8-13" }), h("path", { d: "M10 11v5M14 11v5" })); }
514
+ function EditIcon(props) { return h(Svg, props, h("path", { d: "M4 20h4l10.5-10.5a2.1 2.1 0 0 0-3-3L5 17v3Z" }), h("path", { d: "M14.5 6.5l3 3" })); }
515
+ function PlayIcon(props) { return h(Svg, props, h("path", { d: "M7 4.8v14.4L19.5 12 7 4.8Z" })); }
516
+ function ChevronLeftIcon(props) { return h(Svg, props, h("path", { d: "M14.5 5.5 8 12l6.5 6.5" })); }
517
+ function ChevronRightIcon(props) { return h(Svg, props, h("path", { d: "M9.5 5.5 16 12l-6.5 6.5" })); }
518
+ function CloseIcon(props) { return h(Svg, props, h("path", { d: "M6 6l12 12M18 6 6 18" })); }
519
+ function CheckIcon(props) { return h(Svg, props, h("path", { d: "M5 12.5 10 17.5 19 7" })); }
520
+ function SearchIcon(props) { return h(Svg, props, h("circle", { cx: 11, cy: 11, r: 6.5 }), h("path", { d: "m16 16 4 4" })); }
521
+ function SparkIcon(props) { return h(Svg, props, h("path", { d: "M12 3.5 13.8 9l5.5 1.8-5.5 1.8L12 18l-1.8-5.4L4.7 10.8 10.2 9 12 3.5Z" })); }
522
+ function ChevronDownIcon(props) { return h(Svg, props, h("path", { d: "m6 9.5 6 6 6-6" })); }
523
+ function CopyIcon(props) { return h(Svg, props, h("rect", { x: 8.5, y: 8.5, width: 11, height: 11, rx: 2 }), h("path", { d: "M15.5 5.5h-9a2 2 0 0 0-2 2v9" })); }
524
+ function PlugIcon(props) { return h(Svg, props, h("path", { d: "M9 3v5M15 3v5M6.5 8h11v3.5a5.5 5.5 0 0 1-11 0V8ZM12 17v4" })); }
525
+
526
+ var KIND_ICON = { sqlite: DatabaseIcon, postgres: DatabaseIcon, mysql: DatabaseIcon };
527
+ var KIND_LABEL = { sqlite: "SQLite", postgres: "PostgreSQL", mysql: "MySQL" };
528
+
529
+ // ── styles ──────────────────────────────────────────────────────────
530
+ var css = `
531
+ .dshdb-page{height:100%;min-height:0;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);box-sizing:border-box;padding-top:var(--dsh-frame-top-clearance,0px);overflow:hidden}
532
+ .dshdb-page *{box-sizing:border-box}
533
+ .dshdb-header{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;padding:22px 28px 16px;flex:0 0 auto;border-bottom:1px solid var(--dsw-alias-border-l2)}
534
+ .dshdb-titlewrap{display:flex;align-items:center;gap:13px;min-width:0}
535
+ .dshdb-titlemark{width:38px;height:38px;border-radius:11px;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent);color:var(--dsw-alias-brand-primary);flex:0 0 auto}
536
+ .dshdb-title{font-size:20px;font-weight:600;line-height:1.25;letter-spacing:.01em;margin:0}
537
+ .dshdb-subtitle{font-size:12.5px;color:var(--dsw-alias-label-tertiary);margin:4px 0 0;line-height:1.4}
538
+ .dshdb-actions{display:flex;align-items:center;gap:8px;flex:0 0 auto}
539
+ .dshdb-btn{appearance:none;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px;line-height:1;padding:9px 12px;border-radius:9px;display:inline-flex;align-items:center;gap:7px;cursor:pointer;transition:background .15s ease,border-color .15s ease,color .15s ease}
540
+ .dshdb-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
541
+ .dshdb-btn:disabled{opacity:.5;cursor:default}
542
+ .dshdb-btn.primary{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary);color:#fff}
543
+ .dshdb-btn.primary:hover{filter:brightness(1.06)}
544
+ .dshdb-btn.danger{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 35%,var(--dsw-alias-border-l2))}
545
+ .dshdb-btn.ghost{border-color:transparent;padding:8px}
546
+ .dshdb-btn.small{font-size:12px;padding:6px 9px;border-radius:7px}
547
+ .dshdb-iconbtn{appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-tertiary);width:28px;height:28px;border-radius:7px;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;flex:0 0 auto}
548
+ .dshdb-iconbtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
549
+ .dshdb-body{flex:1 1 auto;min-height:0;display:flex}
550
+ .dshdb-sidebar{width:260px;flex:0 0 auto;border-right:1px solid var(--dsw-alias-border-l2);display:flex;flex-direction:column;min-height:0;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 55%,var(--dsw-alias-bg-base))}
551
+ .dshdb-sidehead{display:flex;align-items:center;justify-content:space-between;padding:14px 14px 8px;color:var(--dsw-alias-label-secondary);font-size:11.5px;font-weight:600;letter-spacing:.08em;text-transform:uppercase}
552
+ .dshdb-connlist{flex:1 1 auto;overflow-y:auto;padding:0 8px 10px;min-height:0}
553
+ .dshdb-connrow{position:relative;display:flex;align-items:center;gap:9px;padding:9px 9px;border-radius:9px;cursor:pointer;border:1px solid transparent;transition:background .12s ease,border-color .12s ease}
554
+ .dshdb-connrow:hover{background:var(--dsw-alias-interactive-bg-hover)}
555
+ .dshdb-connrow.active{background:var(--dsw-alias-bg-base);border-color:var(--dsw-alias-border-l2);box-shadow:var(--dsw-elevation-prominent)}
556
+ .dshdb-connicon{width:30px;height:30px;border-radius:8px;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 10%,transparent);color:var(--dsw-alias-brand-primary);flex:0 0 auto}
557
+ .dshdb-connmeta{min-width:0;flex:1 1 auto;display:flex;flex-direction:column;gap:2px}
558
+ .dshdb-connname{font-size:13px;font-weight:550;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
559
+ .dshdb-conntarget{font-size:11px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:var(--dsw-font-family-mono,var(--dsw-font-family))}
560
+ .dshdb-connrow .dshdb-iconbtn{opacity:0}
561
+ .dshdb-connrow:hover .dshdb-iconbtn,.dshdb-connrow.active .dshdb-iconbtn{opacity:1}
562
+ .dshdb-badge{font-size:10px;font-weight:600;letter-spacing:.02em;padding:2px 5px;border-radius:4px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);border:1px solid var(--dsw-alias-border-l2);white-space:nowrap}
563
+ .dshdb-badge.demo{color:var(--dsw-alias-brand-primary);border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 30%,transparent);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 9%,transparent)}
564
+ .dshdb-sidefoot{padding:10px 12px 12px;border-top:1px solid var(--dsw-alias-border-l2);font-size:11px;color:var(--dsw-alias-label-tertiary);line-height:1.5}
565
+ .dshdb-sidefoot code{font-size:10.5px;word-break:break-all;color:var(--dsw-alias-label-secondary)}
566
+ .dshdb-main{flex:1 1 auto;min-width:0;min-height:0;display:flex;flex-direction:column}
567
+ .dshdb-empty{flex:1;display:flex;align-items:center;justify-content:center;padding:32px}
568
+ .dshdb-emptycard{max-width:470px;text-align:center;display:flex;flex-direction:column;align-items:center;gap:12px}
569
+ .dshdb-emptyart{width:84px;height:84px;border-radius:24px;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 10%,transparent);color:var(--dsw-alias-brand-primary);margin-bottom:2px}
570
+ .dshdb-emptytitle{font-size:17px;font-weight:600;margin:0}
571
+ .dshdb-emptydesc{font-size:13px;line-height:1.65;color:var(--dsw-alias-label-tertiary);margin:0}
572
+ .dshdb-emptyactions{display:flex;gap:9px;margin-top:4px;flex-wrap:wrap;justify-content:center}
573
+ .dshdb-mainhead{flex:0 0 auto;padding:15px 24px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}
574
+ .dshdb-mainheadtop{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;flex-wrap:wrap}
575
+ .dshdb-activeconn{display:flex;align-items:center;gap:9px;min-width:0;flex:1 1 320px}
576
+ .dshdb-activeconnname{font-size:15px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
577
+ .dshdb-activeconntarget{font-size:11.5px;color:var(--dsw-alias-label-tertiary);font-family:var(--dsw-font-family-mono,var(--dsw-font-family));white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
578
+ .dshdb-tabs{display:flex;gap:2px;margin-top:14px}
579
+ .dshdb-tab{appearance:none;border:0;background:transparent;font:inherit;font-size:13px;color:var(--dsw-alias-label-tertiary);padding:9px 12px 10px;border-bottom:2px solid transparent;cursor:pointer;display:inline-flex;align-items:center;gap:6px}
580
+ .dshdb-tab:hover{color:var(--dsw-alias-label-primary)}
581
+ .dshdb-tab.active{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-brand-primary);font-weight:550}
582
+ .dshdb-pane{flex:1 1 auto;min-height:0;display:flex}
583
+ .dshdb-tablelist{width:214px;flex:0 0 auto;border-right:1px solid var(--dsw-alias-border-l2);display:flex;flex-direction:column;min-height:0;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 35%,var(--dsw-alias-bg-base))}
584
+ .dshdb-searchwrap{padding:11px 10px 7px;position:relative}
585
+ .dshdb-searchwrap svg{position:absolute;left:20px;top:20px;color:var(--dsw-alias-label-tertiary);pointer-events:none}
586
+ .dshdb-search{width:100%;appearance:none;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;font-size:12.5px;border-radius:8px;padding:7px 9px 7px 30px;outline:none}
587
+ .dshdb-search:focus{border-color:var(--dsw-alias-brand-primary)}
588
+ .dshdb-tablelistscroll{flex:1 1 auto;overflow-y:auto;padding:2px 7px 10px;min-height:0}
589
+ .dshdb-tablerow{display:flex;align-items:center;gap:7px;width:100%;appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12.5px;text-align:left;padding:7px 8px;border-radius:7px;cursor:pointer}
590
+ .dshdb-tablerow:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
591
+ .dshdb-tablerow.active{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 11%,transparent);color:var(--dsw-alias-brand-primary);font-weight:550}
592
+ .dshdb-tablerow span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
593
+ .dshdb-grid{flex:1 1 auto;min-width:0;min-height:0;display:flex;flex-direction:column}
594
+ .dshdb-gridbar{display:flex;align-items:center;gap:10px;justify-content:space-between;padding:11px 18px;border-bottom:1px solid var(--dsw-alias-border-l2);flex:0 0 auto;flex-wrap:wrap}
595
+ .dshdb-gridtitle{display:flex;align-items:center;gap:8px;font-size:13.5px;font-weight:600;min-width:0}
596
+ .dshdb-gridmeta{font-size:11.5px;color:var(--dsw-alias-label-tertiary);font-weight:400}
597
+ .dshdb-gridtools{display:flex;align-items:center;gap:7px;flex:0 0 auto;flex-wrap:wrap;justify-content:flex-end}
598
+ .dshdb-schemaPanel{flex:0 0 auto;border-bottom:1px solid var(--dsw-alias-border-l2);background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 35%,var(--dsw-alias-bg-base));padding:10px 18px 12px;max-height:210px;overflow:auto}
599
+ .dshdb-cols{display:flex;flex-wrap:wrap;gap:6px}
600
+ .dshdb-col{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:3px 7px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-secondary)}
601
+ .dshdb-col b{font-weight:600;color:var(--dsw-alias-label-primary)}
602
+ .dshdb-col em{font-style:normal;font-size:10px;color:var(--dsw-alias-label-tertiary)}
603
+ .dshdb-col.pk{border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 40%,transparent);color:var(--dsw-alias-brand-primary)}
604
+ .dshdb-gridscroll{flex:1 1 auto;min-height:0;overflow:auto;padding:0 18px 12px}
605
+ .dshdb-table{border-collapse:separate;border-spacing:0;width:100%;font-size:12.5px}
606
+ .dshdb-table th{position:sticky;top:0;z-index:1;text-align:left;font-weight:600;font-size:11.5px;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-base);border-bottom:1px solid var(--dsw-alias-border-l2);padding:9px 10px;white-space:nowrap}
607
+ .dshdb-table th.idx{width:44px;color:var(--dsw-alias-label-caption);text-align:right}
608
+ .dshdb-table td{border-bottom:1px solid var(--dsw-alias-separator-primary);padding:8px 10px;vertical-align:top;max-width:340px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
609
+ .dshdb-table td.idx{color:var(--dsw-alias-label-caption);text-align:right;font-variant-numeric:tabular-nums;font-size:11px}
610
+ .dshdb-table tr:hover td{background:color-mix(in srgb,var(--dsw-alias-interactive-bg-hover) 55%,transparent)}
611
+ .dshdb-null{color:var(--dsw-alias-label-caption);font-style:italic;font-size:11.5px}
612
+ .dshdb-num{font-variant-numeric:tabular-nums;color:var(--dsw-alias-state-business-primary)}
613
+ .dshdb-pager{display:flex;align-items:center;justify-content:flex-end;gap:10px;padding:9px 18px;border-top:1px solid var(--dsw-alias-border-l2);flex:0 0 auto;font-size:11.5px;color:var(--dsw-alias-label-tertiary)}
614
+ .dshdb-pager .dshdb-btn.small{padding:5px 8px}
615
+ .dshdb-pager select{appearance:none;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-secondary);font:inherit;font-size:11.5px;padding:4px 6px}
616
+ .dshdb-connselect,.dshdb-tableselect{display:none;appearance:none;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;font-size:12.5px;padding:7px 9px;max-width:230px}
617
+ .dshdb-sqlpane{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;padding:16px 20px;gap:12px;overflow:auto}
618
+ .dshdb-editorwrap{border:1px solid var(--dsw-alias-border-l2);border-radius:11px;overflow:hidden;background:var(--dsw-alias-bg-base);box-shadow:var(--dsw-elevation-prominent);flex:0 0 auto}
619
+ .dshdb-editortop{display:flex;align-items:center;justify-content:space-between;padding:7px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 55%,var(--dsw-alias-bg-base));font-size:11px;color:var(--dsw-alias-label-tertiary)}
620
+ .dshdb-editor{display:block;width:100%;min-height:128px;max-height:260px;resize:vertical;border:0;outline:none;background:transparent;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family-mono,ui-monospace,SFMono-Regular,Menlo,monospace);font-size:12.5px;line-height:1.65;padding:12px 14px;tab-size:2}
621
+ .dshdb-editor:focus{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dsw-alias-brand-primary) 45%,transparent)}
622
+ .dshdb-sqlbar{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}
623
+ .dshdb-hint{font-size:11.5px;color:var(--dsw-alias-label-tertiary);line-height:1.6;max-width:760px}
624
+ .dshdb-history{display:flex;gap:6px;flex-wrap:wrap}
625
+ .dshdb-history button{appearance:none;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-tertiary);font-family:var(--dsw-font-family-mono,monospace);font-size:10.5px;padding:3px 6px;border-radius:5px;cursor:pointer;max-width:300px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
626
+ .dshdb-history button:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3)}
627
+ .dshdb-result{flex:1 1 auto;min-height:160px;border:1px solid var(--dsw-alias-border-l2);border-radius:11px;overflow:hidden;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base)}
628
+ .dshdb-resulthead{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 12px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:12px;color:var(--dsw-alias-label-secondary);background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 45%,var(--dsw-alias-bg-base));flex:0 0 auto}
629
+ .dshdb-resultbody{flex:1 1 auto;min-height:0;overflow:auto;padding:2px 10px 10px}
630
+ .dshdb-resultbody .dshdb-table th{background:var(--dsw-alias-bg-base)}
631
+ .dshdb-error{border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 8%,transparent);color:var(--dsw-alias-state-error-primary);border-radius:9px;padding:10px 12px;font-size:12.5px;line-height:1.55;white-space:pre-wrap}
632
+ .dshdb-success{border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary) 38%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 8%,transparent);color:var(--dsw-alias-state-success-primary);border-radius:9px;padding:10px 12px;font-size:12.5px;display:flex;align-items:center;gap:8px}
633
+ .dshdb-placeholder{flex:1;display:flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary);font-size:13px;text-align:center;padding:30px;line-height:1.7}
634
+ .dshdb-loading{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dsw-alias-label-tertiary)}
635
+ .dshdb-spinner{width:14px;height:14px;border-radius:50%;border:2px solid var(--dsw-alias-border-l3);border-top-color:var(--dsw-alias-brand-primary);animation:dshdb-spin .8s linear infinite}
636
+ @keyframes dshdb-spin{to{transform:rotate(360deg)}}
637
+ .dshdb-overlay{position:fixed;inset:0;z-index:60;background:rgba(20,22,28,.38);backdrop-filter:blur(3px);display:flex;align-items:center;justify-content:center;padding:24px;animation:dshdb-fade .12s ease}
638
+ @keyframes dshdb-fade{from{opacity:0}to{opacity:1}}
639
+ .dshdb-dialog{width:min(560px,100%);max-height:calc(100vh - 80px);overflow:auto;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:16px;box-shadow:0 24px 70px rgba(0,0,0,.28);padding:20px 20px 18px;font-family:var(--dsw-font-family)}
640
+ .dshdb-dialog.small{width:min(420px,100%)}
641
+ .dshdb-dialoghead{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:14px}
642
+ .dshdb-dialogtitle{font-size:16px;font-weight:600;margin:0}
643
+ .dshdb-dialogdesc{font-size:12px;color:var(--dsw-alias-label-tertiary);margin:5px 0 0;line-height:1.55}
644
+ .dshdb-form{display:flex;flex-direction:column;gap:11px}
645
+ .dshdb-formrow{display:flex;flex-direction:column;gap:5px}
646
+ .dshdb-formgrid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
647
+ .dshdb-label{font-size:11.5px;font-weight:550;color:var(--dsw-alias-label-secondary)}
648
+ .dshdb-input{appearance:none;width:100%;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;padding:8px 10px;outline:none}
649
+ .dshdb-input:focus{border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary) 15%,transparent)}
650
+ .dshdb-input::placeholder{color:var(--dsw-alias-label-caption)}
651
+ .dshdb-hintline{font-size:11px;color:var(--dsw-alias-label-tertiary);line-height:1.45}
652
+ .dshdb-kinds{display:grid;grid-template-columns:repeat(3,1fr);gap:7px}
653
+ .dshdb-kind{appearance:none;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12.5px;padding:9px 6px;border-radius:8px;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:5px}
654
+ .dshdb-kind:hover{background:var(--dsw-alias-interactive-bg-hover)}
655
+ .dshdb-kind.active{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 8%,transparent)}
656
+ .dshdb-kind small{font-size:9.5px;color:var(--dsw-alias-label-tertiary);font-weight:400}
657
+ .dshdb-check{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--dsw-alias-label-secondary);cursor:pointer;user-select:none}
658
+ .dshdb-check input{width:14px;height:14px;accent-color:var(--dsw-alias-brand-primary)}
659
+ .dshdb-dialogfoot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:16px;padding-top:14px;border-top:1px solid var(--dsw-alias-border-l2)}
660
+ .dshdb-dialogfoot .right{display:flex;gap:8px;margin-left:auto}
661
+ .dshdb-testmsg{font-size:11.5px;line-height:1.5;padding:8px 10px;border-radius:8px;margin-top:2px}
662
+ .dshdb-testmsg.ok{color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 8%,transparent)}
663
+ .dshdb-testmsg.bad{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 8%,transparent)}
664
+ .dshdb-chatbar{flex:0 0 auto;display:flex;align-items:flex-start;gap:9px;padding:11px 18px;border-top:1px solid var(--dsw-alias-border-l2);background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 40%,var(--dsw-alias-bg-base));font-size:11.5px;color:var(--dsw-alias-label-tertiary);line-height:1.55}
665
+ .dshdb-chatbar svg{color:var(--dsw-alias-brand-primary);flex:0 0 auto;margin-top:1px}
666
+ .dshdb-chatbar b{color:var(--dsw-alias-label-secondary);font-weight:600}
667
+ @media (max-width:1100px){.dshdb-sidebar{width:220px}.dshdb-tablelist{width:180px}.dshdb-subtitle{display:none}}
668
+ @media (max-width:1240px){.dshdb-sidebar{display:none}.dshdb-connselect{display:inline-flex;align-items:center}.dshdb-activeconnname{display:none}.dshdb-tablelist{width:180px}.dshdb-header{padding:18px 20px 14px}.dshdb-mainhead{padding:14px 18px 0}.dshdb-gridbar{padding:10px 14px}.dshdb-gridscroll{padding:0 14px 12px}.dshdb-pager{padding:9px 14px}.dshdb-sqlpane{padding:14px 16px}.dshdb-chatbar{padding:10px 16px}}
669
+ @media (max-width:900px){.dshdb-tablelist{display:none}.dshdb-tableselect{display:inline-flex;align-items:center}.dshdb-actions .dshdb-btn .dshdb-btnlabel{display:none}.dshdb-activeconn{flex:1 1 100%}.dshdb-header{align-items:center}.dshdb-header .dshdb-subtitle{display:none}}
670
+
671
+ .dshdb-picker-wrap{position:relative;display:inline-flex;align-items:center;min-width:0;max-width:190px}
672
+ .dshdb-picker-btn{appearance:none;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;line-height:1;padding:6px 7px;border-radius:7px;display:inline-flex;align-items:center;gap:5px;cursor:pointer;min-width:0;max-width:100%;transition:background .15s ease,border-color .15s ease,color .15s ease}
673
+ .dshdb-picker-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
674
+ .dshdb-picker-btn.has-selection{color:var(--dsw-alias-label-primary);border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 40%,var(--dsw-alias-border-l2));background:color-mix(in srgb,var(--dsw-alias-brand-primary) 7%,transparent)}
675
+ .dshdb-picker-btn.open{border-color:var(--dsw-alias-brand-primary)}
676
+ .dshdb-picker-btnlabel{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
677
+ .dshdb-picker-count{font-size:9.5px;font-weight:600;background:var(--dsw-alias-brand-primary);color:#fff;border-radius:999px;padding:1px 4px;min-width:13px;text-align:center}
678
+ .dshdb-picker-pop{background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:13px;box-shadow:0 18px 54px rgba(0,0,0,.25);padding:10px;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);animation:dshdb-pop .12s ease}
679
+ @keyframes dshdb-pop{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
680
+ .dshdb-picker-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;padding:2px 4px 6px}
681
+ .dshdb-picker-title{font-size:13px;font-weight:600}
682
+ .dshdb-picker-sub{font-size:10.5px;color:var(--dsw-alias-label-tertiary);margin-top:2px}
683
+ .dshdb-picker-headactions{display:flex;gap:2px;flex:0 0 auto}
684
+ .dshdb-picker-link{appearance:none;border:0;background:transparent;color:var(--dsw-alias-brand-primary);font:inherit;font-size:11px;padding:3px 5px;border-radius:5px;cursor:pointer;white-space:nowrap}
685
+ .dshdb-picker-link:hover{background:var(--dsw-alias-interactive-bg-hover)}
686
+ .dshdb-picker-list{max-height:min(340px,50vh);overflow:auto;display:flex;flex-direction:column;gap:1px;margin:2px 0 4px}
687
+ .dshdb-picker-item{display:flex;align-items:center;gap:8px;padding:7px 7px;border-radius:7px;cursor:pointer;border:1px solid transparent}
688
+ .dshdb-picker-item:hover{background:var(--dsw-alias-interactive-bg-hover)}
689
+ .dshdb-picker-item.checked{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 8%,transparent);border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 20%,transparent)}
690
+ .dshdb-picker-item input{width:13px;height:13px;accent-color:var(--dsw-alias-brand-primary);flex:0 0 auto;margin:0}
691
+ .dshdb-picker-icon{color:var(--dsw-alias-brand-primary);display:flex;flex:0 0 auto}
692
+ .dshdb-picker-meta{min-width:0;flex:1 1 auto;display:flex;flex-direction:column;gap:1px}
693
+ .dshdb-picker-name{font-size:12px;font-weight:550;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
694
+ .dshdb-picker-target{font-size:10px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:var(--dsw-font-family-mono,var(--dsw-font-family))}
695
+ .dshdb-picker-empty{padding:16px 10px;display:flex;flex-direction:column;align-items:center;gap:9px;text-align:center;font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.5}
696
+ .dshdb-picker-error{margin:4px 4px 0;color:var(--dsw-alias-state-error-primary);font-size:10.5px;line-height:1.4}
697
+ .dshdb-picker-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:7px 2px 0;border-top:1px solid var(--dsw-alias-border-l2);margin-top:4px}
698
+ .dshdb-statusdock{display:flex;align-items:center;flex-wrap:nowrap;padding:0 4px 4px;max-width:min(300px,50vw);min-width:0;overflow:hidden;font-size:10.5px;line-height:1.35;color:var(--dsw-alias-label-tertiary)}
699
+ .dshdb-statuspill{appearance:none;border:1px solid var(--dsw-alias-border-l2);background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 65%,transparent);color:var(--dsw-alias-label-tertiary);font:inherit;font-size:10.5px;line-height:1.3;padding:2px 6px 2px 6px;border-radius:999px;display:inline-flex;align-items:center;gap:5px;min-width:0;max-width:100%;cursor:pointer;white-space:nowrap;overflow:hidden}
700
+ .dshdb-statuspill:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}
701
+ .dshdb-statuspill.open{border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 35%,var(--dsw-alias-border-l2))}
702
+ .dshdb-statuspill-name{color:var(--dsw-alias-label-secondary);font-weight:550;font-size:10.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:150px}
703
+ .dshdb-statuspill-meta{color:var(--dsw-alias-label-caption);font-size:9.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:150px}
704
+ .dshdb-statuspill.bad .dshdb-statuspill-meta{color:var(--dsw-alias-state-error-primary)}
705
+ .dshdb-dot{width:6px;height:6px;border-radius:50%;background:var(--dsw-alias-label-caption);flex:0 0 auto}
706
+ .dshdb-dot.ok{background:var(--dsw-alias-state-success-primary)}
707
+ .dshdb-dot.bad{background:var(--dsw-alias-state-error-primary)}
708
+ .dshdb-dot.idle{background:var(--dsw-alias-label-caption)}
709
+ .dshdb-statuspop-head{padding:2px 4px 7px;border-bottom:1px solid var(--dsw-alias-border-l2);margin-bottom:5px}
710
+ .dshdb-statuspop-list{display:flex;flex-direction:column;gap:1px;max-height:min(300px,45vh);overflow:auto}
711
+ .dshdb-statuspop-row{display:flex;align-items:flex-start;gap:8px;padding:6px 7px;border-radius:7px}
712
+ .dshdb-statuspop-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
713
+ .dshdb-statuspop-meta{min-width:0;display:flex;flex-direction:column;gap:1px}
714
+ .dshdb-statuspop-name{font-size:12px;font-weight:550;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
715
+ .dshdb-statuspop-detail{font-size:10.5px;color:var(--dsw-alias-label-tertiary);line-height:1.4;word-break:break-word}
716
+ .dshdb-statuspop-row:has(.dshdb-dot.bad) .dshdb-statuspop-detail{color:var(--dsw-alias-state-error-primary)}
717
+ .dshdb-cv{height:100%;min-height:420px;display:flex;flex-direction:column;padding:14px 22px 16px;box-sizing:border-box;overflow:hidden;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);background:var(--dsw-alias-bg-base)}
718
+ .dshdb-cv *{box-sizing:border-box}
719
+ .dshdb-cvhead{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;flex:0 0 auto}
720
+ .dshdb-cvtitlewrap{display:flex;align-items:center;gap:10px;min-width:0}
721
+ .dshdb-cvtitle{font-size:16px;font-weight:600;margin:0;line-height:1.2}
722
+ .dshdb-cvsub{font-size:11px;color:var(--dsw-alias-label-tertiary);margin:3px 0 0}
723
+ .dshdb-cvactions{display:flex;align-items:center;gap:7px;flex:0 0 auto;flex-wrap:wrap;justify-content:flex-end}
724
+ .dshdb-cvpills{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:10px 0 0;flex:0 0 auto}
725
+ .dshdb-cvpill{appearance:none;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:11.5px;padding:5px 8px;border-radius:999px;display:inline-flex;align-items:center;gap:5px;cursor:pointer;max-width:230px}
726
+ .dshdb-cvpill:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
727
+ .dshdb-cvpill.active{border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 45%,transparent);color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 8%,transparent);font-weight:550}
728
+ .dshdb-cvpill.active .dshdb-dot{background:var(--dsw-alias-brand-primary)}
729
+ .dshdb-cvpillhint{font-size:10.5px;color:var(--dsw-alias-label-caption);margin-left:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
730
+ .dshdb-cvbody{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l2);border-radius:11px;overflow:hidden;margin-top:10px;background:var(--dsw-alias-bg-base)}
731
+ .dshdb-cvdata{flex:1 1 auto;min-height:0;display:flex}
732
+ .dshdb-cv .dshdb-tablelist{width:190px}
733
+ .dshdb-cv .dshdb-gridbar{padding:9px 12px}
734
+ .dshdb-cv .dshdb-gridscroll{padding:0 12px 8px}
735
+ .dshdb-cv .dshdb-pager{padding:7px 12px}
736
+ .dshdb-cv .dshdb-sqlpane{padding:12px 14px;gap:10px;height:100%}
737
+ .dshdb-cv .dshdb-tabs{margin-top:3px;flex:0 0 auto}
738
+ .dshdb-cvempty{flex:1;display:flex;align-items:center;justify-content:center;padding:26px;min-height:300px}
739
+ .dshdb-cvloading{flex:1;display:flex;align-items:center;justify-content:center;min-height:300px}
740
+ .dshdb-cvmessage{padding:6px 0;flex:0 0 auto}
741
+ @media (max-width:1100px){.dshdb-statuspill-meta{display:none}.dshdb-statuspill-name{max-width:110px}}
742
+ @media (max-width:900px){.dshdb-cv{padding:12px 14px 14px}.dshdb-cv .dshdb-tablelist{display:none}.dshdb-cv .dshdb-tableselect{display:inline-flex}.dshdb-cvpillhint{display:none}.dshdb-picker-wrap{max-width:150px}}
743
+
744
+ `;
745
+
746
+ /**
747
+ * Register the stylesheet once; removing it on disposal keeps a disabled
748
+ * plugin from leaking a sheet into the page.
749
+ */
750
+ function installStyles(ctx) {
751
+ ctx.effect(function () {
752
+ var tagId = "dsh-db-studio/client.css";
753
+ if (typeof document === "undefined") return;
754
+ var existing = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]");
755
+ if (existing) return function () {};
756
+ var style = document.createElement("style");
757
+ style.dataset.plugin = "dsh-db-studio";
758
+ style.dataset.pluginCss = tagId;
759
+ style.textContent = css;
760
+ document.head.appendChild(style);
761
+ return function () { style.remove(); };
762
+ }, "dsh-db-studio: styles");
763
+ }
764
+
765
+ // ── small UI atoms ───────────────────────────────────────────────────
766
+ function Spinner() {
767
+ return h("span", { className: "dshdb-loading" }, h("span", { className: "dshdb-spinner" }), "…");
768
+ }
769
+
770
+ function IconButton(props) {
771
+ return h("button", {
772
+ type: "button",
773
+ className: "dshdb-iconbtn",
774
+ title: props.title,
775
+ "aria-label": props.title,
776
+ onClick: function (event) { event.stopPropagation(); if (props.onClick) props.onClick(event); },
777
+ disabled: props.disabled
778
+ }, props.children);
779
+ }
780
+
781
+ function KindBadge(props) {
782
+ return h("span", { className: "dshdb-badge" + (props.demo ? " demo" : "") },
783
+ props.demo ? props.t("demo") : KIND_LABEL[props.kind] || props.kind);
784
+ }
785
+
786
+ function EmptyState(props) {
787
+ return h("div", { className: "dshdb-empty" },
788
+ h("div", { className: "dshdb-emptycard" },
789
+ h("div", { className: "dshdb-emptyart" }, h(DatabaseIcon, { size: 40 })),
790
+ h("h2", { className: "dshdb-emptytitle" }, props.t("emptyTitle")),
791
+ h("p", { className: "dshdb-emptydesc" }, props.t("emptyDesc")),
792
+ h("div", { className: "dshdb-emptyactions" },
793
+ props.onDemo ? h("button", { type: "button", className: "dshdb-btn", onClick: props.onDemo }, h(SparkIcon, { size: 15 }), props.t("useDemo")) : null,
794
+ h("button", { type: "button", className: "dshdb-btn primary", onClick: props.onNew }, h(PlusIcon, { size: 15 }), props.t("newConnection"))
795
+ )
796
+ )
797
+ );
798
+ }
799
+
800
+ function ConnectionRow(props) {
801
+ var connection = props.connection;
802
+ return h("div", {
803
+ className: "dshdb-connrow" + (props.active ? " active" : ""),
804
+ role: "button",
805
+ tabIndex: 0,
806
+ "aria-current": props.active ? "true" : undefined,
807
+ onClick: function () { props.onSelect(connection.id); },
808
+ onKeyDown: function (event) {
809
+ if (event.key === "Enter" || event.key === " ") { event.preventDefault(); props.onSelect(connection.id); }
810
+ },
811
+ title: connection.kind === "sqlite" ? connection.file : (connection.host + ":" + connection.port + "/" + connection.database)
812
+ },
813
+ h("span", { className: "dshdb-connicon" }, h(DatabaseIcon, { size: 16 })),
814
+ h("span", { className: "dshdb-connmeta" },
815
+ h("span", { className: "dshdb-connname" }, connection.name),
816
+ h("span", { className: "dshdb-conntarget" }, connection.kind === "sqlite" ? connection.file : (connection.host + ":" + connection.port + "/" + connection.database))
817
+ ),
818
+ h(KindBadge, { kind: connection.kind, demo: connection.demo, t: props.t }),
819
+ h(IconButton, { title: props.t("edit"), onClick: function () { props.onEdit(connection); } }, h(EditIcon, { size: 14 })),
820
+ h(IconButton, { title: props.t("remove"), onClick: function () { props.onRemove(connection); } }, h(TrashIcon, { size: 14 }))
821
+ );
822
+ }
823
+
824
+ // ── connection dialog ────────────────────────────────────────────────
825
+ var BLANK_FORM = {
826
+ kind: "sqlite",
827
+ name: "",
828
+ file: "",
829
+ host: "127.0.0.1",
830
+ port: "",
831
+ database: "",
832
+ user: "",
833
+ password: "",
834
+ ssl: false,
835
+ readOnly: false
836
+ };
837
+
838
+ function connectionToForm(connection) {
839
+ if (!connection) return Object.assign({}, BLANK_FORM);
840
+ return {
841
+ kind: connection.kind || "sqlite",
842
+ name: connection.name || "",
843
+ file: connection.file || "",
844
+ host: connection.host || "127.0.0.1",
845
+ port: connection.port ? String(connection.port) : "",
846
+ database: connection.database || "",
847
+ user: connection.user || "",
848
+ password: "",
849
+ ssl: connection.ssl === true,
850
+ readOnly: connection.readOnly === true
851
+ };
852
+ }
853
+
854
+ function ConnectionDialog(props) {
855
+ var t = props.t;
856
+ var [form, setForm] = React.useState(function () { return connectionToForm(props.connection); });
857
+ var [testing, setTesting] = React.useState(false);
858
+ var [saving, setSaving] = React.useState(false);
859
+ var [testResult, setTestResult] = React.useState(null);
860
+ var [error, setError] = React.useState(null);
861
+ var editing = Boolean(props.connection && props.connection.id);
862
+
863
+ function update(key, value) {
864
+ setForm(function (previous) {
865
+ var next = Object.assign({}, previous);
866
+ next[key] = value;
867
+ return next;
868
+ });
869
+ setTestResult(null);
870
+ setError(null);
871
+ }
872
+
873
+ function payload() {
874
+ var connection = {
875
+ kind: form.kind,
876
+ name: form.name.trim(),
877
+ readOnly: form.readOnly
878
+ };
879
+ if (form.kind === "sqlite") {
880
+ connection.file = form.file.trim();
881
+ } else {
882
+ connection.host = form.host.trim();
883
+ connection.port = form.port.trim() ? Number(form.port) : undefined;
884
+ connection.database = form.database.trim();
885
+ connection.user = form.user.trim();
886
+ connection.ssl = form.ssl;
887
+ if (form.password) connection.password = form.password;
888
+ }
889
+ return connection;
890
+ }
891
+
892
+ function validate() {
893
+ if (!form.name.trim()) return t("needConnectionName");
894
+ if (form.kind === "sqlite" && !form.file.trim()) return t("needFile");
895
+ if (form.kind !== "sqlite" && !form.database.trim()) return t("needDatabase");
896
+ return null;
897
+ }
898
+
899
+ async function test() {
900
+ var problem = validate();
901
+ if (problem) { setError(problem); return; }
902
+ setTesting(true);
903
+ setError(null);
904
+ try {
905
+ var body = payload();
906
+ if (editing) body.id = props.connection.id;
907
+ var result = await api(Object.assign({ action: "test" }, { connection: body }));
908
+ setTestResult({ ok: true, text: interpolate(t("testOk"), { target: result.target || "" }) });
909
+ } catch (failure) {
910
+ setTestResult({ ok: false, text: interpolate(t("testFail"), { error: failure.message }) });
911
+ } finally {
912
+ setTesting(false);
913
+ }
914
+ }
915
+
916
+ async function save() {
917
+ var problem = validate();
918
+ if (problem) { setError(problem); return; }
919
+ setSaving(true);
920
+ setError(null);
921
+ try {
922
+ var result;
923
+ if (editing) {
924
+ result = await api({ action: "update", id: props.connection.id, patch: payload() });
925
+ } else {
926
+ result = await api({ action: "add", connection: payload() });
927
+ }
928
+ props.onSaved(result && result.connection ? result.connection : null);
929
+ } catch (failure) {
930
+ setError(interpolate(t("saveFailed"), { error: failure.message }));
931
+ } finally {
932
+ setSaving(false);
933
+ }
934
+ }
935
+
936
+ function field(key, label, extra) {
937
+ return h("label", { className: "dshdb-formrow" },
938
+ h("span", { className: "dshdb-label" }, label),
939
+ h("input", Object.assign({
940
+ className: "dshdb-input",
941
+ value: form[key],
942
+ onChange: function (event) { update(key, event.target.value); }
943
+ }, extra || {}))
944
+ );
945
+ }
946
+
947
+ return h("div", { className: "dshdb-overlay", onMouseDown: function (event) { if (event.target === event.currentTarget) props.onClose(); } },
948
+ h("div", { className: "dshdb-dialog", role: "dialog", "aria-modal": "true", "aria-label": editing ? t("edit") : t("newConnection") },
949
+ h("div", { className: "dshdb-dialoghead" },
950
+ h("div", null,
951
+ h("h2", { className: "dshdb-dialogtitle" }, editing ? t("edit") : t("newConnection")),
952
+ h("p", { className: "dshdb-dialogdesc" }, editing ? t("notEditableDemo") : t("emptyDesc"))
953
+ ),
954
+ h(IconButton, { title: t("closeDialog"), onClick: props.onClose }, h(CloseIcon, { size: 16 }))
955
+ ),
956
+ h("div", { className: "dshdb-form" },
957
+ h("div", { className: "dshdb-formrow" },
958
+ h("span", { className: "dshdb-label" }, t("kind")),
959
+ h("div", { className: "dshdb-kinds" },
960
+ ["sqlite", "postgres", "mysql"].map(function (kind) {
961
+ return h("button", {
962
+ key: kind,
963
+ type: "button",
964
+ className: "dshdb-kind" + (form.kind === kind ? " active" : ""),
965
+ onClick: function () {
966
+ update("kind", kind);
967
+ if (kind === "postgres" && !form.port) update("port", "5432");
968
+ if (kind === "mysql" && !form.port) update("port", "3306");
969
+ }
970
+ }, h(DatabaseIcon, { size: 17 }), KIND_LABEL[kind], h("small", { className: "dshdb-hintline" }, kind === "sqlite" ? "local file" : (kind === "postgres" ? "port 5432" : "port 3306")));
971
+ })
972
+ )
973
+ ),
974
+ field("name", t("name"), { placeholder: form.kind === "sqlite" ? "本地数据库" : "生产库(只读)" }),
975
+ form.kind === "sqlite"
976
+ ? h("label", { className: "dshdb-formrow" },
977
+ h("span", { className: "dshdb-label" }, t("file")),
978
+ h("input", { className: "dshdb-input", value: form.file, placeholder: "~/Documents/data.db", onChange: function (event) { update("file", event.target.value); } }),
979
+ h("span", { className: "dshdb-hintline" }, t("fileHint"))
980
+ )
981
+ : h("div", { className: "dshdb-formgrid" },
982
+ field("host", t("host"), { placeholder: "127.0.0.1" }),
983
+ field("port", t("port"), { placeholder: form.kind === "postgres" ? "5432" : "3306", inputMode: "numeric" }),
984
+ field("database", t("database"), { placeholder: "app" }),
985
+ field("user", t("user"), { placeholder: form.kind === "postgres" ? "postgres" : "root" })
986
+ ),
987
+ form.kind === "sqlite" ? null : field("password", t("password"), {
988
+ type: "password",
989
+ placeholder: typeof props.connection?.hasPassword === "boolean" && props.connection.hasPassword ? t("passwordKeep") : ""
990
+ }),
991
+ h("div", { className: "dshdb-formrow" },
992
+ h("label", { className: "dshdb-check" },
993
+ h("input", { type: "checkbox", checked: form.readOnly, onChange: function (event) { update("readOnly", event.target.checked); } }),
994
+ t("readonlyConnection")
995
+ )
996
+ ),
997
+ form.kind === "sqlite" ? null : h("div", { className: "dshdb-formrow" },
998
+ h("label", { className: "dshdb-check" },
999
+ h("input", { type: "checkbox", checked: form.ssl, onChange: function (event) { update("ssl", event.target.checked); } }),
1000
+ t("ssl")
1001
+ )
1002
+ ),
1003
+ error ? h("div", { className: "dshdb-error" }, error) : null,
1004
+ testResult ? h("div", { className: "dshdb-testmsg " + (testResult.ok ? "ok" : "bad") }, testResult.text) : null
1005
+ ),
1006
+ h("div", { className: "dshdb-dialogfoot" },
1007
+ h("button", { type: "button", className: "dshdb-btn", onClick: test, disabled: testing || saving },
1008
+ testing ? h(Spinner) : h(PlugIcon, { size: 14 }),
1009
+ testing ? t("testing") : t("test")
1010
+ ),
1011
+ h("div", { className: "right" },
1012
+ h("button", { type: "button", className: "dshdb-btn", onClick: props.onClose, disabled: saving }, t("cancel")),
1013
+ h("button", { type: "button", className: "dshdb-btn primary", onClick: save, disabled: saving || testing },
1014
+ saving ? h(Spinner) : h(CheckIcon, { size: 14 }),
1015
+ saving ? t("saving") : t("save")
1016
+ )
1017
+ )
1018
+ )
1019
+ )
1020
+ );
1021
+ }
1022
+
1023
+ // ── data grid ───────────────────────────────────────────────────────
1024
+ function DataGrid(props) {
1025
+ var t = props.t;
1026
+ var result = props.result;
1027
+ var columns = result && result.columns ? result.columns : [];
1028
+ var rows = result && result.rows ? result.rows : [];
1029
+ var offset = result && result.offset ? result.offset : 0;
1030
+ if (!result) {
1031
+ return h("div", { className: "dshdb-placeholder" }, h("div", null, props.t("noRows")));
1032
+ }
1033
+ return h("div", { className: "dshdb-gridscroll" },
1034
+ h("table", { className: "dshdb-table" },
1035
+ h("thead", null, h("tr", null,
1036
+ h("th", { className: "idx" }, t("rowIndex")),
1037
+ columns.map(function (name) { return h("th", { key: name }, name); })
1038
+ )),
1039
+ h("tbody", null,
1040
+ rows.length === 0
1041
+ ? h("tr", null, h("td", { colSpan: columns.length + 1 }, h("span", { className: "dshdb-null" }, t("noRows"))))
1042
+ : rows.map(function (row, index) {
1043
+ return h("tr", { key: index },
1044
+ h("td", { className: "idx" }, fmtNumber(offset + index + 1)),
1045
+ columns.map(function (name) {
1046
+ var text = cellText(row ? row[name] : null);
1047
+ var numeric = typeof text === "string" && /^-?\d+(\.\d+)?$/.test(text);
1048
+ return h("td", { key: name, title: text === null ? "NULL" : text },
1049
+ text === null ? h("span", { className: "dshdb-null" }, "NULL") : h("span", { className: numeric ? "dshdb-num" : "" }, text)
1050
+ );
1051
+ })
1052
+ );
1053
+ })
1054
+ )
1055
+ )
1056
+ );
1057
+ }
1058
+
1059
+ // ── main page ───────────────────────────────────────────────────────
1060
+ function DbStudioPage(props) {
1061
+ var t = props.t || function (key) { return key; };
1062
+ var [overview, setOverview] = React.useState(null);
1063
+ var [overviewError, setOverviewError] = React.useState(null);
1064
+ var [activeId, setActiveId] = React.useState(null);
1065
+ var [tables, setTables] = React.useState([]);
1066
+ var [tablesLoading, setTablesLoading] = React.useState(false);
1067
+ var [tableQuery, setTableQuery] = React.useState("");
1068
+ var [selectedTable, setSelectedTable] = React.useState(null);
1069
+ var [schema, setSchema] = React.useState(null);
1070
+ var [showSchema, setShowSchema] = React.useState(false);
1071
+ var [page, setPage] = React.useState(0);
1072
+ var [pageSize, setPageSize] = React.useState(50);
1073
+ var [result, setResult] = React.useState(null);
1074
+ var [rowsLoading, setRowsLoading] = React.useState(false);
1075
+ var [rowsError, setRowsError] = React.useState(null);
1076
+ var [tab, setTab] = React.useState("data");
1077
+ var [sql, setSql] = React.useState("");
1078
+ var [sqlResult, setSqlResult] = React.useState(null);
1079
+ var [sqlError, setSqlError] = React.useState(null);
1080
+ var [sqlLoading, setSqlLoading] = React.useState(false);
1081
+ var [sqlHistory, setSqlHistory] = React.useState([]);
1082
+ var [dialog, setDialog] = React.useState(null);
1083
+ var [pendingDelete, setPendingDelete] = React.useState(null);
1084
+ var [busy, setBusy] = React.useState(false);
1085
+
1086
+ var activeConnection = overview && overview.connections ? overview.connections.find(function (row) { return row.id === activeId; }) : null;
1087
+
1088
+ React.useEffect(function () {
1089
+ var alive = true;
1090
+ setOverviewError(null);
1091
+ api({ action: "overview" }).then(function (data) {
1092
+ if (!alive) return;
1093
+ setOverview(data);
1094
+ setActiveId(function (current) {
1095
+ if (current && data.connections.some(function (row) { return row.id === current; })) return current;
1096
+ return data.connections.length ? data.connections[0].id : null;
1097
+ });
1098
+ }).catch(function (error) {
1099
+ if (alive) setOverviewError(interpolate(t("loadFailed"), { error: error.message }));
1100
+ });
1101
+ return function () { alive = false; };
1102
+ }, []);
1103
+
1104
+ React.useEffect(function () {
1105
+ if (!activeId) { setTables([]); setSelectedTable(null); setSchema(null); setResult(null); return; }
1106
+ var alive = true;
1107
+ setTablesLoading(true);
1108
+ setTables([]);
1109
+ setSelectedTable(null);
1110
+ setSchema(null);
1111
+ setResult(null);
1112
+ setRowsError(null);
1113
+ api({ action: "tables", id: activeId }).then(function (data) {
1114
+ if (!alive) return;
1115
+ var rows = data.tables || [];
1116
+ setTables(rows);
1117
+ if (rows.length) selectTable(rows[0].name, 0, true);
1118
+ }).catch(function (error) {
1119
+ if (alive) setTables([]);
1120
+ }).finally(function () { if (alive) setTablesLoading(false); });
1121
+ return function () { alive = false; };
1122
+ }, [activeId]);
1123
+
1124
+ function selectConnection(id) {
1125
+ setActiveId(id);
1126
+ setTab("data");
1127
+ setSql("");
1128
+ setSqlResult(null);
1129
+ setSqlError(null);
1130
+ setPage(0);
1131
+ }
1132
+
1133
+ function selectTable(name, nextPage, internal) {
1134
+ setSelectedTable(name);
1135
+ setPage(nextPage || 0);
1136
+ setRowsLoading(true);
1137
+ setRowsError(null);
1138
+ setResult(null);
1139
+ setShowSchema(false);
1140
+ if (!internal) setTab("data");
1141
+ var targetPage = nextPage || 0;
1142
+ Promise.all([
1143
+ api({ action: "describe", id: activeId, table: name }),
1144
+ api({ action: "rows", id: activeId, table: name, limit: pageSize, offset: targetPage * pageSize })
1145
+ ]).then(function (values) {
1146
+ setSchema(values[0]);
1147
+ setResult(values[1]);
1148
+ }).catch(function (error) {
1149
+ setRowsError(interpolate(t("loadFailed"), { error: error.message }));
1150
+ }).finally(function () { setRowsLoading(false); });
1151
+ }
1152
+
1153
+ function reloadRows() {
1154
+ if (!selectedTable) return;
1155
+ setRowsLoading(true);
1156
+ setRowsError(null);
1157
+ api({ action: "rows", id: activeId, table: selectedTable, limit: pageSize, offset: page * pageSize }).then(function (data) {
1158
+ setResult(data);
1159
+ }).catch(function (error) {
1160
+ setRowsError(interpolate(t("loadFailed"), { error: error.message }));
1161
+ }).finally(function () { setRowsLoading(false); });
1162
+ }
1163
+
1164
+ function changePage(nextPage) {
1165
+ if (!selectedTable || nextPage < 0) return;
1166
+ var maxPage = result && result.total ? Math.max(0, Math.ceil(result.total / pageSize) - 1) : 0;
1167
+ if (nextPage > maxPage) return;
1168
+ setPage(nextPage);
1169
+ setRowsLoading(true);
1170
+ setRowsError(null);
1171
+ api({ action: "rows", id: activeId, table: selectedTable, limit: pageSize, offset: nextPage * pageSize }).then(function (data) {
1172
+ setResult(data);
1173
+ }).catch(function (error) {
1174
+ setRowsError(interpolate(t("loadFailed"), { error: error.message }));
1175
+ }).finally(function () { setRowsLoading(false); });
1176
+ }
1177
+
1178
+ function changePageSize(value) {
1179
+ var size = Number(value) || 50;
1180
+ setPageSize(size);
1181
+ if (!selectedTable) return;
1182
+ setPage(0);
1183
+ setRowsLoading(true);
1184
+ api({ action: "rows", id: activeId, table: selectedTable, limit: size, offset: 0 }).then(function (data) {
1185
+ setResult(data);
1186
+ }).catch(function (error) {
1187
+ setRowsError(interpolate(t("loadFailed"), { error: error.message }));
1188
+ }).finally(function () { setRowsLoading(false); });
1189
+ }
1190
+
1191
+ function runSql() {
1192
+ var text = sql.trim();
1193
+ if (!text) return;
1194
+ setSqlLoading(true);
1195
+ setSqlError(null);
1196
+ setSqlResult(null);
1197
+ var readOnly = isReadOnlySql(text);
1198
+ var body = readOnly
1199
+ ? { action: "query", id: activeId, sql: text, limit: Number(overview?.config?.maxRows) || 200 }
1200
+ : { action: "execute", id: activeId, sql: text };
1201
+ api(body).then(function (data) {
1202
+ setSqlResult({ readOnly: readOnly, data: data });
1203
+ setSqlHistory(function (previous) {
1204
+ return [text].concat(previous.filter(function (item) { return item !== text; })).slice(0, 5);
1205
+ });
1206
+ }).catch(function (error) {
1207
+ setSqlError(interpolate(t("queryFailed"), { error: error.message }));
1208
+ }).finally(function () { setSqlLoading(false); });
1209
+ }
1210
+
1211
+ function refreshTables() {
1212
+ if (!activeId) return;
1213
+ setBusy(true);
1214
+ api({ action: "tables", id: activeId }).then(function (data) {
1215
+ setTables(data.tables || []);
1216
+ if (!selectedTable && data.tables && data.tables.length) selectTable(data.tables[0].name, 0, true);
1217
+ }).catch(function () {}).finally(function () { setBusy(false); });
1218
+ }
1219
+
1220
+ function reloadOverview(preserveActive) {
1221
+ return api({ action: "overview" }).then(function (data) {
1222
+ setOverview(data);
1223
+ if (preserveActive && data.connections.some(function (row) { return row.id === activeId; })) return;
1224
+ setActiveId(data.connections.length ? data.connections[0].id : null);
1225
+ });
1226
+ }
1227
+
1228
+ async function saveDemo() {
1229
+ setBusy(true);
1230
+ try {
1231
+ var data = await api({ action: "seed-demo" });
1232
+ setOverview(data);
1233
+ setActiveId(data.connections.length ? data.connections[0].id : null);
1234
+ } catch (error) {
1235
+ setOverviewError(interpolate(t("loadFailed"), { error: error.message }));
1236
+ } finally {
1237
+ setBusy(false);
1238
+ }
1239
+ }
1240
+
1241
+ async function removeConnection() {
1242
+ if (!pendingDelete) return;
1243
+ setBusy(true);
1244
+ try {
1245
+ await api({ action: "remove", id: pendingDelete.id });
1246
+ setPendingDelete(null);
1247
+ await reloadOverview(false);
1248
+ } catch (error) {
1249
+ setOverviewError(interpolate(t("saveFailed"), { error: error.message }));
1250
+ setPendingDelete(null);
1251
+ } finally {
1252
+ setBusy(false);
1253
+ }
1254
+ }
1255
+
1256
+ function openNew() { setDialog({ mode: "new" }); }
1257
+ function openEdit(connection) { setDialog({ mode: "edit", connection: connection }); }
1258
+
1259
+ var filteredTables = tableQuery.trim()
1260
+ ? tables.filter(function (row) { return row.name.toLowerCase().indexOf(tableQuery.trim().toLowerCase()) !== -1; })
1261
+ : tables;
1262
+ var total = result && result.total ? result.total : 0;
1263
+ var maxPage = Math.max(0, Math.ceil(total / pageSize) - 1);
1264
+ var missingDrivers = [];
1265
+ if (overview && overview.drivers) {
1266
+ if (!overview.drivers.postgres) missingDrivers.push("pg");
1267
+ if (!overview.drivers.mysql) missingDrivers.push("mysql2");
1268
+ }
1269
+
1270
+ var header = h("header", { className: "dshdb-header" },
1271
+ h("div", { className: "dshdb-titlewrap" },
1272
+ h("div", { className: "dshdb-titlemark" }, h(DatabaseIcon, { size: 21 })),
1273
+ h("div", null,
1274
+ h("h1", { className: "dshdb-title" }, t("title")),
1275
+ h("p", { className: "dshdb-subtitle" }, t("subtitle"))
1276
+ )
1277
+ ),
1278
+ h("div", { className: "dshdb-actions" },
1279
+ h("button", { type: "button", className: "dshdb-btn ghost", title: t("refresh"), onClick: function () { reloadOverview(true); refreshTables(); }, disabled: busy }, h(RefreshIcon, { size: 15 })),
1280
+ h("button", { type: "button", className: "dshdb-btn primary", onClick: openNew }, h(PlusIcon, { size: 15 }), h("span", { className: "dshdb-btnlabel" }, t("newConnection")))
1281
+ )
1282
+ );
1283
+
1284
+ var sidebar = h("aside", { className: "dshdb-sidebar" },
1285
+ h("div", { className: "dshdb-sidehead" },
1286
+ h("span", null, t("connections") + " · " + (overview ? overview.connections.length : 0)),
1287
+ h(IconButton, { title: t("newConnection"), onClick: openNew }, h(PlusIcon, { size: 14 }))
1288
+ ),
1289
+ h("div", { className: "dshdb-connlist" },
1290
+ overview && overview.connections.map(function (connection) {
1291
+ return h(ConnectionRow, {
1292
+ key: connection.id,
1293
+ connection: connection,
1294
+ active: connection.id === activeId,
1295
+ t: t,
1296
+ onSelect: selectConnection,
1297
+ onEdit: openEdit,
1298
+ onRemove: function (row) { setPendingDelete(row); }
1299
+ });
1300
+ })
1301
+ ),
1302
+ missingDrivers.length
1303
+ ? h("div", { className: "dshdb-sidefoot" }, missingDrivers.map(function (driver) {
1304
+ return h("div", { key: driver }, interpolate(t("driverMissing"), { driver: driver }));
1305
+ }))
1306
+ : null
1307
+ );
1308
+
1309
+ var main = null;
1310
+ if (overviewError) {
1311
+ main = h("div", { className: "dshdb-empty" }, h("div", { className: "dshdb-emptycard" }, h("div", { className: "dshdb-error" }, overviewError)));
1312
+ } else if (!overview) {
1313
+ main = h("div", { className: "dshdb-empty" }, h(Spinner));
1314
+ } else if (!activeConnection) {
1315
+ main = h(EmptyState, { t: t, onNew: openNew, onDemo: overview.connections.length === 0 ? saveDemo : null });
1316
+ } else {
1317
+ var tableList = h("div", { className: "dshdb-tablelist" },
1318
+ h("div", { className: "dshdb-searchwrap" },
1319
+ h(SearchIcon, { size: 14 }),
1320
+ h("input", {
1321
+ className: "dshdb-search",
1322
+ value: tableQuery,
1323
+ placeholder: t("searchTable"),
1324
+ onChange: function (event) { setTableQuery(event.target.value); }
1325
+ })
1326
+ ),
1327
+ h("div", { className: "dshdb-tablelistscroll" },
1328
+ tablesLoading ? h("div", { className: "dshdb-placeholder" }, h(Spinner))
1329
+ : filteredTables.length === 0 ? h("div", { className: "dshdb-placeholder" }, t("noTables"))
1330
+ : filteredTables.map(function (row) {
1331
+ return h("button", {
1332
+ key: (row.schema || "") + "." + row.name,
1333
+ type: "button",
1334
+ className: "dshdb-tablerow" + (selectedTable === row.name ? " active" : ""),
1335
+ onClick: function () { selectTable(row.name, 0); },
1336
+ title: row.name
1337
+ },
1338
+ h(TableIcon, { size: 14 }),
1339
+ h("span", null, row.name),
1340
+ row.type === "view" ? h("span", { className: "dshdb-badge" }, t("view")) : null
1341
+ );
1342
+ })
1343
+ )
1344
+ );
1345
+
1346
+ var dataPane = h("div", { className: "dshdb-pane" },
1347
+ tableList,
1348
+ h("div", { className: "dshdb-grid" },
1349
+ h("div", { className: "dshdb-gridbar" },
1350
+ h("div", { className: "dshdb-gridtitle" },
1351
+ h(TableIcon, { size: 15 }),
1352
+ h("span", null, selectedTable || t("selectTable")),
1353
+ selectedTable ? h("span", { className: "dshdb-gridmeta" }, interpolate(t("totalRows"), { n: fmtNumber(total) })) : null
1354
+ ),
1355
+ h("div", { className: "dshdb-gridtools" },
1356
+ h("select", {
1357
+ className: "dshdb-tableselect",
1358
+ value: selectedTable || "",
1359
+ "aria-label": t("tables"),
1360
+ onChange: function (event) { if (event.target.value) selectTable(event.target.value, 0); }
1361
+ }, tables.map(function (row) {
1362
+ var tableName = row.schema ? row.schema + "." + row.name : row.name;
1363
+ return h("option", { key: tableName, value: tableName }, tableName);
1364
+ })),
1365
+ h("button", { type: "button", className: "dshdb-btn small", onClick: function () { setShowSchema(function (value) { return !value; }); }, disabled: !selectedTable },
1366
+ h(ChevronDownIcon, { size: 13, style: { transform: showSchema ? "rotate(180deg)" : "none" } }), t("schema")),
1367
+ h(IconButton, { title: t("refresh"), onClick: reloadRows, disabled: !selectedTable }, h(RefreshIcon, { size: 14 }))
1368
+ )
1369
+ ),
1370
+ showSchema && schema ? h("div", { className: "dshdb-schemaPanel" },
1371
+ h("div", { className: "dshdb-cols" }, (schema.columns || []).map(function (column) {
1372
+ return h("span", { key: column.name, className: "dshdb-col" + (column.primaryKey ? " pk" : ""), title: column.type },
1373
+ h("b", null, column.name),
1374
+ h("em", null, column.type),
1375
+ column.primaryKey ? h("em", null, t("pk")) : null,
1376
+ column.notNull ? h("em", null, "NOT NULL") : null,
1377
+ column.defaultValue !== null && column.defaultValue !== undefined ? h("em", null, "=" + String(column.defaultValue)) : null
1378
+ );
1379
+ }))
1380
+ ) : null,
1381
+ rowsError ? h("div", { className: "dshdb-sqlpane" }, h("div", { className: "dshdb-error" }, rowsError)) : null,
1382
+ rowsLoading ? h("div", { className: "dshdb-placeholder" }, h(Spinner)) : null,
1383
+ !rowsLoading && !rowsError && !selectedTable ? h("div", { className: "dshdb-placeholder" }, h("div", null, h("div", null, t("selectTable")), h("div", { className: "dshdb-gridmeta" }, t("selectTableHint")))) : null,
1384
+ !rowsLoading && !rowsError && selectedTable ? h(DataGrid, { t: t, result: result }) : null,
1385
+ selectedTable ? h("div", { className: "dshdb-pager" },
1386
+ h("span", null, interpolate(t("page"), { page: page + 1 }) + " / " + (maxPage + 1)),
1387
+ h("select", { value: pageSize, onChange: function (event) { changePageSize(event.target.value); }, "aria-label": t("pageSize") },
1388
+ [25, 50, 100, 200].map(function (size) { return h("option", { key: size, value: size }, size + " / " + t("pageSize")); })
1389
+ ),
1390
+ h("button", { type: "button", className: "dshdb-btn small", onClick: function () { changePage(page - 1); }, disabled: page <= 0 }, h(ChevronLeftIcon, { size: 13 })),
1391
+ h("button", { type: "button", className: "dshdb-btn small", onClick: function () { changePage(page + 1); }, disabled: page >= maxPage }, h(ChevronRightIcon, { size: 13 }))
1392
+ ) : null
1393
+ )
1394
+ );
1395
+
1396
+ var sqlPane = h("div", { className: "dshdb-sqlpane" },
1397
+ h("div", { className: "dshdb-editorwrap" },
1398
+ h("div", { className: "dshdb-editortop" },
1399
+ h("span", null, (KIND_LABEL[activeConnection.kind] || activeConnection.kind) + " · " + (activeConnection.database || activeConnection.file || "")),
1400
+ h("span", null, "⌘ / Ctrl + Enter")
1401
+ ),
1402
+ h("textarea", {
1403
+ className: "dshdb-editor",
1404
+ value: sql,
1405
+ spellCheck: false,
1406
+ placeholder: t("sqlPlaceholder"),
1407
+ onChange: function (event) { setSql(event.target.value); },
1408
+ onKeyDown: function (event) {
1409
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") { event.preventDefault(); runSql(); }
1410
+ }
1411
+ })
1412
+ ),
1413
+ h("div", { className: "dshdb-sqlbar" },
1414
+ h("div", { className: "dshdb-hint" }, t("sqlHint")),
1415
+ h("div", { style: { display: "flex", gap: "7px", alignItems: "center" } },
1416
+ sql ? h("button", { type: "button", className: "dshdb-btn small ghost", onClick: function () { setSql(""); setSqlResult(null); setSqlError(null); } }, t("clear")) : null,
1417
+ h("button", { type: "button", className: "dshdb-btn primary", onClick: runSql, disabled: sqlLoading || !sql.trim() },
1418
+ sqlLoading ? h(Spinner) : h(PlayIcon, { size: 13 }),
1419
+ sqlLoading ? t("running") : t("run")
1420
+ )
1421
+ )
1422
+ ),
1423
+ sqlHistory.length ? h("div", { className: "dshdb-history" }, sqlHistory.map(function (item, index) {
1424
+ return h("button", { key: index, type: "button", title: item, onClick: function () { setSql(item); } }, item);
1425
+ })) : null,
1426
+ sqlError ? h("div", { className: "dshdb-error" }, sqlError) : null,
1427
+ sqlResult ? h("div", { className: "dshdb-result" },
1428
+ h("div", { className: "dshdb-resulthead" },
1429
+ h("span", null, t("result")),
1430
+ h("span", { className: "dshdb-gridmeta" },
1431
+ typeof sqlResult.data.elapsedMs === "number" ? sqlResult.data.elapsedMs + " ms" : "",
1432
+ sqlResult.data.truncated ? " · " + t("truncated") : ""
1433
+ )
1434
+ ),
1435
+ h("div", { className: "dshdb-resultbody" },
1436
+ sqlResult.readOnly
1437
+ ? h(DataGrid, { t: t, result: { columns: sqlResult.data.columns, rows: sqlResult.data.rows, offset: 0 } })
1438
+ : h("div", { style: { padding: "10px 4px" } },
1439
+ h("div", { className: "dshdb-success" }, h(CheckIcon, { size: 15 }), interpolate(t("affected"), { n: sqlResult.data.changes ?? 0 })),
1440
+ sqlResult.data.lastInsertRowid !== null && sqlResult.data.lastInsertRowid !== undefined && sqlResult.data.lastInsertRowid !== ""
1441
+ ? h("div", { className: "dshdb-gridmeta", style: { marginTop: "8px" } }, "last insert id = " + sqlResult.data.lastInsertRowid)
1442
+ : null
1443
+ )
1444
+ )
1445
+ ) : h("div", { className: "dshdb-result" }, h("div", { className: "dshdb-placeholder" }, t("result")))
1446
+ );
1447
+
1448
+ main = h("section", { className: "dshdb-main" },
1449
+ h("div", { className: "dshdb-mainhead" },
1450
+ h("div", { className: "dshdb-mainheadtop" },
1451
+ h("div", { className: "dshdb-activeconn" },
1452
+ h("select", {
1453
+ className: "dshdb-connselect",
1454
+ value: activeId || "",
1455
+ "aria-label": t("connections"),
1456
+ onChange: function (event) { selectConnection(event.target.value); }
1457
+ }, overview.connections.map(function (row) {
1458
+ return h("option", { key: row.id, value: row.id }, row.name);
1459
+ })),
1460
+ h("span", { className: "dshdb-connicon" }, h(DatabaseIcon, { size: 16 })),
1461
+ h("div", { style: { minWidth: 0 } },
1462
+ h("div", { className: "dshdb-activeconnname" }, activeConnection.name),
1463
+ h("div", { className: "dshdb-activeconntarget" }, activeConnection.kind === "sqlite" ? activeConnection.file : (activeConnection.user + "@" + activeConnection.host + ":" + activeConnection.port + "/" + activeConnection.database))
1464
+ ),
1465
+ h(KindBadge, { kind: activeConnection.kind, demo: activeConnection.demo, t: t }),
1466
+ activeConnection.readOnly ? h("span", { className: "dshdb-badge" }, t("readOnly")) : null
1467
+ ),
1468
+ h("div", { className: "dshdb-gridtools" },
1469
+ h("button", { type: "button", className: "dshdb-btn small", onClick: function () { openEdit(activeConnection); } }, h(EditIcon, { size: 13 }), t("edit")),
1470
+ h("button", { type: "button", className: "dshdb-btn small danger", onClick: function () { setPendingDelete(activeConnection); } }, h(TrashIcon, { size: 13 }), t("remove"))
1471
+ )
1472
+ ),
1473
+ h("div", { className: "dshdb-tabs", role: "tablist", "aria-label": t("tabsAria") },
1474
+ h("button", { type: "button", role: "tab", "aria-selected": tab === "data", className: "dshdb-tab" + (tab === "data" ? " active" : ""), onClick: function () { setTab("data"); } }, h(TableIcon, { size: 14 }), t("data")),
1475
+ h("button", { type: "button", role: "tab", "aria-selected": tab === "sql", className: "dshdb-tab" + (tab === "sql" ? " active" : ""), onClick: function () { setTab("sql"); } }, h(PlayIcon, { size: 13 }), t("sql"))
1476
+ )
1477
+ ),
1478
+ tab === "data" ? dataPane : sqlPane,
1479
+ h("div", { className: "dshdb-chatbar" },
1480
+ h(SparkIcon, { size: 14 }),
1481
+ h("div", null, h("b", null, t("inChat") + " · "), t("inChatHint"))
1482
+ )
1483
+ );
1484
+ }
1485
+
1486
+ return h("div", { className: "dshdb-page" },
1487
+ header,
1488
+ h("div", { className: "dshdb-body" }, sidebar, main || h("div", { className: "dshdb-main" })),
1489
+ dialog ? h(ConnectionDialog, {
1490
+ t: t,
1491
+ connection: dialog.connection || null,
1492
+ onClose: function () { setDialog(null); },
1493
+ onSaved: function () { setDialog(null); reloadOverview(true); }
1494
+ }) : null,
1495
+ pendingDelete ? h("div", { className: "dshdb-overlay", onMouseDown: function (event) { if (event.target === event.currentTarget) setPendingDelete(null); } },
1496
+ h("div", { className: "dshdb-dialog small", role: "dialog", "aria-modal": "true" },
1497
+ h("div", { className: "dshdb-dialoghead" },
1498
+ h("div", null,
1499
+ h("h2", { className: "dshdb-dialogtitle" }, t("removeTitle")),
1500
+ h("p", { className: "dshdb-dialogdesc" }, "\"" + (pendingDelete.name || pendingDelete.id) + "\" — " + t("removeDesc"))
1501
+ ),
1502
+ h(IconButton, { title: t("close"), onClick: function () { setPendingDelete(null); } }, h(CloseIcon, { size: 16 }))
1503
+ ),
1504
+ h("div", { className: "dshdb-dialogfoot" },
1505
+ h("div", { className: "right" },
1506
+ h("button", { type: "button", className: "dshdb-btn", onClick: function () { setPendingDelete(null); } }, t("cancel")),
1507
+ h("button", { type: "button", className: "dshdb-btn danger", onClick: removeConnection, disabled: busy }, h(TrashIcon, { size: 14 }), t("confirmRemove"))
1508
+ )
1509
+ )
1510
+ )
1511
+ ) : null
1512
+ );
1513
+ }
1514
+
1515
+ // ── composer: database multi-select ─────────────────────────────────
1516
+ /** Anchored popover used by the composer picker (portal when available). */
1517
+ function PickerPopover(props) {
1518
+ var rect = props.rect;
1519
+ if (!rect) return null;
1520
+ var viewportWidth = typeof window !== "undefined" ? window.innerWidth : 1200;
1521
+ var viewportHeight = typeof window !== "undefined" ? window.innerHeight : 800;
1522
+ var width = Math.min(360, Math.max(280, viewportWidth - 24));
1523
+ var left = Math.min(Math.max(8, rect.left), Math.max(8, viewportWidth - width - 8));
1524
+ var bottom = Math.max(8, viewportHeight - rect.top + 8);
1525
+ var content = h("div", {
1526
+ className: "dshdb-picker-pop",
1527
+ role: "dialog",
1528
+ "aria-label": props.label,
1529
+ style: { position: "fixed", left: left, bottom: bottom, width: width, zIndex: 90 },
1530
+ onMouseDown: function (event) { event.stopPropagation(); },
1531
+ onClick: function (event) { event.stopPropagation(); }
1532
+ }, props.children);
1533
+ if (ReactDOM && typeof ReactDOM.createPortal === "function" && typeof document !== "undefined" && document.body) {
1534
+ return ReactDOM.createPortal(content, document.body);
1535
+ }
1536
+ return content;
1537
+ }
1538
+
1539
+ function ConversationDbPicker(props) {
1540
+ var t = props.t;
1541
+ var sessionId = props.sessionId;
1542
+ var entry = useSessionDb(sessionId);
1543
+ var state = entry.state;
1544
+ var [open, setOpen] = React.useState(false);
1545
+ var [rect, setRect] = React.useState(null);
1546
+ var buttonRef = React.useRef(null);
1547
+
1548
+ function measure() {
1549
+ var node = buttonRef.current;
1550
+ if (!node) return;
1551
+ var box = node.getBoundingClientRect();
1552
+ setRect({ left: box.left, top: box.top, width: box.width, height: box.height });
1553
+ }
1554
+
1555
+ React.useEffect(function () {
1556
+ if (!open) return;
1557
+ measure();
1558
+ function update() { measure(); }
1559
+ window.addEventListener("resize", update);
1560
+ window.addEventListener("scroll", update, true);
1561
+ return function () {
1562
+ window.removeEventListener("resize", update);
1563
+ window.removeEventListener("scroll", update, true);
1564
+ };
1565
+ }, [open]);
1566
+
1567
+ React.useEffect(function () {
1568
+ if (!open) return;
1569
+ function onDown(event) {
1570
+ var target = event.target;
1571
+ if (target && target.closest && (target.closest(".dshdb-picker-pop") || target.closest(".dshdb-picker-wrap"))) return;
1572
+ setOpen(false);
1573
+ }
1574
+ function onKey(event) {
1575
+ if (event.key === "Escape") setOpen(false);
1576
+ }
1577
+ document.addEventListener("mousedown", onDown);
1578
+ document.addEventListener("keydown", onKey);
1579
+ return function () {
1580
+ document.removeEventListener("mousedown", onDown);
1581
+ document.removeEventListener("keydown", onKey);
1582
+ };
1583
+ }, [open]);
1584
+
1585
+ var all = state ? state.connections || [] : [];
1586
+ var selectedIds = state ? state.selectedIds || [] : [];
1587
+ var selected = state ? state.selected || [] : [];
1588
+ var count = selectedIds.length;
1589
+ var label = "选择数据库";
1590
+ if (selected.length === 1) label = selected[0].name;
1591
+ else if (selected.length > 1) label = interpolate(t("databaseCount"), { n: selected.length });
1592
+ else if (all.length === 0) label = "连接数据库";
1593
+
1594
+ function toggle(id) {
1595
+ var next = selectedIds.includes(id)
1596
+ ? selectedIds.filter(function (item) { return item !== id; })
1597
+ : selectedIds.concat([id]);
1598
+ setSessionSelection(sessionId, next).catch(function () {});
1599
+ }
1600
+
1601
+ function setAll(ids) {
1602
+ setSessionSelection(sessionId, ids).catch(function () {});
1603
+ }
1604
+
1605
+ function openStudio() {
1606
+ setOpen(false);
1607
+ if (props.openStudio) props.openStudio();
1608
+ else openGlobalPanel();
1609
+ }
1610
+
1611
+ var popover = open ? h(PickerPopover, {
1612
+ rect: rect,
1613
+ label: t("title")
1614
+ },
1615
+ h("div", { className: "dshdb-picker-head" },
1616
+ h("div", null,
1617
+ h("div", { className: "dshdb-picker-title" }, t("title")),
1618
+ h("div", { className: "dshdb-picker-sub" }, count + " / " + all.length)
1619
+ ),
1620
+ h("div", { className: "dshdb-picker-headactions" },
1621
+ h("button", { type: "button", className: "dshdb-picker-link", onClick: function () { setAll(all.map(function (row) { return row.id; })); } }, t("selectAll")),
1622
+ h("button", { type: "button", className: "dshdb-picker-link", onClick: function () { setAll([]); } }, t("clearAll"))
1623
+ )
1624
+ ),
1625
+ h("div", { className: "dshdb-picker-list" },
1626
+ all.length === 0
1627
+ ? h("div", { className: "dshdb-picker-empty" },
1628
+ h("div", null, t("noConnections")),
1629
+ h("button", { type: "button", className: "dshdb-btn primary", onClick: openStudio }, h(PlusIcon, { size: 14 }), t("newConnection")))
1630
+ : all.map(function (connection) {
1631
+ return h("label", { key: connection.id, className: "dshdb-picker-item" + (selectedIds.includes(connection.id) ? " checked" : "") },
1632
+ h("input", {
1633
+ type: "checkbox",
1634
+ checked: selectedIds.includes(connection.id),
1635
+ onChange: function () { toggle(connection.id); }
1636
+ }),
1637
+ h("span", { className: "dshdb-picker-icon" }, h(DatabaseIcon, { size: 15 })),
1638
+ h("span", { className: "dshdb-picker-meta" },
1639
+ h("span", { className: "dshdb-picker-name" }, connection.name),
1640
+ h("span", { className: "dshdb-picker-target" }, sessionKindLabel(connection.kind) + " · " + connectionSubtitle(connection))
1641
+ ),
1642
+ connection.demo ? h("span", { className: "dshdb-badge demo" }, t("demo")) : null
1643
+ );
1644
+ })
1645
+ ),
1646
+ entry.error ? h("div", { className: "dshdb-picker-error" }, entry.error) : null,
1647
+ h("div", { className: "dshdb-picker-foot" },
1648
+ h("button", { type: "button", className: "dshdb-picker-link", onClick: openStudio }, t("openPanel")),
1649
+ h("button", { type: "button", className: "dshdb-btn small primary", onClick: function () { setOpen(false); } }, t("done"))
1650
+ )
1651
+ ) : null;
1652
+
1653
+ return h("div", { className: "dshdb-picker-wrap" },
1654
+ h("button", {
1655
+ ref: buttonRef,
1656
+ type: "button",
1657
+ className: "dshdb-picker-btn" + (count > 0 ? " has-selection" : "") + (open ? " open" : ""),
1658
+ title: selected.length ? selected.map(function (row) { return row.name; }).join(", ") : t("selectDatabase"),
1659
+ "aria-haspopup": "dialog",
1660
+ "aria-expanded": open,
1661
+ onMouseDown: function (event) { event.stopPropagation(); },
1662
+ onClick: function (event) { event.stopPropagation(); setOpen(function (value) { return !value; }); }
1663
+ },
1664
+ h(DatabaseIcon, { size: 14 }),
1665
+ h("span", { className: "dshdb-picker-btnlabel" }, label),
1666
+ h(ChevronDownIcon, { size: 11 })
1667
+ ),
1668
+ popover
1669
+ );
1670
+ }
1671
+
1672
+ // ── composer: connection status strip ───────────────────────────────
1673
+ //
1674
+ // One pill, never a wrapping wall of chips: the composer dock already
1675
+ // shares its row with the shipped context/cost readouts, and a second or
1676
+ // third database used to make that row fold. The pill names the selection
1677
+ // (or its count) and opens a small popover with the per-connection detail.
1678
+ function ConversationDbStatus(props) {
1679
+ var t = props.t;
1680
+ var sessionId = props.sessionId;
1681
+ var entry = useSessionDb(sessionId);
1682
+ var state = entry.state;
1683
+ var selected = state ? state.selected || [] : [];
1684
+ var key = selected.map(function (row) { return row.id; }).join(",");
1685
+ var [open, setOpen] = React.useState(false);
1686
+ var [rect, setRect] = React.useState(null);
1687
+ var buttonRef = React.useRef(null);
1688
+
1689
+ React.useEffect(function () {
1690
+ if (!selected.length) return;
1691
+ loadSessionStatus(sessionId);
1692
+ var timer = window.setInterval(function () { loadSessionStatus(sessionId); }, 30000);
1693
+ return function () { window.clearInterval(timer); };
1694
+ }, [sessionId, key]);
1695
+
1696
+ React.useEffect(function () {
1697
+ if (!open) return;
1698
+ var node = buttonRef.current;
1699
+ if (node) {
1700
+ var box = node.getBoundingClientRect();
1701
+ setRect({ left: box.left, top: box.top, width: box.width, height: box.height });
1702
+ }
1703
+ function update() {
1704
+ var el = buttonRef.current;
1705
+ if (!el) return;
1706
+ var b = el.getBoundingClientRect();
1707
+ setRect({ left: b.left, top: b.top, width: b.width, height: b.height });
1708
+ }
1709
+ function onDown(event) {
1710
+ var target = event.target;
1711
+ if (target && target.closest && (target.closest(".dshdb-picker-pop") || target.closest(".dshdb-statusdock"))) return;
1712
+ setOpen(false);
1713
+ }
1714
+ function onKey(event) {
1715
+ if (event.key === "Escape") setOpen(false);
1716
+ }
1717
+ window.addEventListener("resize", update);
1718
+ window.addEventListener("scroll", update, true);
1719
+ document.addEventListener("mousedown", onDown);
1720
+ document.addEventListener("keydown", onKey);
1721
+ return function () {
1722
+ window.removeEventListener("resize", update);
1723
+ window.removeEventListener("scroll", update, true);
1724
+ document.removeEventListener("mousedown", onDown);
1725
+ document.removeEventListener("keydown", onKey);
1726
+ };
1727
+ }, [open]);
1728
+
1729
+ if (!selected.length) return null;
1730
+
1731
+ var statusById = {};
1732
+ ((entry.status && entry.status.connections) || []).forEach(function (row) { statusById[row.id] = row; });
1733
+ var checked = selected.map(function (connection) { return statusById[connection.id]; }).filter(Boolean);
1734
+ var failed = checked.filter(function (row) { return row.ok === false; }).length;
1735
+ var connected = checked.filter(function (row) { return row.ok === true; }).length;
1736
+ var checking = checked.length < selected.length || entry.statusLoading;
1737
+ var dotClass = failed > 0 ? " bad" : checking ? " idle" : " ok";
1738
+
1739
+ var label;
1740
+ var meta;
1741
+ if (selected.length === 1) {
1742
+ var one = selected[0];
1743
+ var oneStatus = statusById[one.id];
1744
+ label = one.name;
1745
+ meta = sessionKindLabel(one.kind);
1746
+ if (oneStatus && oneStatus.ok === true && typeof oneStatus.tableCount === "number") meta += " · " + interpolate(t("tableCount"), { n: oneStatus.tableCount });
1747
+ else if (oneStatus && oneStatus.ok === false) meta += " · " + t("statusFailed");
1748
+ else if (checking) meta += " · " + t("statusChecking");
1749
+ } else {
1750
+ label = interpolate(t("databaseCount"), { n: selected.length });
1751
+ meta = failed > 0
1752
+ ? interpolate(t("connectedOf"), { ok: connected, n: selected.length })
1753
+ : checking ? t("statusChecking") : t("allConnected");
1754
+ }
1755
+
1756
+ var title = selected.map(function (connection) {
1757
+ var status = statusById[connection.id];
1758
+ var text = connection.name + " · " + sessionKindLabel(connection.kind);
1759
+ if (status && status.ok === true && typeof status.tableCount === "number") text += " · " + interpolate(t("tableCount"), { n: status.tableCount });
1760
+ else if (status && status.ok === false) text += " · " + t("statusFailed");
1761
+ return text;
1762
+ }).join("\n");
1763
+
1764
+ var popover = open ? h(PickerPopover, { rect: rect, label: t("statusLabel") },
1765
+ h("div", { className: "dshdb-statuspop-head" },
1766
+ h("div", { className: "dshdb-picker-title" }, t("statusLabel")),
1767
+ h("div", { className: "dshdb-picker-sub" }, interpolate(t("databaseCount"), { n: selected.length }))
1768
+ ),
1769
+ h("div", { className: "dshdb-statuspop-list" }, selected.map(function (connection) {
1770
+ var status = statusById[connection.id];
1771
+ var ok = status ? status.ok : undefined;
1772
+ var detail = sessionKindLabel(connection.kind);
1773
+ if (status && status.ok === true && typeof status.tableCount === "number") detail += " · " + interpolate(t("tableCount"), { n: status.tableCount }) + (typeof status.latencyMs === "number" ? " · " + status.latencyMs + " ms" : "");
1774
+ else if (status && status.ok === false) detail += " · " + (status.error || t("statusFailed"));
1775
+ else detail += " · " + t("statusChecking");
1776
+ return h("div", { key: connection.id, className: "dshdb-statuspop-row" },
1777
+ h("span", { className: "dshdb-dot" + (ok === false ? " bad" : ok === true ? " ok" : " idle") }),
1778
+ h("span", { className: "dshdb-statuspop-meta" },
1779
+ h("span", { className: "dshdb-statuspop-name" }, connection.name),
1780
+ h("span", { className: "dshdb-statuspop-detail" }, detail)
1781
+ )
1782
+ );
1783
+ })),
1784
+ h("div", { className: "dshdb-picker-foot" },
1785
+ h("button", { type: "button", className: "dshdb-picker-link", onClick: function () { setOpen(false); } }, t("done")),
1786
+ h("button", { type: "button", className: "dshdb-btn small primary", onClick: function () { setOpen(false); if (props.openStudio) props.openStudio(); else openGlobalPanel(); } }, t("openPanel"))
1787
+ )
1788
+ ) : null;
1789
+
1790
+ return h("div", { className: "dshdb-statusdock", role: "status", "aria-label": t("statusLabel") },
1791
+ h("button", {
1792
+ ref: buttonRef,
1793
+ type: "button",
1794
+ className: "dshdb-statuspill" + dotClass + (open ? " open" : ""),
1795
+ title: title,
1796
+ "aria-haspopup": "dialog",
1797
+ "aria-expanded": open,
1798
+ onMouseDown: function (event) { event.stopPropagation(); },
1799
+ onClick: function (event) { event.stopPropagation(); setOpen(function (value) { return !value; }); }
1800
+ },
1801
+ h("span", { className: "dshdb-dot" }),
1802
+ h("span", { className: "dshdb-statuspill-name" }, label),
1803
+ h("span", { className: "dshdb-statuspill-meta" }, meta),
1804
+ h(ChevronDownIcon, { size: 10 })
1805
+ ),
1806
+ popover
1807
+ );
1808
+ }
1809
+
1810
+ /** Column chips shared by the global panel and the conversation tab. */
1811
+ function SchemaChips(props) {
1812
+ var t = props.t;
1813
+ var schema = props.schema;
1814
+ if (!schema || !schema.columns) return null;
1815
+ return h("div", { className: "dshdb-schemaPanel" },
1816
+ h("div", { className: "dshdb-cols" }, schema.columns.map(function (column) {
1817
+ return h("span", { key: column.name, className: "dshdb-col" + (column.primaryKey ? " pk" : ""), title: column.type },
1818
+ h("b", null, column.name),
1819
+ h("em", null, column.type),
1820
+ column.primaryKey ? h("em", null, t("pk")) : null,
1821
+ column.notNull ? h("em", null, "NOT NULL") : null,
1822
+ column.defaultValue !== null && column.defaultValue !== undefined ? h("em", null, "=" + String(column.defaultValue)) : null
1823
+ );
1824
+ }))
1825
+ );
1826
+ }
1827
+
1828
+ /** Empty state shared by both "no connections" and "nothing selected". */
1829
+ function ConversationDbEmpty(props) {
1830
+ var t = props.t;
1831
+ return h("div", { className: "dshdb-cvempty" },
1832
+ h("div", { className: "dshdb-emptycard" },
1833
+ h("div", { className: "dshdb-emptyart" }, h(DatabaseIcon, { size: 38 })),
1834
+ h("h2", { className: "dshdb-emptytitle" }, props.title),
1835
+ h("p", { className: "dshdb-emptydesc" }, props.description),
1836
+ h("div", { className: "dshdb-emptyactions" },
1837
+ props.onDemo ? h("button", { type: "button", className: "dshdb-btn", onClick: props.onDemo, disabled: props.busy }, h(SparkIcon, { size: 15 }), t("useDemo")) : null,
1838
+ props.onSelectAll ? h("button", { type: "button", className: "dshdb-btn", onClick: props.onSelectAll }, h(CheckIcon, { size: 15 }), t("selectAllRecent")) : null,
1839
+ h("button", { type: "button", className: "dshdb-btn primary", onClick: props.onNew }, h(PlusIcon, { size: 15 }), t("newConnection")),
1840
+ props.onOpenPanel ? h("button", { type: "button", className: "dshdb-btn ghost", onClick: props.onOpenPanel }, t("openGlobal")) : null
1841
+ )
1842
+ )
1843
+ );
1844
+ }
1845
+
1846
+ // ── conversation tab: the selected databases ────────────────────────
1847
+ function ConversationDbView(props) {
1848
+ var t = props.t || function (key) { return key; };
1849
+ var sessionId = props.sessionId;
1850
+ var entry = useSessionDb(sessionId);
1851
+ var state = entry.state;
1852
+ var all = state ? state.connections || [] : [];
1853
+ var selected = state ? state.selected || [] : [];
1854
+ var selectedKey = selected.map(function (row) { return row.id; }).join(",");
1855
+
1856
+ var [activeId, setActiveId] = React.useState(null);
1857
+ var [tables, setTables] = React.useState([]);
1858
+ var [tablesLoading, setTablesLoading] = React.useState(false);
1859
+ var [tableQuery, setTableQuery] = React.useState("");
1860
+ var [selectedTable, setSelectedTable] = React.useState(null);
1861
+ var [schema, setSchema] = React.useState(null);
1862
+ var [showSchema, setShowSchema] = React.useState(false);
1863
+ var [page, setPage] = React.useState(0);
1864
+ var [pageSize, setPageSize] = React.useState(50);
1865
+ var [result, setResult] = React.useState(null);
1866
+ var [rowsLoading, setRowsLoading] = React.useState(false);
1867
+ var [rowsError, setRowsError] = React.useState(null);
1868
+ var [tab, setTab] = React.useState("data");
1869
+ var [sql, setSql] = React.useState("");
1870
+ var [sqlResult, setSqlResult] = React.useState(null);
1871
+ var [sqlError, setSqlError] = React.useState(null);
1872
+ var [sqlLoading, setSqlLoading] = React.useState(false);
1873
+ var [dialog, setDialog] = React.useState(null);
1874
+ var [busy, setBusy] = React.useState(false);
1875
+ var [error, setError] = React.useState(null);
1876
+
1877
+ React.useEffect(function () {
1878
+ if (!selected.length) { setActiveId(null); return; }
1879
+ setActiveId(function (current) {
1880
+ if (current && selected.some(function (row) { return row.id === current; })) return current;
1881
+ return selected[0].id;
1882
+ });
1883
+ }, [selectedKey]);
1884
+
1885
+ React.useEffect(function () {
1886
+ if (!activeId) {
1887
+ setTables([]);
1888
+ setSelectedTable(null);
1889
+ setSchema(null);
1890
+ setResult(null);
1891
+ return;
1892
+ }
1893
+ var alive = true;
1894
+ setTablesLoading(true);
1895
+ setTables([]);
1896
+ setSelectedTable(null);
1897
+ setSchema(null);
1898
+ setResult(null);
1899
+ setRowsError(null);
1900
+ api({ action: "tables", id: activeId, sessionId: sessionId }).then(function (data) {
1901
+ if (!alive) return;
1902
+ var rows = data.tables || [];
1903
+ setTables(rows);
1904
+ if (rows.length) selectTable(activeId, rows[0].name, 0, true);
1905
+ }).catch(function (failure) {
1906
+ if (alive) setRowsError(interpolate(t("loadFailed"), { error: failure.message }));
1907
+ }).finally(function () {
1908
+ if (alive) setTablesLoading(false);
1909
+ });
1910
+ return function () { alive = false; };
1911
+ }, [activeId, sessionId]);
1912
+
1913
+ function selectTable(connectionId, name, nextPage, internal) {
1914
+ setSelectedTable(name);
1915
+ setPage(nextPage || 0);
1916
+ setRowsLoading(true);
1917
+ setRowsError(null);
1918
+ setResult(null);
1919
+ setShowSchema(false);
1920
+ if (!internal) setTab("data");
1921
+ var target = nextPage || 0;
1922
+ Promise.all([
1923
+ api({ action: "describe", id: connectionId, table: name, sessionId: sessionId }),
1924
+ api({ action: "rows", id: connectionId, table: name, limit: pageSize, offset: target * pageSize, sessionId: sessionId })
1925
+ ]).then(function (values) {
1926
+ setSchema(values[0]);
1927
+ setResult(values[1]);
1928
+ }).catch(function (failure) {
1929
+ setRowsError(interpolate(t("loadFailed"), { error: failure.message }));
1930
+ }).finally(function () { setRowsLoading(false); });
1931
+ }
1932
+
1933
+ function loadRows(nextPage, size) {
1934
+ if (!activeId || !selectedTable) return;
1935
+ var limit = size || pageSize;
1936
+ setRowsLoading(true);
1937
+ setRowsError(null);
1938
+ api({ action: "rows", id: activeId, table: selectedTable, limit: limit, offset: nextPage * limit, sessionId: sessionId }).then(function (data) {
1939
+ setResult(data);
1940
+ }).catch(function (failure) {
1941
+ setRowsError(interpolate(t("loadFailed"), { error: failure.message }));
1942
+ }).finally(function () { setRowsLoading(false); });
1943
+ }
1944
+
1945
+ function changePage(nextPage) {
1946
+ if (nextPage < 0) return;
1947
+ var total = result && result.total ? result.total : 0;
1948
+ var maxPage = Math.max(0, Math.ceil(total / pageSize) - 1);
1949
+ if (nextPage > maxPage) return;
1950
+ setPage(nextPage);
1951
+ loadRows(nextPage);
1952
+ }
1953
+
1954
+ function changePageSize(value) {
1955
+ var size = Number(value) || 50;
1956
+ setPageSize(size);
1957
+ setPage(0);
1958
+ loadRows(0, size);
1959
+ }
1960
+
1961
+ function runSql() {
1962
+ var text = sql.trim();
1963
+ if (!text || !activeId) return;
1964
+ setSqlLoading(true);
1965
+ setSqlError(null);
1966
+ setSqlResult(null);
1967
+ var readOnly = isReadOnlySql(text);
1968
+ var body = readOnly
1969
+ ? { action: "query", id: activeId, sql: text, limit: Number(state && state.config && state.config.maxRows) || 200, sessionId: sessionId }
1970
+ : { action: "execute", id: activeId, sql: text, sessionId: sessionId };
1971
+ api(body).then(function (data) {
1972
+ setSqlResult({ readOnly: readOnly, data: data });
1973
+ }).catch(function (failure) {
1974
+ setSqlError(interpolate(t("queryFailed"), { error: failure.message }));
1975
+ }).finally(function () { setSqlLoading(false); });
1976
+ }
1977
+
1978
+ async function openDemo() {
1979
+ setBusy(true);
1980
+ setError(null);
1981
+ try {
1982
+ var data = await api({ action: "seed-demo" });
1983
+ await loadSessionState(sessionId, true);
1984
+ var current = sessionEntry(sessionId).state;
1985
+ var ids = current ? current.selectedIds || [] : [];
1986
+ var demo = (data.connections || []).find(function (row) { return row.demo; });
1987
+ if (demo && !ids.includes(demo.id)) ids = ids.concat([demo.id]);
1988
+ if (demo) {
1989
+ await setSessionSelection(sessionId, ids);
1990
+ setActiveId(demo.id);
1991
+ } else {
1992
+ await setSessionSelection(sessionId, ids);
1993
+ }
1994
+ } catch (failure) {
1995
+ setError(interpolate(t("loadFailed"), { error: failure.message }));
1996
+ } finally {
1997
+ setBusy(false);
1998
+ }
1999
+ }
2000
+
2001
+ function selectAllRecent() {
2002
+ setSessionSelection(sessionId, all.map(function (row) { return row.id; })).catch(function (failure) {
2003
+ setError(interpolate(t("saveFailed"), { error: failure.message }));
2004
+ });
2005
+ }
2006
+
2007
+ async function onConnectionSaved(saved) {
2008
+ setDialog(null);
2009
+ try {
2010
+ await loadSessionState(sessionId, true);
2011
+ var current = sessionEntry(sessionId).state;
2012
+ var ids = current ? current.selectedIds || [] : [];
2013
+ if (saved && saved.id && !ids.includes(saved.id)) ids = ids.concat([saved.id]);
2014
+ await setSessionSelection(sessionId, ids);
2015
+ if (saved && saved.id) setActiveId(saved.id);
2016
+ } catch (failure) {
2017
+ setError(interpolate(t("saveFailed"), { error: failure.message }));
2018
+ }
2019
+ }
2020
+
2021
+ if (!state) {
2022
+ return h("div", { className: "dshdb-cv" },
2023
+ h("div", { className: "dshdb-cvloading" }, entry.error ? h("div", { className: "dshdb-error" }, entry.error) : h(Spinner)));
2024
+ }
2025
+
2026
+ var dialogNode = dialog ? h(ConnectionDialog, {
2027
+ t: t,
2028
+ connection: dialog.connection || null,
2029
+ onClose: function () { setDialog(null); },
2030
+ onSaved: onConnectionSaved
2031
+ }) : null;
2032
+
2033
+ if (all.length === 0) {
2034
+ return h("div", { className: "dshdb-cv" },
2035
+ error ? h("div", { className: "dshdb-cvmessage" }, h("div", { className: "dshdb-error" }, error)) : null,
2036
+ h(ConversationDbEmpty, {
2037
+ t: t,
2038
+ title: t("conversationEmptyTitle"),
2039
+ description: t("conversationEmptyDesc"),
2040
+ busy: busy,
2041
+ onDemo: openDemo,
2042
+ onNew: function () { setDialog({ mode: "new" }); },
2043
+ onOpenPanel: openGlobalPanel
2044
+ }),
2045
+ dialogNode
2046
+ );
2047
+ }
2048
+
2049
+ if (selected.length === 0) {
2050
+ return h("div", { className: "dshdb-cv" },
2051
+ error ? h("div", { className: "dshdb-cvmessage" }, h("div", { className: "dshdb-error" }, error)) : null,
2052
+ h(ConversationDbEmpty, {
2053
+ t: t,
2054
+ title: t("noSelectionTitle"),
2055
+ description: t("noSelectionDesc"),
2056
+ busy: busy,
2057
+ onSelectAll: selectAllRecent,
2058
+ onNew: function () { setDialog({ mode: "new" }); },
2059
+ onOpenPanel: openGlobalPanel
2060
+ }),
2061
+ dialogNode
2062
+ );
2063
+ }
2064
+
2065
+ var active = selected.find(function (row) { return row.id === activeId; }) || selected[0];
2066
+ var filteredTables = tableQuery.trim()
2067
+ ? tables.filter(function (row) { return row.name.toLowerCase().indexOf(tableQuery.trim().toLowerCase()) !== -1; })
2068
+ : tables;
2069
+ var total = result && result.total ? result.total : 0;
2070
+ var maxPage = Math.max(0, Math.ceil(total / pageSize) - 1);
2071
+
2072
+ var tableList = h("div", { className: "dshdb-tablelist" },
2073
+ h("div", { className: "dshdb-searchwrap" },
2074
+ h(SearchIcon, { size: 14 }),
2075
+ h("input", {
2076
+ className: "dshdb-search",
2077
+ value: tableQuery,
2078
+ placeholder: t("searchTable"),
2079
+ onChange: function (event) { setTableQuery(event.target.value); }
2080
+ })
2081
+ ),
2082
+ h("div", { className: "dshdb-tablelistscroll" },
2083
+ tablesLoading ? h("div", { className: "dshdb-placeholder" }, h(Spinner))
2084
+ : filteredTables.length === 0 ? h("div", { className: "dshdb-placeholder" }, t("noTables"))
2085
+ : filteredTables.map(function (row) {
2086
+ var tableName = row.schema ? row.schema + "." + row.name : row.name;
2087
+ return h("button", {
2088
+ key: tableName,
2089
+ type: "button",
2090
+ className: "dshdb-tablerow" + (selectedTable === tableName ? " active" : ""),
2091
+ onClick: function () { selectTable(active.id, tableName, 0); },
2092
+ title: tableName
2093
+ }, h(TableIcon, { size: 14 }), h("span", null, row.name));
2094
+ })
2095
+ )
2096
+ );
2097
+
2098
+ var dataPane = h("div", { className: "dshdb-cvdata" },
2099
+ tableList,
2100
+ h("div", { className: "dshdb-grid" },
2101
+ h("div", { className: "dshdb-gridbar" },
2102
+ h("div", { className: "dshdb-gridtitle" },
2103
+ h(TableIcon, { size: 15 }),
2104
+ h("span", null, selectedTable || t("selectTable")),
2105
+ selectedTable ? h("span", { className: "dshdb-gridmeta" }, interpolate(t("totalRows"), { n: fmtNumber(total) })) : null
2106
+ ),
2107
+ h("div", { className: "dshdb-gridtools" },
2108
+ h("select", {
2109
+ className: "dshdb-tableselect",
2110
+ value: selectedTable || "",
2111
+ "aria-label": t("tables"),
2112
+ onChange: function (event) { if (event.target.value) selectTable(active.id, event.target.value, 0); }
2113
+ }, tables.map(function (row) {
2114
+ var tableName = row.schema ? row.schema + "." + row.name : row.name;
2115
+ return h("option", { key: tableName, value: tableName }, tableName);
2116
+ })),
2117
+ h("button", { type: "button", className: "dshdb-btn small", disabled: !selectedTable, onClick: function () { setShowSchema(function (value) { return !value; }); } },
2118
+ h(ChevronDownIcon, { size: 13, style: { transform: showSchema ? "rotate(180deg)" : "none" } }), t("schema")),
2119
+ h(IconButton, { title: t("refresh"), disabled: !selectedTable, onClick: function () { if (selectedTable) selectTable(active.id, selectedTable, page, true); } }, h(RefreshIcon, { size: 14 }))
2120
+ )
2121
+ ),
2122
+ showSchema && schema ? h(SchemaChips, { t: t, schema: schema }) : null,
2123
+ rowsError ? h("div", { className: "dshdb-cvmessage" }, h("div", { className: "dshdb-error" }, rowsError)) : null,
2124
+ rowsLoading ? h("div", { className: "dshdb-placeholder" }, h(Spinner)) : null,
2125
+ !rowsLoading && !rowsError && !selectedTable ? h("div", { className: "dshdb-placeholder" }, h("div", null, h("div", null, t("selectTable")), h("div", { className: "dshdb-gridmeta" }, t("selectTableHint")))) : null,
2126
+ !rowsLoading && !rowsError && selectedTable ? h(DataGrid, { t: t, result: result }) : null,
2127
+ selectedTable ? h("div", { className: "dshdb-pager" },
2128
+ h("span", null, interpolate(t("page"), { page: page + 1 }) + " / " + (maxPage + 1)),
2129
+ h("select", { value: pageSize, onChange: function (event) { changePageSize(event.target.value); }, "aria-label": t("pageSize") },
2130
+ [25, 50, 100, 200].map(function (size) { return h("option", { key: size, value: size }, size + " / " + t("pageSize")); })
2131
+ ),
2132
+ h("button", { type: "button", className: "dshdb-btn small", onClick: function () { changePage(page - 1); }, disabled: page <= 0 }, h(ChevronLeftIcon, { size: 13 })),
2133
+ h("button", { type: "button", className: "dshdb-btn small", onClick: function () { changePage(page + 1); }, disabled: page >= maxPage }, h(ChevronRightIcon, { size: 13 }))
2134
+ ) : null
2135
+ )
2136
+ );
2137
+
2138
+ var sqlPane = h("div", { className: "dshdb-sqlpane" },
2139
+ h("div", { className: "dshdb-editorwrap" },
2140
+ h("div", { className: "dshdb-editortop" },
2141
+ h("span", null, sessionKindLabel(active.kind) + " · " + (active.database || active.file || "")),
2142
+ h("span", null, "⌘ / Ctrl + Enter")
2143
+ ),
2144
+ h("textarea", {
2145
+ className: "dshdb-editor",
2146
+ value: sql,
2147
+ spellCheck: false,
2148
+ placeholder: t("sqlPlaceholder"),
2149
+ onChange: function (event) { setSql(event.target.value); },
2150
+ onKeyDown: function (event) {
2151
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") { event.preventDefault(); runSql(); }
2152
+ }
2153
+ })
2154
+ ),
2155
+ h("div", { className: "dshdb-sqlbar" },
2156
+ h("div", { className: "dshdb-hint" }, t("sqlHint")),
2157
+ h("div", { style: { display: "flex", gap: "7px", alignItems: "center" } },
2158
+ sql ? h("button", { type: "button", className: "dshdb-btn small ghost", onClick: function () { setSql(""); setSqlResult(null); setSqlError(null); } }, t("clear")) : null,
2159
+ h("button", { type: "button", className: "dshdb-btn primary", onClick: runSql, disabled: sqlLoading || !sql.trim() },
2160
+ sqlLoading ? h(Spinner) : h(PlayIcon, { size: 13 }),
2161
+ sqlLoading ? t("running") : t("run")
2162
+ )
2163
+ )
2164
+ ),
2165
+ sqlError ? h("div", { className: "dshdb-error" }, sqlError) : null,
2166
+ sqlResult ? h("div", { className: "dshdb-result" },
2167
+ h("div", { className: "dshdb-resulthead" },
2168
+ h("span", null, t("result")),
2169
+ h("span", { className: "dshdb-gridmeta" },
2170
+ typeof sqlResult.data.elapsedMs === "number" ? sqlResult.data.elapsedMs + " ms" : "",
2171
+ sqlResult.data.truncated ? " · " + t("truncated") : ""
2172
+ )
2173
+ ),
2174
+ h("div", { className: "dshdb-resultbody" },
2175
+ sqlResult.readOnly
2176
+ ? h(DataGrid, { t: t, result: { columns: sqlResult.data.columns, rows: sqlResult.data.rows, offset: 0 } })
2177
+ : h("div", { style: { padding: "10px 4px" } },
2178
+ h("div", { className: "dshdb-success" }, h(CheckIcon, { size: 15 }), interpolate(t("affected"), { n: sqlResult.data.changes ?? 0 })))
2179
+ )
2180
+ ) : h("div", { className: "dshdb-result" }, h("div", { className: "dshdb-placeholder" }, t("result")))
2181
+ );
2182
+
2183
+ return h("div", { className: "dshdb-cv" },
2184
+ h("div", { className: "dshdb-cvhead" },
2185
+ h("div", { className: "dshdb-cvtitlewrap" },
2186
+ h("div", { className: "dshdb-titlemark" }, h(DatabaseIcon, { size: 20 })),
2187
+ h("div", null,
2188
+ h("h2", { className: "dshdb-cvtitle" }, t("title")),
2189
+ h("p", { className: "dshdb-cvsub" }, interpolate(t("connectionCount"), { n: selected.length }))
2190
+ )
2191
+ ),
2192
+ h("div", { className: "dshdb-cvactions" },
2193
+ h("button", { type: "button", className: "dshdb-btn small", onClick: function () { selectTable(active.id, selectedTable, page, true); } }, h(RefreshIcon, { size: 13 }), t("refresh")),
2194
+ h("button", { type: "button", className: "dshdb-btn small", onClick: openGlobalPanel }, t("openGlobal")),
2195
+ h("button", { type: "button", className: "dshdb-btn small primary", onClick: function () { setDialog({ mode: "new" }); } }, h(PlusIcon, { size: 13 }), t("newConnection"))
2196
+ )
2197
+ ),
2198
+ h("div", { className: "dshdb-cvpills" },
2199
+ selected.map(function (connection) {
2200
+ return h("button", {
2201
+ key: connection.id,
2202
+ type: "button",
2203
+ className: "dshdb-cvpill" + (connection.id === active.id ? " active" : ""),
2204
+ onClick: function () { setActiveId(connection.id); }
2205
+ },
2206
+ h("span", { className: "dshdb-dot" }),
2207
+ connection.name,
2208
+ connection.demo ? h("span", { className: "dshdb-badge demo" }, t("demo")) : null
2209
+ );
2210
+ }),
2211
+ h("span", { className: "dshdb-cvpillhint" }, t("askInChat"))
2212
+ ),
2213
+ h("div", { className: "dshdb-tabs", role: "tablist", "aria-label": t("tabsAria") },
2214
+ h("button", { type: "button", role: "tab", "aria-selected": tab === "data", className: "dshdb-tab" + (tab === "data" ? " active" : ""), onClick: function () { setTab("data"); } }, h(TableIcon, { size: 14 }), t("data")),
2215
+ h("button", { type: "button", role: "tab", "aria-selected": tab === "sql", className: "dshdb-tab" + (tab === "sql" ? " active" : ""), onClick: function () { setTab("sql"); } }, h(PlayIcon, { size: 13 }), t("sql"))
2216
+ ),
2217
+ error ? h("div", { className: "dshdb-cvmessage" }, h("div", { className: "dshdb-error" }, error)) : null,
2218
+ h("div", { className: "dshdb-cvbody" }, tab === "data" ? dataPane : sqlPane),
2219
+ dialogNode
2220
+ );
2221
+ }
2222
+
2223
+ /** Decorative database glyph for the sidebar row. */
2224
+ function DbStudioIcon(props) {
2225
+ return h(DatabaseIcon, { size: props.size || 18 });
2226
+ }
2227
+
2228
+ /** Slash-command glyph (the trigger menu asks for a 14px icon). */
2229
+ function DbCommandIcon(props) {
2230
+ return h(DatabaseIcon, { size: props.size || 14 });
2231
+ }
2232
+
2233
+ exports.name = "dsh-db-studio";
2234
+ exports.inject = ["slots", "layout", "locale", "commandUi"];
2235
+ exports.apply = function (ctx) {
2236
+ APP_CTX = ctx;
2237
+ installStyles(ctx);
2238
+ ctx.effect(function () {
2239
+ return ctx.locale.register(NS, { zh: zh, en: en });
2240
+ }, "dsh-db-studio: dictionaries");
2241
+ var t = ctx.locale.bind(NS);
2242
+
2243
+ // `/db` in the composer: same row treatment as the built-in commands,
2244
+ // with a database glyph in front and no message submitted.
2245
+ var commandUi = ctx.get("commandUi");
2246
+ if (commandUi && typeof commandUi.register === "function") {
2247
+ ctx.effect(function () {
2248
+ return commandUi.register({
2249
+ name: "db",
2250
+ available: function () { return true; },
2251
+ label: function () { return t("commandLabel"); },
2252
+ description: function () { return t("commandDesc"); },
2253
+ icon: DbCommandIcon,
2254
+ ui: {
2255
+ kind: "action",
2256
+ run: function () { openGlobalPanel(); }
2257
+ }
2258
+ });
2259
+ }, "dsh-db-studio: /db command");
2260
+ }
2261
+
2262
+ ctx.slots.inject("main", function () {
2263
+ return ctx.slots.register({
2264
+ name: "main",
2265
+ key: "dbstudio",
2266
+ locale: NS
2267
+ }, DbStudioPage);
2268
+ });
2269
+ ctx.slots.inject("sidebar.panellist", function () {
2270
+ return ctx.slots.register({
2271
+ name: "sidebar.panellist",
2272
+ id: "dbstudio",
2273
+ order: 20,
2274
+ locale: NS,
2275
+ label: function () { return t("title"); }
2276
+ }, DbStudioIcon);
2277
+ });
2278
+
2279
+ // Conversation tab, right after Chat and before Trajectory (order 10).
2280
+ ctx.slots.inject("conversation.view", function () {
2281
+ return ctx.slots.register({
2282
+ name: "conversation.view",
2283
+ id: "dbstudio",
2284
+ order: 5,
2285
+ locale: NS,
2286
+ label: function () { return t("title"); }
2287
+ }, ConversationDbView);
2288
+ });
2289
+
2290
+ // Composer control, beside the workspace/permission seats.
2291
+ ctx.slots.inject("conversation.input.left", function () {
2292
+ return ctx.slots.register({
2293
+ name: "conversation.input.left",
2294
+ id: "dbstudio",
2295
+ order: 20,
2296
+ locale: NS
2297
+ }, ConversationDbPicker);
2298
+ });
2299
+
2300
+ // Connection status under the composer.
2301
+ ctx.slots.inject("conversation.composer.dock", function () {
2302
+ return ctx.slots.register({
2303
+ name: "conversation.composer.dock",
2304
+ id: "dbstudio",
2305
+ order: 20,
2306
+ locale: NS
2307
+ }, ConversationDbStatus);
2308
+ });
2309
+ };
2310
+
2311
+ return module.exports;
2312
+ }
2313
+ });