dsh-db-tool 1.5.5 → 1.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/client.js +146 -435
- package/dist/adapters/sql-shared/pg-like.js +123 -6
- package/dist/store/connections.js +51 -4
- package/package.json +2 -4
package/client/client.js
CHANGED
|
@@ -92,11 +92,8 @@ window.__ModuleLoader__.load({
|
|
|
92
92
|
emptyStructure: "暂无字段信息",
|
|
93
93
|
emptyData: "暂无数据",
|
|
94
94
|
retry: "重试",
|
|
95
|
-
browsePanelHint: "数据浏览已在浮动窗口打开,可继续操作其他功能。",
|
|
96
|
-
reopenBrowse: "打开数据浏览",
|
|
97
95
|
selectTableHint: "在左侧选择表后查看数据",
|
|
98
|
-
//
|
|
99
|
-
editCell: "编辑",
|
|
96
|
+
// 单元格底部详情栏(Navicat 式就地写回)
|
|
100
97
|
cellValue: "完整值",
|
|
101
98
|
copyBtn: "复制",
|
|
102
99
|
copied: "已复制",
|
|
@@ -201,11 +198,8 @@ window.__ModuleLoader__.load({
|
|
|
201
198
|
emptyStructure: "No columns",
|
|
202
199
|
emptyData: "No rows",
|
|
203
200
|
retry: "Retry",
|
|
204
|
-
browsePanelHint: "Browse is open in a floating window; you can keep working here.",
|
|
205
|
-
reopenBrowse: "Open Data Browser",
|
|
206
201
|
selectTableHint: "Select a table on the left to view data",
|
|
207
|
-
// Cell
|
|
208
|
-
editCell: "Edit",
|
|
202
|
+
// Cell bottom detail bar (Navicat-style in-place write-back)
|
|
209
203
|
cellValue: "Full value",
|
|
210
204
|
copyBtn: "Copy",
|
|
211
205
|
copied: "Copied",
|
|
@@ -322,22 +316,25 @@ window.__ModuleLoader__.load({
|
|
|
322
316
|
if (styleEl) return;
|
|
323
317
|
styleEl = document.createElement("style");
|
|
324
318
|
styleEl.textContent = [
|
|
325
|
-
"/* ===== Apple-style Design Tokens(dark 基线,light 经 media query 反转;.dbt-browse-root
|
|
319
|
+
"/* ===== Apple-style Design Tokens(dark 基线,light 经 media query 反转;.dbt-browse-root 并入以承载内嵌浏览面板与单元格浮层) ===== */",
|
|
326
320
|
".dbt-panel,.dbt-browse-root{--dbt-accent:var(--dsh-accent,#0a84ff);--dbt-bg:transparent;--dbt-surface:rgba(120,120,128,.12);--dbt-surface-strong:rgba(120,120,128,.18);--dbt-separator:rgba(120,120,128,.24);--dbt-text:rgba(235,235,245,.92);--dbt-text-secondary:rgba(235,235,245,.6);--dbt-danger:#ff453a;--dbt-success:#30d158;--dbt-warning:#ffd60a;--dbt-dialog-bg:rgba(40,40,44,.85);--dbt-th-bg:rgba(30,30,32,.72);--dbt-seg-active:rgba(255,255,255,.14);--dbt-shadow:0 8px 32px rgba(0,0,0,.28);--dbt-radius-card:12px;--dbt-radius-ctrl:8px;--dbt-radius-pill:6px;--dbt-ease:cubic-bezier(.25,.1,.25,1);--dbt-mono:ui-monospace,'SF Mono',Menlo,Consolas,monospace;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Segoe UI',system-ui,sans-serif;}",
|
|
327
321
|
"@media (prefers-color-scheme: light){.dbt-panel,.dbt-browse-root{--dbt-accent:var(--dsh-accent,#007aff);--dbt-surface:rgba(120,120,128,.08);--dbt-surface-strong:rgba(120,120,128,.14);--dbt-separator:rgba(60,60,67,.18);--dbt-text:rgba(30,30,32,.92);--dbt-text-secondary:rgba(60,60,67,.6);--dbt-danger:#ff3b30;--dbt-success:#34c759;--dbt-warning:#ff9f0a;--dbt-dialog-bg:rgba(252,252,252,.9);--dbt-th-bg:rgba(255,255,255,.72);--dbt-seg-active:rgba(255,255,255,.9);--dbt-shadow:0 8px 32px rgba(0,0,0,.12);}}",
|
|
328
|
-
//
|
|
322
|
+
// 浏览面板根自持文字色:PiP 独立文档挂 body 后无侧栏祖先 color 可继承(内嵌/浮窗两态共用)
|
|
329
323
|
".dbt-browse-root{color:var(--dbt-text);}",
|
|
330
324
|
"/* ===== 根容器 ===== */",
|
|
331
|
-
".dbt-panel{font-size:13px;line-height:1.45;display:flex;flex-direction:column;gap:
|
|
325
|
+
".dbt-panel{font-size:13px;line-height:1.45;display:flex;flex-direction:column;gap:10px;padding:16px;height:100%;box-sizing:border-box;overflow-y:auto;}",
|
|
332
326
|
"/* ===== iOS segmented control(顶部 tab) ===== */",
|
|
333
327
|
".dbt-tabs{display:flex;gap:2px;background:var(--dbt-surface);border-radius:var(--dbt-radius-ctrl);padding:2px;}",
|
|
334
328
|
".dbt-tabs button{flex:1;border:none;background:transparent;color:inherit;border-radius:var(--dbt-radius-pill);padding:5px 8px;font-size:12px;font-weight:500;cursor:pointer;transition:all .18s var(--dbt-ease);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}",
|
|
335
329
|
".dbt-tabs button.active{background:var(--dbt-seg-active);font-weight:600;box-shadow:0 1px 3px rgba(0,0,0,.12);}",
|
|
336
330
|
"/* ===== inset grouped 卡片 ===== */",
|
|
337
|
-
".dbt-card{background:var(--dbt-surface);border:none;border-radius:var(--dbt-radius-card);padding:12px;display:flex;flex-direction:column;gap:
|
|
331
|
+
".dbt-card{background:var(--dbt-surface);border:none;border-radius:var(--dbt-radius-card);padding:12px;display:flex;flex-direction:column;gap:8px;}",
|
|
338
332
|
".dbt-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}",
|
|
333
|
+
"/* ===== 共享列布局(T1 逐步替换各视图内联 flex-column) ===== */",
|
|
334
|
+
".dbt-col{display:flex;flex-direction:column;gap:8px;}",
|
|
335
|
+
".dbt-col.dbt-col-tight{gap:2px;}",
|
|
339
336
|
"/* ===== 填充式无边框输入 ===== */",
|
|
340
|
-
".dbt-row input,.dbt-row select,.dbt-card input,.dbt-card select,.dbt-card textarea,.dbt-group input,.dbt-group select{flex:1;min-width:60px;background:var(--dbt-surface-strong);border:none;color:inherit;border-radius:var(--dbt-radius-ctrl);padding:
|
|
337
|
+
".dbt-row input,.dbt-row select,.dbt-card input,.dbt-card select,.dbt-card textarea,.dbt-group input,.dbt-group select{flex:1;min-width:60px;background:var(--dbt-surface-strong);border:none;color:inherit;border-radius:var(--dbt-radius-ctrl);padding:5px 12px;font-size:13px;font-family:inherit;transition:all .18s var(--dbt-ease);}",
|
|
341
338
|
".dbt-row input:focus,.dbt-row select:focus,.dbt-card input:focus,.dbt-card select:focus,.dbt-card textarea:focus,.dbt-group input:focus,.dbt-group select:focus{outline:2px solid var(--dbt-accent);outline-offset:-1px;}",
|
|
342
339
|
".dbt-card textarea{font-family:var(--dbt-mono);min-height:96px;resize:vertical;}",
|
|
343
340
|
"/* ===== 按钮 ===== */",
|
|
@@ -350,24 +347,25 @@ window.__ModuleLoader__.load({
|
|
|
350
347
|
".dbt-btn.danger:hover{background:rgba(255,69,58,.12);}",
|
|
351
348
|
".dbt-btn:disabled{opacity:.4;cursor:default;transform:none;}",
|
|
352
349
|
"/* ===== 文本反馈 ===== */",
|
|
350
|
+
".dbt-panel strong,.dbt-browse-root strong{font-size:13px;font-weight:600;}",
|
|
353
351
|
".dbt-muted,.dbt-readhint{color:var(--dbt-text-secondary);font-size:11px;}",
|
|
354
352
|
".dbt-err{color:var(--dbt-danger);font-size:12px;white-space:pre-wrap;}",
|
|
355
353
|
".dbt-msg{color:var(--dbt-success);font-size:12px;white-space:pre-wrap;}",
|
|
356
354
|
"/* ===== 行 hairline 表格 + 毛玻璃 sticky 表头 ===== */",
|
|
357
355
|
".dbt-table{width:100%;border-collapse:collapse;font-size:12px;}",
|
|
358
|
-
".dbt-table th,.dbt-table td{border:none;border-bottom:1px solid var(--dbt-separator);padding:
|
|
359
|
-
".dbt-table th{position:sticky;top:0;z-index:1;background:var(--dbt-th-bg);-webkit-backdrop-filter:blur(20px) saturate(180%);backdrop-filter:blur(20px) saturate(180%);font-weight:600;font-size:
|
|
356
|
+
".dbt-table th,.dbt-table td{border:none;border-bottom:1px solid var(--dbt-separator);padding:7px 10px;text-align:left;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
357
|
+
".dbt-table th{position:sticky;top:0;z-index:1;background:var(--dbt-th-bg);-webkit-backdrop-filter:blur(20px) saturate(180%);backdrop-filter:blur(20px) saturate(180%);font-weight:600;font-size:12px;}",
|
|
360
358
|
".dbt-table td{font-family:var(--dbt-mono);}",
|
|
361
359
|
".dbt-tablewrap{border:none;border-radius:var(--dbt-radius-card);overflow:auto;max-height:320px;background:var(--dbt-surface);}",
|
|
362
|
-
"/* =====
|
|
360
|
+
"/* ===== 浏览面板表格:列分明(列间 hairline + 表头加深一档 + 行 hover) ===== */",
|
|
363
361
|
".dbt-browse-root .dbt-table th + th,.dbt-browse-root .dbt-table td + td{border-left:1px solid var(--dbt-separator);}",
|
|
364
|
-
".dbt-browse-root .dbt-table th{background:rgba(30,30,32,.85);
|
|
365
|
-
".dbt-browse-root .dbt-table td{font-size:
|
|
362
|
+
".dbt-browse-root .dbt-table th{background:rgba(30,30,32,.85);}",
|
|
363
|
+
".dbt-browse-root .dbt-table td{font-size:12px;}",
|
|
366
364
|
".dbt-browse-root .dbt-table tbody tr:hover td{background:var(--dbt-surface-strong);}",
|
|
367
365
|
"@media (prefers-color-scheme: light){.dbt-browse-root .dbt-table th{background:rgba(242,242,247,.9);}}",
|
|
368
366
|
"/* ===== 对话框(macOS alert 材质) ===== */",
|
|
369
367
|
".dbt-overlay{position:fixed;inset:0;background:rgba(0,0,0,.4);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;z-index:9999;}",
|
|
370
|
-
".dbt-dialog{background:var(--dbt-dialog-bg);color:inherit;border:1px solid var(--dbt-separator);border-radius:
|
|
368
|
+
".dbt-dialog{background:var(--dbt-dialog-bg);color:inherit;border:1px solid var(--dbt-separator);border-radius:var(--dbt-radius-card);padding:16px;max-width:460px;width:90%;box-sizing:border-box;display:flex;flex-direction:column;gap:10px;font-size:13px;-webkit-backdrop-filter:blur(20px) saturate(180%);backdrop-filter:blur(20px) saturate(180%);box-shadow:var(--dbt-shadow);}",
|
|
371
369
|
".dbt-dialog pre{background:var(--dbt-surface-strong);border-radius:var(--dbt-radius-pill);padding:8px;font-family:var(--dbt-mono);font-size:12px;white-space:pre-wrap;word-break:break-all;max-height:160px;overflow:auto;}",
|
|
372
370
|
".dbt-dialog .dbt-row{justify-content:flex-end;}",
|
|
373
371
|
"/* ===== 授权/连接列表:inset grouped(一张卡多行 hairline) ===== */",
|
|
@@ -377,7 +375,7 @@ window.__ModuleLoader__.load({
|
|
|
377
375
|
".dbt-listrow:hover{background:var(--dbt-surface-strong);}",
|
|
378
376
|
".dbt-tree{display:flex;flex-direction:column;}",
|
|
379
377
|
"/* ===== Navicat 式对象树(数据浏览) ===== */",
|
|
380
|
-
".dbt-treerow{display:flex;gap:6px;align-items:center;padding:
|
|
378
|
+
".dbt-treerow{display:flex;gap:6px;align-items:center;padding:10px 12px;font-size:13px;cursor:pointer;user-select:none;border-bottom:1px solid var(--dbt-separator);transition:background .15s var(--dbt-ease);}",
|
|
381
379
|
".dbt-treerow:last-child{border-bottom:none;}",
|
|
382
380
|
".dbt-treerow:hover{background:var(--dbt-surface-strong);}",
|
|
383
381
|
".dbt-treerow.active{color:var(--dbt-accent);font-weight:600;}",
|
|
@@ -385,34 +383,27 @@ window.__ModuleLoader__.load({
|
|
|
385
383
|
".dbt-chev.open{transform:rotate(90deg);}",
|
|
386
384
|
".dbt-chev.leaf{visibility:hidden;}",
|
|
387
385
|
".dbt-treename{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
388
|
-
/* ===== Navicat
|
|
389
|
-
".dbt-browse-
|
|
390
|
-
".dbt-browse-
|
|
391
|
-
".dbt-browse-
|
|
392
|
-
".dbt-browse-
|
|
393
|
-
".dbt-browse-
|
|
394
|
-
".dbt-browse-
|
|
395
|
-
".dbt-browse-
|
|
396
|
-
".dbt-browse-
|
|
397
|
-
".dbt-browse-
|
|
398
|
-
".dbt-browse-main{flex:1;min-width:0;display:flex;flex-direction:column;}",
|
|
399
|
-
".dbt-browse-tabs{flex:none;padding:10px 14px 0;}",
|
|
400
|
-
".dbt-browse-content{flex:1;min-height:0;position:relative;display:flex;flex-direction:column;gap:10px;padding:12px 14px;overflow:hidden;}",
|
|
401
|
-
".dbt-browse-tablewrap{flex:1;min-height:0;overflow:auto;border:none;border-radius:var(--dbt-radius-card);background:var(--dbt-surface);}",
|
|
402
|
-
".dbt-browse-footer{flex:none;display:flex;align-items:center;gap:8px;padding:10px 14px;border-top:1px solid var(--dbt-separator);}",
|
|
403
|
-
".dbt-browse-empty{flex:1;display:flex;align-items:center;justify-content:center;color:var(--dbt-text-secondary);font-size:13px;}",
|
|
404
|
-
".dbt-browse-errorbox{flex:1;display:flex;flex-direction:column;gap:10px;align-items:flex-start;justify-content:center;}",
|
|
386
|
+
/* ===== Navicat 式浏览(内嵌面板,纵向流式;浮窗壳类已随 DOM 回收) ===== */
|
|
387
|
+
".dbt-browse-body{display:flex;flex-direction:column;gap:10px;min-height:0;}",
|
|
388
|
+
".dbt-browse-tree{flex:none;max-height:280px;overflow-y:auto;background:var(--dbt-surface);border-radius:var(--dbt-radius-card);}",
|
|
389
|
+
".dbt-browse-main{flex:1;min-height:0;display:flex;flex-direction:column;gap:8px;}",
|
|
390
|
+
".dbt-browse-tabs{flex:none;}",
|
|
391
|
+
".dbt-browse-content{flex:1;min-height:0;position:relative;display:flex;flex-direction:column;}",
|
|
392
|
+
".dbt-browse-tablewrap{flex:none;max-height:320px;overflow:auto;border:none;border-radius:var(--dbt-radius-card);background:var(--dbt-surface);}",
|
|
393
|
+
".dbt-browse-footer{flex:none;display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--dbt-surface);border-radius:var(--dbt-radius-ctrl);}",
|
|
394
|
+
".dbt-browse-empty{display:flex;align-items:center;justify-content:center;color:var(--dbt-text-secondary);font-size:13px;padding:24px 12px;}",
|
|
395
|
+
".dbt-browse-errorbox{display:flex;flex-direction:column;gap:8px;align-items:flex-start;}",
|
|
405
396
|
".dbt-browse-skel{display:flex;flex-direction:column;gap:8px;}",
|
|
406
397
|
".dbt-browse-skelrow{display:flex;gap:8px;}",
|
|
407
398
|
".dbt-browse-skelcell{flex:1;height:24px;background:var(--dbt-surface-strong);border-radius:var(--dbt-radius-pill);}",
|
|
408
|
-
"/* =====
|
|
399
|
+
"/* ===== 浏览面板:单元格按钮与详情底栏(Apple 分区卡) ===== */",
|
|
409
400
|
".dbt-cellbtn{border:none;background:transparent;color:inherit;font:inherit;padding:0;margin:0;cursor:pointer;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;width:100%;text-align:left;}",
|
|
410
401
|
".dbt-cellbtn:focus-visible{outline:2px solid var(--dbt-accent);outline-offset:-1px;border-radius:2px;}",
|
|
411
|
-
".dbt-
|
|
412
|
-
".dbt-
|
|
413
|
-
".dbt-
|
|
402
|
+
".dbt-celldetail{margin-top:10px;padding:12px;background:var(--dbt-surface);border-radius:var(--dbt-radius-card);display:flex;flex-direction:column;gap:8px;}",
|
|
403
|
+
".dbt-celldetail-input{font-family:var(--dbt-mono);font-size:12px;min-height:72px;resize:vertical;background:var(--dbt-surface-strong);border:none;color:inherit;border-radius:var(--dbt-radius-ctrl);padding:8px;width:100%;box-sizing:border-box;}",
|
|
404
|
+
".dbt-celldetail-input:focus{outline:2px solid var(--dbt-accent);outline-offset:-1px;}",
|
|
414
405
|
".dbt-celledit{display:flex;flex-direction:column;gap:8px;}",
|
|
415
|
-
".dbt-celledit textarea{font-family:var(--dbt-mono);font-size:13px;min-height:96px;resize:vertical;background:var(--dbt-surface-strong);border:none;color:inherit;border-radius:
|
|
406
|
+
".dbt-celledit textarea{font-family:var(--dbt-mono);font-size:13px;min-height:96px;resize:vertical;background:var(--dbt-surface-strong);border:none;color:inherit;border-radius:var(--dbt-radius-ctrl);padding:5px 12px;}",
|
|
416
407
|
".dbt-celledit textarea:focus{outline:2px solid var(--dbt-accent);outline-offset:-1px;}",
|
|
417
408
|
".dbt-nullchip{display:inline-flex;align-items:center;border:1px solid var(--dbt-warning);color:var(--dbt-warning);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:600;}",
|
|
418
409
|
"/* ===== 视图切换入场(仅 transform/opacity;reduced-motion 全局已禁) ===== */",
|
|
@@ -420,12 +411,11 @@ window.__ModuleLoader__.load({
|
|
|
420
411
|
".dbt-view{display:flex;flex-direction:column;gap:10px;animation:dbt-in .22s var(--dbt-ease);}",
|
|
421
412
|
"/* ===== mini segmented(ro/rw 等切换) ===== */",
|
|
422
413
|
".dbt-seg{display:flex;gap:2px;background:var(--dbt-surface);border-radius:var(--dbt-radius-ctrl);padding:2px;}",
|
|
423
|
-
".dbt-seg button{flex:none;border:none;background:transparent;color:inherit;font-size:
|
|
414
|
+
".dbt-seg button{flex:none;border:none;background:transparent;color:inherit;font-size:12px;padding:5px 12px;border-radius:var(--dbt-radius-pill);cursor:pointer;transition:all .18s var(--dbt-ease);}",
|
|
424
415
|
".dbt-seg button.active{background:var(--dbt-seg-active);font-weight:600;box-shadow:0 1px 3px rgba(0,0,0,.12);}",
|
|
425
|
-
".dbt-browse-root .dbt-seg button{font-size:12px;padding:4px 12px;}",
|
|
426
416
|
"/* ===== 降级:透明度减弱 / 动效减弱 ===== */",
|
|
427
|
-
"@media (prefers-reduced-transparency: reduce){.dbt-overlay{backdrop-filter:none;-webkit-backdrop-filter:none;background:rgba(0,0,0,.55);}.dbt-dialog
|
|
428
|
-
"@media (prefers-color-scheme: light) and (prefers-reduced-transparency: reduce){.dbt-dialog
|
|
417
|
+
"@media (prefers-reduced-transparency: reduce){.dbt-overlay{backdrop-filter:none;-webkit-backdrop-filter:none;background:rgba(0,0,0,.55);}.dbt-dialog{backdrop-filter:none;-webkit-backdrop-filter:none;background:#2c2c2e;}.dbt-celldetail{background:#2c2c2e;}.dbt-table th{backdrop-filter:none;-webkit-backdrop-filter:none;background:#1e1e20;}.dbt-browse-root .dbt-table th{background:#1e1e20;}}",
|
|
418
|
+
"@media (prefers-color-scheme: light) and (prefers-reduced-transparency: reduce){.dbt-dialog{background:#f5f5f7;}.dbt-celldetail{background:#f2f2f7;}.dbt-table th,.dbt-browse-root .dbt-table th{background:#f2f2f7;}}",
|
|
429
419
|
"@media (prefers-reduced-motion: reduce){.dbt-panel *,.dbt-browse-root *{transition:none!important;animation:none!important;}.dbt-btn:active{transform:none;}}",
|
|
430
420
|
].join("\n");
|
|
431
421
|
document.head.appendChild(styleEl);
|
|
@@ -460,79 +450,6 @@ window.__ModuleLoader__.load({
|
|
|
460
450
|
);
|
|
461
451
|
}
|
|
462
452
|
|
|
463
|
-
// --- 弹窗挂载:优先 createPortal 挂 document.body(避免侧栏容器裁剪);宿主未提供 react-dom 时回退 Panel 内 fixed overlay(DangerDialog 同模式) ---
|
|
464
|
-
// 三重降级解析(模块级缓存,全模块一次):①模块加载时同步 require(dsh.client.inject 声明 react-dom 后应成功)
|
|
465
|
-
// ②宿主模块系统异步 import(ctx.modules,宿主 rc.8+ 提供)③均不可用则保持 Panel 内回退渲染。
|
|
466
|
-
let portalImpl = null; // 解析成功的 createPortal(缓存,全模块一次)
|
|
467
|
-
let portalTried = false; // 是否已尝试解析(异步 import 只发起一次)
|
|
468
|
-
let portalPending = null; // 订阅解析成功的回调列表
|
|
469
|
-
try {
|
|
470
|
-
var rd = require("react-dom");
|
|
471
|
-
if (rd && typeof rd.createPortal === "function") { portalImpl = rd.createPortal.bind(rd); portalTried = true; }
|
|
472
|
-
} catch (e) { /* 留 null,等异步解析 */ }
|
|
473
|
-
function resolvePortalAsync(ctx) {
|
|
474
|
-
if (portalTried) return;
|
|
475
|
-
portalTried = true;
|
|
476
|
-
if (portalImpl) return;
|
|
477
|
-
var mods = ctx && ctx.modules;
|
|
478
|
-
var p = mods && typeof mods.import === "function" ? mods.import("react-dom") : null;
|
|
479
|
-
if (p && typeof p.then === "function") {
|
|
480
|
-
p.then(function (m) {
|
|
481
|
-
if (m && typeof m.createPortal === "function") {
|
|
482
|
-
portalImpl = m.createPortal.bind(m);
|
|
483
|
-
var waiters = portalPending || []; portalPending = null;
|
|
484
|
-
for (var i = 0; i < waiters.length; i++) waiters[i]();
|
|
485
|
-
}
|
|
486
|
-
}).catch(function () { /* 解析失败保持回退渲染 */ });
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
function onPortalReady(cb) {
|
|
490
|
-
if (portalImpl) { cb(); return; }
|
|
491
|
-
(portalPending = portalPending || []).push(cb);
|
|
492
|
-
}
|
|
493
|
-
function mountDialog(children) {
|
|
494
|
-
return (portalImpl && !portalDisabled) ? portalImpl(children, document.body) : children;
|
|
495
|
-
}
|
|
496
|
-
// PiP 独立窗口渲染时禁 portal:PiP root 本身就挂在 pipDoc.body 上,再 portal 回主 document 会把 DOM 拉出独立窗口
|
|
497
|
-
let portalDisabled = false;
|
|
498
|
-
// createRoot 三重解析(PiP 独立窗口需要独立 React root,结构对齐上方 portal 解析):
|
|
499
|
-
// ①模块加载时同步 require react-dom → react-dom/client(宿主按 specifier 解析,两者是不同键)
|
|
500
|
-
// ②宿主模块系统异步 import("react-dom/client") ③均不可用则 PiP 超时降级 DOM 浮窗
|
|
501
|
-
let reactCreateRoot = null;
|
|
502
|
-
let rootTried = false; // 异步 import 只发起一次
|
|
503
|
-
let rootPending = null; // 订阅解析成功的回调列表
|
|
504
|
-
let rootSource = null; // 就绪方式:sync / async / null(未就绪,诊断日志用)
|
|
505
|
-
try {
|
|
506
|
-
var rd2 = require("react-dom");
|
|
507
|
-
if (rd2 && typeof rd2.createRoot === "function") {
|
|
508
|
-
reactCreateRoot = rd2.createRoot.bind(rd2); rootTried = true; rootSource = "sync";
|
|
509
|
-
} else {
|
|
510
|
-
try { var rdc = require("react-dom/client"); if (rdc && typeof rdc.createRoot === "function") { reactCreateRoot = rdc.createRoot.bind(rdc); rootTried = true; rootSource = "sync"; } } catch (e2) { /* 保持 null */ }
|
|
511
|
-
}
|
|
512
|
-
} catch (e) { /* 保持 null,等异步解析 */ }
|
|
513
|
-
function resolveRootAsync(ctx) {
|
|
514
|
-
if (rootTried) return;
|
|
515
|
-
rootTried = true;
|
|
516
|
-
if (reactCreateRoot) return;
|
|
517
|
-
var mods = ctx && ctx.modules;
|
|
518
|
-
var p = mods && typeof mods.import === "function" ? mods.import("react-dom/client") : null;
|
|
519
|
-
if (p && typeof p.then === "function") {
|
|
520
|
-
p.then(function (m) {
|
|
521
|
-
if (m && typeof m.createRoot === "function") {
|
|
522
|
-
reactCreateRoot = m.createRoot.bind(m);
|
|
523
|
-
rootSource = "async";
|
|
524
|
-
var waiters = rootPending || []; rootPending = null;
|
|
525
|
-
for (var i = 0; i < waiters.length; i++) waiters[i]();
|
|
526
|
-
}
|
|
527
|
-
}).catch(function () { /* 解析失败,PiP 走超时降级 */ });
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
function onRootReady(cb) {
|
|
531
|
-
if (reactCreateRoot) { cb(); return; }
|
|
532
|
-
(rootPending = rootPending || []).push(cb);
|
|
533
|
-
}
|
|
534
|
-
let pipResult = "not-tried"; // 最近一次 PiP 尝试结果:opened / rejected:<err.name>(诊断日志用)
|
|
535
|
-
|
|
536
453
|
/* ---------------- 连接管理 ---------------- */
|
|
537
454
|
const EMPTY_FORM = { id: "", kind: "mysql", name: "", mode: "url", url: "", urlUser: "", urlPassword: "", host: "", port: "", user: "", password: "", database: "", ssl: false };
|
|
538
455
|
|
|
@@ -866,7 +783,7 @@ window.__ModuleLoader__.load({
|
|
|
866
783
|
draftTest && draftTest.ok === null ? t("testing") : t("test")),
|
|
867
784
|
React.createElement("button", { className: "dbt-btn", onClick: props.onCancel }, t("cancel")),
|
|
868
785
|
draftTest && draftTest.ok !== null
|
|
869
|
-
? React.createElement("span", {
|
|
786
|
+
? React.createElement("span", { className: draftTest.ok ? "dbt-msg" : "dbt-err" }, draftTest.msg)
|
|
870
787
|
: null,
|
|
871
788
|
),
|
|
872
789
|
);
|
|
@@ -958,7 +875,7 @@ window.__ModuleLoader__.load({
|
|
|
958
875
|
{ className: "dbt-muted", style: { fontFamily: MONO_FONT, fontSize: 11, wordBreak: "break-all" } },
|
|
959
876
|
c.safeUrl || (c.host + ":" + (c.port || "")),
|
|
960
877
|
) : null,
|
|
961
|
-
testInfo[c.id] ? React.createElement("div", {
|
|
878
|
+
testInfo[c.id] ? React.createElement("div", { className: testInfo[c.id].startsWith("✓") ? "dbt-msg" : "dbt-err" }, testInfo[c.id]) : null,
|
|
962
879
|
),
|
|
963
880
|
// 右列:动作按钮(次要语义,danger 仅删除)
|
|
964
881
|
React.createElement(
|
|
@@ -1038,7 +955,7 @@ window.__ModuleLoader__.load({
|
|
|
1038
955
|
}
|
|
1039
956
|
|
|
1040
957
|
/* ---------------- 数据浏览 ---------------- */
|
|
1041
|
-
// Navicat 式对象树:连接 ▸ 库/[schema] ▸
|
|
958
|
+
// Navicat 式对象树:连接 ▸ 库/[schema] ▸ 表,懒加载展开(浏览面板唯一树实例,展开状态随面板生命周期)
|
|
1042
959
|
function BrowseTree(props) {
|
|
1043
960
|
const { conns, projectPath, sel, onSelect, autoExpand } = props;
|
|
1044
961
|
// sel 由父组件持有(弹窗内 active 高亮与初始选中都依赖);onSelect(选中记录, 触发元素) 上报
|
|
@@ -1186,7 +1103,7 @@ window.__ModuleLoader__.load({
|
|
|
1186
1103
|
return React.createElement(
|
|
1187
1104
|
"div",
|
|
1188
1105
|
{ style: { display: "flex", flexDirection: "column", gap: 8 } },
|
|
1189
|
-
// Navicat
|
|
1106
|
+
// Navicat 式对象树:点击展开连接/库,点击表上报选中(由父组件就地预览)
|
|
1190
1107
|
conns.length === 0 ? React.createElement("div", { className: "dbt-muted" }, t("noConns")) :
|
|
1191
1108
|
React.createElement("div", { className: "dbt-group" },
|
|
1192
1109
|
React.createElement("div", { className: "dbt-tree" }, treeRows)),
|
|
@@ -1208,9 +1125,8 @@ window.__ModuleLoader__.load({
|
|
|
1208
1125
|
};
|
|
1209
1126
|
function PreviewGrid(props) {
|
|
1210
1127
|
const { preview, schema, kind, editable, tableType, tableName, dbRef, schemaName, connId, projectPath, onSaved, askConfirm } = props;
|
|
1211
|
-
const [
|
|
1212
|
-
const [
|
|
1213
|
-
const [text, setText] = React.useState(""); // 编辑态 textarea 值(进入浮层时就位)
|
|
1128
|
+
const [sel, setSel] = React.useState(null); // {rowIdx, colName, cell} 底部详情栏当前单元格(null=收起底栏)
|
|
1129
|
+
const [text, setText] = React.useState(""); // 详情栏 textarea 值(选中即就位)
|
|
1214
1130
|
const [editNull, setEditNull] = React.useState(false); // 「设为 NULL」勾选(null 单元格默认勾选)
|
|
1215
1131
|
const [cellErr, setCellErr] = React.useState("");
|
|
1216
1132
|
const [busy, setBusy] = React.useState(false);
|
|
@@ -1221,23 +1137,23 @@ window.__ModuleLoader__.load({
|
|
|
1221
1137
|
? schema.filter((c) => c && c.key === "PRI").map((c) => c.name)
|
|
1222
1138
|
: [];
|
|
1223
1139
|
|
|
1224
|
-
//
|
|
1225
|
-
function
|
|
1226
|
-
setPop({ rowIdx, colName, cell });
|
|
1227
|
-
setEditing(false);
|
|
1140
|
+
// 点单元格开合底栏:同格再点收起;新格选中即把 textarea 初值就位——textarea 空 ↔ 勾选设为 NULL
|
|
1141
|
+
function toggleCell(rowIdx, colName, cell) {
|
|
1228
1142
|
setCellErr("");
|
|
1143
|
+
if (sel && sel.rowIdx === rowIdx && sel.colName === colName) { setSel(null); return; }
|
|
1144
|
+
setSel({ rowIdx, colName, cell });
|
|
1229
1145
|
setEditNull(cell === null);
|
|
1230
1146
|
setText(cell === null ? "" : String(cell));
|
|
1231
1147
|
}
|
|
1232
1148
|
function copyCell() {
|
|
1233
|
-
if (!
|
|
1234
|
-
navigator.clipboard.writeText(String(
|
|
1149
|
+
if (!sel) return;
|
|
1150
|
+
navigator.clipboard.writeText(String(sel.cell)).then(() => {
|
|
1235
1151
|
setCopied(true);
|
|
1236
1152
|
window.setTimeout(() => setCopied(false), 1500);
|
|
1237
1153
|
}, () => { /* 剪贴板不可用(无权限等):静默,按钮文案不变 */ });
|
|
1238
1154
|
}
|
|
1239
1155
|
|
|
1240
|
-
// 单元格编辑资格:返回 null=可编辑,否则为原因 i18n key
|
|
1156
|
+
// 单元格编辑资格:返回 null=可编辑,否则为原因 i18n key(底栏 .dbt-readhint 展示)
|
|
1241
1157
|
function cellReason(colName, shown) {
|
|
1242
1158
|
if (!editable || !dialectOf(kind)) return "readOnlyRo";
|
|
1243
1159
|
if (isMongo) return colName === "_id" ? "readOnlyRo" : null;
|
|
@@ -1252,11 +1168,11 @@ window.__ModuleLoader__.load({
|
|
|
1252
1168
|
return null;
|
|
1253
1169
|
}
|
|
1254
1170
|
|
|
1255
|
-
// 保存:构造写回命令(按 kind 分派)→ 顺序执行(redis 两步)→
|
|
1171
|
+
// 保存:构造写回命令(按 kind 分派)→ 顺序执行(redis 两步)→ 全部成功收起底栏并回调 onSaved
|
|
1256
1172
|
async function saveCell() {
|
|
1257
|
-
if (!
|
|
1173
|
+
if (!sel) return;
|
|
1258
1174
|
setCellErr("");
|
|
1259
|
-
const { rowIdx, colName } =
|
|
1175
|
+
const { rowIdx, colName } = sel;
|
|
1260
1176
|
const row = (preview.rows || [])[rowIdx] || [];
|
|
1261
1177
|
let r;
|
|
1262
1178
|
if (kind === "redis") {
|
|
@@ -1281,26 +1197,32 @@ window.__ModuleLoader__.load({
|
|
|
1281
1197
|
askConfirm,
|
|
1282
1198
|
);
|
|
1283
1199
|
}
|
|
1284
|
-
|
|
1200
|
+
setSel(null);
|
|
1285
1201
|
if (onSaved) onSaved();
|
|
1286
1202
|
} catch (e) {
|
|
1287
|
-
// 取消确认(e.cancelled
|
|
1203
|
+
// 取消确认(e.cancelled)不算失败:底栏保持打开供重试
|
|
1288
1204
|
if (!e.cancelled) setCellErr(t("error") + ": " + String(e && e.message ? e.message : e));
|
|
1289
1205
|
} finally {
|
|
1290
1206
|
setBusy(false);
|
|
1291
1207
|
}
|
|
1292
1208
|
}
|
|
1293
1209
|
|
|
1294
|
-
|
|
1210
|
+
// 切表/翻页联动:预览数据引用变化即收起底栏,避免行号错位指向旧行
|
|
1211
|
+
React.useEffect(() => { setSel(null); }, [preview]);
|
|
1212
|
+
|
|
1213
|
+
const shown = sel ? (sel.cell === null ? "NULL" : String(sel.cell)) : null;
|
|
1214
|
+
const selReason = sel ? cellReason(sel.colName, shown) : null;
|
|
1215
|
+
// 截断/BLOB 独立提示:命中即补显,若已作为只读原因展示过则不再重复
|
|
1216
|
+
const truncHint = !sel ? null : isTruncatedCell(shown) ? "truncatedNoEdit" : isBlobCell(shown) ? "blobNoEdit" : null;
|
|
1295
1217
|
return React.createElement(
|
|
1296
1218
|
"div",
|
|
1297
1219
|
{ className: "dbt-browse-root", style: { display: "flex", flexDirection: "column", flex: 1, minHeight: 0 } },
|
|
1298
|
-
// 顶层只读提示:授权为 ro
|
|
1220
|
+
// 顶层只读提示:授权为 ro 时整格只读,单元格仍可点开底栏查看完整值
|
|
1299
1221
|
!editable ? React.createElement("div", { className: "dbt-readhint" }, t("readOnlyRo")) : null,
|
|
1300
|
-
//
|
|
1222
|
+
// 预览表格区(flex:1 撑满剩余空间;底部详情栏在表格之后、面板流内)
|
|
1301
1223
|
React.createElement(
|
|
1302
1224
|
"div",
|
|
1303
|
-
{ style: {
|
|
1225
|
+
{ style: { flex: 1, minHeight: 0, display: "flex", flexDirection: "column" } },
|
|
1304
1226
|
React.createElement(
|
|
1305
1227
|
"div",
|
|
1306
1228
|
{ className: "dbt-browse-tablewrap" },
|
|
@@ -1321,81 +1243,63 @@ window.__ModuleLoader__.load({
|
|
|
1321
1243
|
React.createElement("td", { key: j, title: row[j] === null ? "NULL" : String(row[j]) },
|
|
1322
1244
|
React.createElement("button", {
|
|
1323
1245
|
className: "dbt-cellbtn", type: "button",
|
|
1324
|
-
onClick: () =>
|
|
1325
|
-
}, row[j] === null ? React.createElement("span", { className: "dbt-nullchip" }, "NULL") : String(row[j]))))),
|
|
1246
|
+
onClick: () => toggleCell(i, colName, row[j]),
|
|
1247
|
+
}, row[j] === null ? React.createElement("span", { className: "dbt-nullchip" }, "NULL") : String(row[j])))))),
|
|
1326
1248
|
),
|
|
1327
1249
|
),
|
|
1328
1250
|
),
|
|
1329
|
-
|
|
1251
|
+
),
|
|
1252
|
+
// 底部详情栏(Navicat/检查器式):预览表格下方一栏,textarea 直接可编辑,点保存写回
|
|
1253
|
+
sel ? React.createElement(
|
|
1254
|
+
"div",
|
|
1255
|
+
{ className: "dbt-celldetail" },
|
|
1256
|
+
// 标题行:完整值 · 列名;null 单元格带 NULL 芯片;只读原因随之展示
|
|
1257
|
+
React.createElement(
|
|
1330
1258
|
"div",
|
|
1331
|
-
|
|
1332
|
-
React.createElement("strong", null, t("cellValue") + " · " +
|
|
1333
|
-
React.createElement(
|
|
1334
|
-
|
|
1335
|
-
{ className: "dbt-cellpop-value" },
|
|
1336
|
-
pop.cell === null ? React.createElement("span", { className: "dbt-nullchip" }, "NULL") : String(pop.cell),
|
|
1337
|
-
),
|
|
1338
|
-
popReason ? React.createElement("div", { className: "dbt-readhint" }, t(popReason)) : null,
|
|
1339
|
-
cellErr ? React.createElement("div", { className: "dbt-err" }, cellErr) : null,
|
|
1340
|
-
editing
|
|
1341
|
-
? React.createElement(
|
|
1342
|
-
"div",
|
|
1343
|
-
{ className: "dbt-celledit" },
|
|
1344
|
-
React.createElement("textarea", { value: text, onChange: (e) => setText(e.target.value) }),
|
|
1345
|
-
React.createElement(
|
|
1346
|
-
"label",
|
|
1347
|
-
// 不用 .dbt-row:全局 input flex:1 会把 checkbox 拉伸占满、标签被推到远端(视觉审查命中)
|
|
1348
|
-
{ style: { display: "flex", alignItems: "center", gap: 6, flexWrap: "nowrap", width: "fit-content" } },
|
|
1349
|
-
React.createElement("input", { type: "checkbox", style: { flex: "none", minWidth: 0, width: "auto" }, checked: editNull, onChange: (e) => setEditNull(e.target.checked) }),
|
|
1350
|
-
t("setNull"),
|
|
1351
|
-
),
|
|
1352
|
-
React.createElement(
|
|
1353
|
-
"div",
|
|
1354
|
-
{ className: "dbt-row" },
|
|
1355
|
-
React.createElement("button", { className: "dbt-btn primary", disabled: busy, onClick: saveCell }, busy ? t("cellSaving") : t("save")),
|
|
1356
|
-
React.createElement("button", { className: "dbt-btn", disabled: busy, onClick: () => setEditing(false) }, t("cancel")),
|
|
1357
|
-
),
|
|
1358
|
-
)
|
|
1359
|
-
: React.createElement(
|
|
1360
|
-
"div",
|
|
1361
|
-
{ className: "dbt-cellpop-actions" },
|
|
1362
|
-
React.createElement("button", { className: "dbt-btn", onClick: copyCell }, copied ? t("copied") : t("copyBtn")),
|
|
1363
|
-
popReason === null ? React.createElement("button", { className: "dbt-btn primary", onClick: () => setEditing(true) }, t("editCell")) : null,
|
|
1364
|
-
React.createElement("button", { className: "dbt-btn", onClick: () => setPop(null) }, t("close")),
|
|
1365
|
-
),
|
|
1366
|
-
) : null,
|
|
1259
|
+
null,
|
|
1260
|
+
React.createElement("strong", null, t("cellValue") + " · " + sel.colName),
|
|
1261
|
+
sel.cell === null ? React.createElement("span", { className: "dbt-nullchip" }, "NULL") : null,
|
|
1262
|
+
selReason ? React.createElement("span", { className: "dbt-readhint" }, t(selReason)) : null,
|
|
1367
1263
|
),
|
|
1368
|
-
|
|
1264
|
+
// 完整值编辑区:textarea 恒为编辑形态,不可编辑时只读
|
|
1265
|
+
React.createElement("textarea", {
|
|
1266
|
+
className: "dbt-celldetail-input",
|
|
1267
|
+
value: text,
|
|
1268
|
+
readOnly: selReason !== null,
|
|
1269
|
+
onChange: (e) => setText(e.target.value),
|
|
1270
|
+
}),
|
|
1271
|
+
sel.cell === null ? React.createElement(
|
|
1272
|
+
"label",
|
|
1273
|
+
// 不用 .dbt-row:全局 input flex:1 会把 checkbox 拉伸占满、标签被推到远端(视觉审查命中)
|
|
1274
|
+
{ style: { display: "flex", alignItems: "center", gap: 6, flexWrap: "nowrap", width: "fit-content" } },
|
|
1275
|
+
React.createElement("input", { type: "checkbox", style: { flex: "none", minWidth: 0, width: "auto" }, checked: editNull, onChange: (e) => setEditNull(e.target.checked) }),
|
|
1276
|
+
t("setNull"),
|
|
1277
|
+
) : null,
|
|
1278
|
+
truncHint && truncHint !== selReason ? React.createElement("span", { className: "dbt-readhint" }, t(truncHint)) : null,
|
|
1279
|
+
cellErr ? React.createElement("span", { className: "dbt-err" }, cellErr) : null,
|
|
1280
|
+
React.createElement(
|
|
1281
|
+
"div",
|
|
1282
|
+
{ className: "dbt-row" },
|
|
1283
|
+
React.createElement("button", { className: "dbt-btn", onClick: copyCell }, copied ? t("copied") : t("copyBtn")),
|
|
1284
|
+
selReason === null ? React.createElement("button", { className: "dbt-btn primary", disabled: busy, onClick: saveCell }, busy ? t("cellSaving") : t("save")) : null,
|
|
1285
|
+
React.createElement("button", { className: "dbt-btn", onClick: () => setSel(null) }, t("close")),
|
|
1286
|
+
),
|
|
1287
|
+
) : null,
|
|
1369
1288
|
);
|
|
1370
1289
|
}
|
|
1371
1290
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1291
|
+
// Navicat 式浏览面板(内嵌侧边栏):上半对象树选库-表,下半结构/数据预览,点表即看,不再弹窗
|
|
1292
|
+
function BrowsePane(props) {
|
|
1293
|
+
const { conns, projectPath, grants, askConfirm } = props;
|
|
1374
1294
|
// grants(editable 判定)与 askConfirm(单元格写回确认)下传 PreviewGrid
|
|
1375
|
-
|
|
1376
|
-
// createPortal 不换组件实例——portalReady 切换前后是同一 DOM 节点,pos state 保持,居中定位不受 portal 重挂影响。
|
|
1377
|
-
const [portalReady, setPortalReady] = React.useState(!!portalImpl);
|
|
1378
|
-
React.useEffect(() => {
|
|
1379
|
-
resolvePortalAsync(ctx);
|
|
1380
|
-
resolveRootAsync(ctx);
|
|
1381
|
-
onPortalReady(function () { setPortalReady(true); });
|
|
1382
|
-
// 诊断:一次性输出(帮助真机排查 iframe 隔离与 portal/createRoot 解析结果、PiP 尝试结果)
|
|
1383
|
-
if (!window.__dbtPortalDiag) {
|
|
1384
|
-
window.__dbtPortalDiag = true;
|
|
1385
|
-
console.info("[dbt] portal:", portalImpl ? "sync/async ok" : "unavailable",
|
|
1386
|
-
"createRoot:", rootSource || "none", "pip:", pipResult,
|
|
1387
|
-
"sameDoc:", window.top === window.self);
|
|
1388
|
-
}
|
|
1389
|
-
}, []);
|
|
1390
|
-
const [sel, setSel] = React.useState(null); // 弹窗内当前选中(null=未选表,右栏显示引导空态)
|
|
1295
|
+
const [sel, setSel] = React.useState(null); // 面板内当前选中(null=未选表,内容区显示引导空态)
|
|
1391
1296
|
const [view, setView] = React.useState("structure"); // structure | preview(纯视图切换,不影响数据加载)
|
|
1392
1297
|
const [schema, setSchema] = React.useState([]);
|
|
1393
1298
|
const [preview, setPreview] = React.useState(null); // QueryResult
|
|
1394
1299
|
const [page, setPage] = React.useState(1);
|
|
1395
1300
|
const [busy, setBusy] = React.useState("");
|
|
1396
|
-
const [loadErr, setLoadErr] = React.useState(""); //
|
|
1301
|
+
const [loadErr, setLoadErr] = React.useState(""); // 内容区加载失败(就地显示 + 重试)
|
|
1397
1302
|
const openSeq = React.useRef(0); // 请求序号守卫
|
|
1398
|
-
const closeRef = React.useRef(null);
|
|
1399
1303
|
|
|
1400
1304
|
// 请求序号守卫(沿用面板):快速切表/翻页时丢弃晚到旧响应,防止旧数据覆盖新选中项
|
|
1401
1305
|
const openTable = React.useCallback((s, pg) => {
|
|
@@ -1412,59 +1316,16 @@ window.__ModuleLoader__.load({
|
|
|
1412
1316
|
if (seq !== openSeq.current) return; // 旧请求晚到,丢弃
|
|
1413
1317
|
setSchema(sch || []); setPreview(prev); setPage(pg || 1);
|
|
1414
1318
|
})
|
|
1415
|
-
//
|
|
1319
|
+
// 面板内错误就地显示(不走面板顶部错误区)
|
|
1416
1320
|
.catch((e) => { if (seq === openSeq.current) setLoadErr(String(e && e.message ? e.message : e)); })
|
|
1417
1321
|
.finally(() => { if (seq === openSeq.current) setBusy(""); });
|
|
1418
1322
|
}, [projectPath]);
|
|
1419
1323
|
React.useEffect(() => { if (sel) openTable(sel, 1); }, [sel]); // eslint-disable-line
|
|
1420
1324
|
|
|
1421
|
-
//
|
|
1422
|
-
// 经容器 onKeyDown 触发(stopPropagation 防止宿主级快捷键同时响应)。初始焦点=关闭按钮
|
|
1423
|
-
React.useEffect(() => {
|
|
1424
|
-
if (closeRef.current) closeRef.current.focus();
|
|
1425
|
-
}, []);
|
|
1426
|
-
|
|
1427
|
-
// 拖动定位:首帧给个保守值,mount 后按浮窗实测尺寸居中(坐标基准取浮窗所在 document 的视口——DOM 浮窗为主视口,PiP 独立窗口为 PiP 视口)
|
|
1428
|
-
const dialogRef2 = React.useRef(null);
|
|
1429
|
-
const [pos, setPos] = React.useState(() => ({ top: 72, left: 12 }));
|
|
1430
|
-
React.useEffect(() => {
|
|
1431
|
-
const el = dialogRef2.current;
|
|
1432
|
-
if (!el) return;
|
|
1433
|
-
const view = (el.ownerDocument && el.ownerDocument.defaultView) || window;
|
|
1434
|
-
const w = view.innerWidth || 1200, h = view.innerHeight || 800;
|
|
1435
|
-
setPos({
|
|
1436
|
-
top: Math.max(0, Math.round((h - el.offsetHeight) / 2)),
|
|
1437
|
-
left: Math.max(0, Math.round((w - el.offsetWidth) / 2)),
|
|
1438
|
-
});
|
|
1439
|
-
}, []);
|
|
1440
|
-
const draggingRef = React.useRef(null); // {dx, dy}
|
|
1441
|
-
const [dragging, setDragging] = React.useState(false);
|
|
1442
|
-
function stopDrag() { draggingRef.current = null; setDragging(false); }
|
|
1443
|
-
function onHeaderPointerDown(e) {
|
|
1444
|
-
// header 内可点元素(关闭按钮)不启动拖动,否则 setPointerCapture 会把 click 重定向到 header 使按钮失效
|
|
1445
|
-
if (e.target.closest && e.target.closest("button")) return;
|
|
1446
|
-
draggingRef.current = { dx: e.clientX - pos.left, dy: e.clientY - pos.top };
|
|
1447
|
-
e.currentTarget.setPointerCapture(e.pointerId);
|
|
1448
|
-
setDragging(true);
|
|
1449
|
-
}
|
|
1450
|
-
function onHeaderPointerMove(e) {
|
|
1451
|
-
if (!draggingRef.current) return;
|
|
1452
|
-
const view = (e.currentTarget.ownerDocument && e.currentTarget.ownerDocument.defaultView) || window;
|
|
1453
|
-
const h = view.innerHeight || 800, w = view.innerWidth || 1200;
|
|
1454
|
-
// clamp 全部用浮窗实测尺寸:整体保持在视口内(窄视口下常量估宽会把窗拖丢)
|
|
1455
|
-
const dlg = e.currentTarget.parentElement;
|
|
1456
|
-
const dlgH = (dlg && dlg.offsetHeight) || 720;
|
|
1457
|
-
const dlgW = (dlg && dlg.offsetWidth) || 1100;
|
|
1458
|
-
setPos({
|
|
1459
|
-
top: Math.min(Math.max(e.clientY - draggingRef.current.dy, 0), Math.max(0, h - dlgH)),
|
|
1460
|
-
left: Math.min(Math.max(e.clientX - draggingRef.current.dx, 0), Math.max(0, w - dlgW)),
|
|
1461
|
-
});
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
// 右栏互斥状态:未选中引导 / 加载骨架 / 错误重试 / 空态 / 表格
|
|
1325
|
+
// 内容区互斥状态:未选中引导 / 加载骨架 / 错误重试 / 空态 / 表格
|
|
1465
1326
|
function rightPane() {
|
|
1466
1327
|
if (!sel && busy !== "open" && !loadErr) {
|
|
1467
|
-
//
|
|
1328
|
+
// 未选表:显示引导空态(内嵌面板可直接浏览,不强制先选表)
|
|
1468
1329
|
return React.createElement("div", { className: "dbt-browse-empty" }, t("selectTableHint"));
|
|
1469
1330
|
}
|
|
1470
1331
|
if (busy === "open") {
|
|
@@ -1474,7 +1335,7 @@ window.__ModuleLoader__.load({
|
|
|
1474
1335
|
[0, 1, 2, 3, 4, 5].map((c) => React.createElement("div", { className: "dbt-browse-skelcell", key: c })))));
|
|
1475
1336
|
}
|
|
1476
1337
|
if (loadErr) {
|
|
1477
|
-
// 错误就地显示 +
|
|
1338
|
+
// 错误就地显示 + 重试(内嵌面板保持就地呈现,不走 props.onError)
|
|
1478
1339
|
return React.createElement("div", { className: "dbt-browse-errorbox" },
|
|
1479
1340
|
React.createElement("div", { className: "dbt-err" }, t("error") + ": " + loadErr),
|
|
1480
1341
|
React.createElement("button", { className: "dbt-btn", onClick: () => openTable(sel, 1) }, t("retry")));
|
|
@@ -1506,168 +1367,49 @@ window.__ModuleLoader__.load({
|
|
|
1506
1367
|
});
|
|
1507
1368
|
}
|
|
1508
1369
|
|
|
1509
|
-
//
|
|
1510
|
-
return
|
|
1370
|
+
// 内嵌面板纵向布局:上半对象树(限高滚动,容器类名留给样式层),下半预览主区 + 分页
|
|
1371
|
+
return React.createElement(
|
|
1511
1372
|
"div",
|
|
1512
|
-
{
|
|
1513
|
-
className: "dbt-browse-root dbt-browse-dialog" + (dragging ? " dragging" : ""),
|
|
1514
|
-
ref: dialogRef2,
|
|
1515
|
-
style: { position: "fixed", top: pos.top, left: pos.left },
|
|
1516
|
-
role: "dialog", "aria-labelledby": "dbt-browse-title",
|
|
1517
|
-
onKeyDown: (e) => {
|
|
1518
|
-
if (e.key === "Escape") { e.stopPropagation(); onClose(); }
|
|
1519
|
-
},
|
|
1520
|
-
},
|
|
1521
|
-
// ---- header:面包屑 + 关闭(整条 header 可拖动)----
|
|
1373
|
+
{ className: "dbt-view" },
|
|
1522
1374
|
React.createElement(
|
|
1523
1375
|
"div",
|
|
1524
|
-
{
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
conn: (conns.find((c) => c.id === sel.connId) || {}).name || sel.connId,
|
|
1535
|
-
db: sel.schemaName ? sel.db + "." + sel.schemaName : sel.db,
|
|
1536
|
-
table: sel.table.name,
|
|
1537
|
-
})),
|
|
1538
|
-
React.createElement(
|
|
1539
|
-
"button",
|
|
1540
|
-
{ className: "dbt-browse-close", ref: closeRef, "aria-label": t("close"), onClick: onClose },
|
|
1541
|
-
React.createElement("svg", { width: 12, height: 12, viewBox: "0 0 12 12", fill: "none", "aria-hidden": "true" },
|
|
1542
|
-
React.createElement("path", { d: "M2.5 2.5 L9.5 9.5 M9.5 2.5 L2.5 9.5", stroke: "currentColor", strokeWidth: 1.4, strokeLinecap: "round" })),
|
|
1543
|
-
),
|
|
1544
|
-
),
|
|
1545
|
-
// ---- body:左树 + 右主区 ----
|
|
1376
|
+
{ style: { maxHeight: 280, overflowY: "auto" } },
|
|
1377
|
+
React.createElement(BrowseTree, {
|
|
1378
|
+
conns, projectPath, sel, autoExpand: true, // 内嵌后此树是唯一树实例,挂载即展开第一层级
|
|
1379
|
+
onSelect: (s) => setSel(s), // 树内切表
|
|
1380
|
+
}),
|
|
1381
|
+
),
|
|
1382
|
+
React.createElement(
|
|
1383
|
+
"div",
|
|
1384
|
+
{ className: "dbt-browse-main" },
|
|
1385
|
+
// 结构/数据:mini segmented(复用面板同款与 i18n key)
|
|
1546
1386
|
React.createElement(
|
|
1547
1387
|
"div",
|
|
1548
|
-
{ className: "dbt-browse-
|
|
1549
|
-
React.createElement(
|
|
1550
|
-
"div",
|
|
1551
|
-
{ className: "dbt-browse-tree" },
|
|
1552
|
-
React.createElement(BrowseTree, {
|
|
1553
|
-
conns, projectPath, sel, autoExpand: true, // 本任务后弹窗树是唯一树实例,挂载即展开第一层级
|
|
1554
|
-
onSelect: (s) => setSel(s), // 弹窗内树切表
|
|
1555
|
-
}),
|
|
1556
|
-
),
|
|
1388
|
+
{ className: "dbt-browse-tabs" },
|
|
1557
1389
|
React.createElement(
|
|
1558
1390
|
"div",
|
|
1559
|
-
{ className: "dbt-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
{ className: "dbt-browse-tabs" },
|
|
1564
|
-
React.createElement(
|
|
1565
|
-
"div",
|
|
1566
|
-
{ className: "dbt-seg" },
|
|
1567
|
-
["structure", "preview"].map((v) =>
|
|
1568
|
-
React.createElement("button", { key: v, className: view === v ? "active" : "", onClick: () => setView(v) },
|
|
1569
|
-
v === "structure" ? t("structure") : t("preview"))),
|
|
1570
|
-
),
|
|
1571
|
-
),
|
|
1572
|
-
React.createElement("div", { className: "dbt-browse-content" }, rightPane()),
|
|
1391
|
+
{ className: "dbt-seg" },
|
|
1392
|
+
["structure", "preview"].map((v) =>
|
|
1393
|
+
React.createElement("button", { key: v, className: view === v ? "active" : "", onClick: () => setView(v) },
|
|
1394
|
+
v === "structure" ? t("structure") : t("preview"))),
|
|
1573
1395
|
),
|
|
1574
1396
|
),
|
|
1575
|
-
//
|
|
1576
|
-
|
|
1577
|
-
"div",
|
|
1578
|
-
{ className: "dbt-browse-footer" },
|
|
1579
|
-
React.createElement("button", { className: "dbt-btn", disabled: page <= 1 || busy === "open", onClick: () => openTable(sel, page - 1) }, "‹ " + t("prevPage")),
|
|
1580
|
-
React.createElement("span", { className: "dbt-muted" }, t("pageInfo", { page })),
|
|
1581
|
-
React.createElement("span", { style: { flex: 1 } }),
|
|
1582
|
-
preview && preview.truncated ? React.createElement("span", { className: "dbt-muted" }, t("previewTruncated")) : null,
|
|
1583
|
-
React.createElement("button", { className: "dbt-btn", disabled: (preview && preview.truncated) === false || busy === "open", onClick: () => openTable(sel, page + 1) }, t("nextPage") + " ›"),
|
|
1584
|
-
) : null,
|
|
1397
|
+
// 限高兜底:无固定高度弹窗壳后给内容区可视上限,预览表在容器内滚动、单元格浮层贴底可见
|
|
1398
|
+
React.createElement("div", { className: "dbt-browse-content", style: { maxHeight: 420 } }, rightPane()),
|
|
1585
1399
|
),
|
|
1400
|
+
// 分页条(仅已选表的数据预览视图;空选中态不显示)
|
|
1401
|
+
sel && view === "preview" && !loadErr ? React.createElement(
|
|
1402
|
+
"div",
|
|
1403
|
+
{ className: "dbt-browse-footer" },
|
|
1404
|
+
React.createElement("button", { className: "dbt-btn", disabled: page <= 1 || busy === "open", onClick: () => openTable(sel, page - 1) }, "‹ " + t("prevPage")),
|
|
1405
|
+
React.createElement("span", { className: "dbt-muted" }, t("pageInfo", { page })),
|
|
1406
|
+
React.createElement("span", { style: { flex: 1 } }),
|
|
1407
|
+
preview && preview.truncated ? React.createElement("span", { className: "dbt-muted" }, t("previewTruncated")) : null,
|
|
1408
|
+
React.createElement("button", { className: "dbt-btn", disabled: (preview && preview.truncated) === false || busy === "open", onClick: () => openTable(sel, page + 1) }, t("nextPage") + " ›"),
|
|
1409
|
+
) : null,
|
|
1586
1410
|
);
|
|
1587
1411
|
}
|
|
1588
1412
|
|
|
1589
|
-
/* ---------------- PiP 独立窗口(Document Picture-in-Picture) ---------------- */
|
|
1590
|
-
// OS 级独立窗口:可拖出桌面端主窗口、系统级置顶。内容为完整数据浏览 UI(同 JS 环境复用
|
|
1591
|
-
// BrowseDialog/DangerDialog 与 mock 之外的宿主 fetch 同源);DOM 浮窗作为不可用时的降级。
|
|
1592
|
-
// 打开成功返回 pipWindow,失败返回 null(调用方降级为 DOM 浮窗)。
|
|
1593
|
-
function PipBrowseApp(props) { // {ctx, conns, projectPath, grants, onClose}
|
|
1594
|
-
// 危险操作确认渲染在 PiP 窗口内(Panel 的确认对话框在主窗口,PiP 用户看不见)
|
|
1595
|
-
const [confirmReq, setConfirmReq] = React.useState(null);
|
|
1596
|
-
const askConfirm = React.useCallback((info) => new Promise((resolve) => {
|
|
1597
|
-
setConfirmReq(Object.assign({}, info, { resolve }));
|
|
1598
|
-
}), []);
|
|
1599
|
-
return React.createElement(
|
|
1600
|
-
React.Fragment,
|
|
1601
|
-
null,
|
|
1602
|
-
React.createElement(BrowseDialog, {
|
|
1603
|
-
ctx: props.ctx, conns: props.conns, projectPath: props.projectPath,
|
|
1604
|
-
grants: props.grants, askConfirm: askConfirm, onClose: props.onClose,
|
|
1605
|
-
}),
|
|
1606
|
-
confirmReq ? React.createElement(DangerDialog, { challenge: confirmReq, onClose: () => setConfirmReq(null) }) : null,
|
|
1607
|
-
);
|
|
1608
|
-
}
|
|
1609
|
-
function openBrowsePip(ctx, deps) { // deps: {conns, projectPath, grants, onClosed, onFallback} → 返回 pipWindow | null
|
|
1610
|
-
try {
|
|
1611
|
-
const dpip = window.documentPictureInPicture;
|
|
1612
|
-
// 入口只硬性要求 dpip;createRoot/portal 未就绪不再放弃——手势内先开窗,root 异步就绪后渲染,超时由调用方降级
|
|
1613
|
-
if (!dpip || typeof dpip.requestWindow !== "function") return null;
|
|
1614
|
-
resolveRootAsync(ctx); // createRoot 同步未就绪时,趁 3 秒超时窗口发起宿主异步 import
|
|
1615
|
-
// PiP 只能在用户手势内同步发起 requestWindow,Promise then 里再做 DOM 装配
|
|
1616
|
-
const p = dpip.requestWindow({ width: 1100, height: 720 });
|
|
1617
|
-
pipResult = "opened"; // requestWindow 已发起;拒绝走下方 catch
|
|
1618
|
-
return p.then((w) => {
|
|
1619
|
-
const doc = w.document;
|
|
1620
|
-
// 相对地址基准:PiP 文档是 about:blank,fetch 的相对 API 路径须按主 origin 解析
|
|
1621
|
-
const base = doc.createElement("base");
|
|
1622
|
-
base.href = window.location.origin + "/";
|
|
1623
|
-
doc.head.appendChild(base);
|
|
1624
|
-
// 样式:整套 --dbt-* token 与规则注入 PiP 文档
|
|
1625
|
-
if (styleEl) doc.head.appendChild(doc.importNode(styleEl, true));
|
|
1626
|
-
doc.body.style.margin = "0";
|
|
1627
|
-
// 加载占位:createRoot 异步解析期间给用户可见反馈(PiP 场景少用,不做 i18n key)
|
|
1628
|
-
const ph = doc.createElement("div");
|
|
1629
|
-
ph.style.cssText = "display:flex;align-items:center;justify-content:center;height:100%;font:14px sans-serif;color:#888;";
|
|
1630
|
-
ph.textContent = "正在打开数据浏览…";
|
|
1631
|
-
doc.body.appendChild(ph);
|
|
1632
|
-
let root = null;
|
|
1633
|
-
let settled = false; // root 已渲染或窗口已关闭:晚到的 onRootReady 回调与超时兜底均不再处理
|
|
1634
|
-
const closePip = () => {
|
|
1635
|
-
if (root) { try { root.unmount(); } catch (e) { /* 已卸载 */ } }
|
|
1636
|
-
portalDisabled = false;
|
|
1637
|
-
try { w.close(); } catch (e2) { /* 已关闭 */ }
|
|
1638
|
-
};
|
|
1639
|
-
onRootReady(() => {
|
|
1640
|
-
if (settled) return;
|
|
1641
|
-
settled = true;
|
|
1642
|
-
portalDisabled = true; // PiP root 已在 pipDoc.body 上,禁 portal 防 DOM 被拉回主窗口
|
|
1643
|
-
root = reactCreateRoot(doc.body);
|
|
1644
|
-
root.render(React.createElement(PipBrowseApp, {
|
|
1645
|
-
ctx, conns: deps.conns, projectPath: deps.projectPath, grants: deps.grants,
|
|
1646
|
-
onClose: () => { closePip(); deps.onClosed(); },
|
|
1647
|
-
}));
|
|
1648
|
-
ph.remove();
|
|
1649
|
-
});
|
|
1650
|
-
w.addEventListener("pagehide", () => { // 用户点 PiP 窗口系统关闭钮
|
|
1651
|
-
settled = true;
|
|
1652
|
-
if (root) { try { root.unmount(); } catch (e) { /* 已卸载 */ } }
|
|
1653
|
-
portalDisabled = false;
|
|
1654
|
-
deps.onClosed();
|
|
1655
|
-
});
|
|
1656
|
-
// 超时兜底:3 秒内 createRoot 没就绪 → 关 PiP 清理并回调降级(调用方打开 DOM 浮窗)
|
|
1657
|
-
setTimeout(() => {
|
|
1658
|
-
if (settled) return;
|
|
1659
|
-
settled = true;
|
|
1660
|
-
closePip();
|
|
1661
|
-
deps.onFallback();
|
|
1662
|
-
}, 3000);
|
|
1663
|
-
return w;
|
|
1664
|
-
}).catch((err) => { pipResult = "rejected:" + (err && err.name || "Error"); return null; });
|
|
1665
|
-
} catch (e) {
|
|
1666
|
-
pipResult = "rejected:" + (e && e.name || "Error");
|
|
1667
|
-
return Promise.resolve(null);
|
|
1668
|
-
}
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
1413
|
/* ---------------- SQL 控制台 ---------------- */
|
|
1672
1414
|
// Apple 等宽字体栈(规范 §1):优先样式层 --dbt-mono token,fallback 内联栈
|
|
1673
1415
|
const MONO_FONT = "var(--dbt-mono, ui-monospace, \"SF Mono\", Menlo, Consolas, monospace)";
|
|
@@ -1794,25 +1536,6 @@ window.__ModuleLoader__.load({
|
|
|
1794
1536
|
const [error, setError] = React.useState("");
|
|
1795
1537
|
const [message, setMessage] = React.useState("");
|
|
1796
1538
|
const [confirmReq, setConfirmReq] = React.useState(null); // {statement,danger,reason,resolve}
|
|
1797
|
-
// 数据浏览浮窗开关:由 Panel 层持有(不随 view 切换卸载),跨 tab 常驻
|
|
1798
|
-
const [browseOpen, setBrowseOpen] = React.useState(false);
|
|
1799
|
-
// PiP 独立窗口句柄:优先于 DOM 浮窗(OS 级窗口,可拖出桌面端主窗口);存在时 DOM 浮窗不渲染
|
|
1800
|
-
const [pipWin, setPipWin] = React.useState(null);
|
|
1801
|
-
function openBrowse() {
|
|
1802
|
-
if (pipWin) { try { pipWin.focus(); } catch (e) { /* 已关闭 */ } return; }
|
|
1803
|
-
// requestWindow 必须在用户手势内同步发起(本函数仅由 onClick 直调)
|
|
1804
|
-
const p = openBrowsePip(props.ctx, {
|
|
1805
|
-
conns, projectPath, grants,
|
|
1806
|
-
onClosed: () => setPipWin(null),
|
|
1807
|
-
// PiP 超时降级(createRoot 未就绪):清掉窗口句柄并回退 DOM 浮窗
|
|
1808
|
-
onFallback: () => { setPipWin(null); setBrowseOpen(true); },
|
|
1809
|
-
});
|
|
1810
|
-
if (p && typeof p.then === "function") {
|
|
1811
|
-
p.then((w) => { if (w) setPipWin(w); else setBrowseOpen(true); });
|
|
1812
|
-
} else {
|
|
1813
|
-
setBrowseOpen(true);
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
1539
|
|
|
1817
1540
|
const askConfirm = React.useCallback((info) => new Promise((resolve) => {
|
|
1818
1541
|
setConfirmReq(Object.assign({}, info, { resolve }));
|
|
@@ -1867,10 +1590,9 @@ window.__ModuleLoader__.load({
|
|
|
1867
1590
|
"div",
|
|
1868
1591
|
{ className: "dbt-tabs" },
|
|
1869
1592
|
[["manage", t("viewManage")], ["grants", t("viewGrants")], ["browse", t("viewBrowse")], ["console", t("viewConsole")]].map(([v, label]) =>
|
|
1870
|
-
// browse tab:切视图的同时直接打开浮窗(单击即用,不必再进树点表)
|
|
1871
1593
|
React.createElement("button", {
|
|
1872
1594
|
key: v, className: view === v ? "active" : "",
|
|
1873
|
-
onClick: () =>
|
|
1595
|
+
onClick: () => setView(v),
|
|
1874
1596
|
}, label)),
|
|
1875
1597
|
),
|
|
1876
1598
|
(projectEdited || !projectPath)
|
|
@@ -1906,25 +1628,14 @@ window.__ModuleLoader__.load({
|
|
|
1906
1628
|
React.createElement("div", { className: "dbt-view", key: view },
|
|
1907
1629
|
view === "manage" ? React.createElement(ManageView, shared) : null,
|
|
1908
1630
|
view === "grants" ? React.createElement(GrantsView, shared) : null,
|
|
1909
|
-
// browse
|
|
1910
|
-
view === "browse" ? React.createElement(
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
// 主提示用正常文字色(dbt-muted 11px 次要色压深底对比不足,视觉审查命中)
|
|
1914
|
-
React.createElement("div", { style: { fontSize: 13 } }, t("browsePanelHint")),
|
|
1915
|
-
React.createElement("div", { className: "dbt-row" },
|
|
1916
|
-
React.createElement("button", { className: "dbt-btn primary", onClick: openBrowse }, t("reopenBrowse"))),
|
|
1917
|
-
) : null,
|
|
1631
|
+
// browse:内嵌浏览面板(对象树 + 结构/数据预览,点表即看)
|
|
1632
|
+
view === "browse" ? React.createElement(BrowsePane, {
|
|
1633
|
+
ctx: props.ctx, conns, projectPath, grants, askConfirm,
|
|
1634
|
+
}) : null,
|
|
1918
1635
|
view === "console" ? React.createElement(ConsoleView, Object.assign({}, shared, { askConfirm })) : null,
|
|
1919
1636
|
),
|
|
1920
1637
|
React.createElement("div", { className: "dbt-muted" }, t("footerHint")),
|
|
1921
1638
|
confirmReq ? React.createElement(DangerDialog, { challenge: confirmReq, onClose: () => setConfirmReq(null) }) : null,
|
|
1922
|
-
// 浮窗常驻:portal 到 body 且在 dbt-view 外,切 tab 时不消失
|
|
1923
|
-
browseOpen ? React.createElement(BrowseDialog, {
|
|
1924
|
-
conns, projectPath, grants, askConfirm,
|
|
1925
|
-
ctx: props.ctx, // 宿主 ctx 透传给浮窗,供 portal 异步解析(ctx.modules.import)
|
|
1926
|
-
onClose: () => setBrowseOpen(false),
|
|
1927
|
-
}) : null,
|
|
1928
1639
|
);
|
|
1929
1640
|
}
|
|
1930
1641
|
|
|
@@ -1947,7 +1658,7 @@ window.__ModuleLoader__.load({
|
|
|
1947
1658
|
key: "db-tool-" + String(localeKey || "zh"),
|
|
1948
1659
|
visible: props.visible,
|
|
1949
1660
|
scope: props.scope,
|
|
1950
|
-
ctx: ctx, // 宿主 ctx 下传,Panel 透传给
|
|
1661
|
+
ctx: ctx, // 宿主 ctx 下传,Panel 透传给 BrowsePane
|
|
1951
1662
|
});
|
|
1952
1663
|
}
|
|
1953
1664
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { assertIdent, clampLimit, clampOffset, humanize, normalizeCell, quoteIdent } from './common.js';
|
|
2
|
-
function poolConfig(conn) {
|
|
2
|
+
function poolConfig(conn, overrideUrl) {
|
|
3
3
|
const base = {
|
|
4
4
|
max: 10,
|
|
5
5
|
idleTimeoutMillis: 30000,
|
|
6
6
|
connectionTimeoutMillis: 5000,
|
|
7
7
|
};
|
|
8
8
|
if (conn.url) {
|
|
9
|
-
|
|
9
|
+
// overrideUrl:多主机探测选定台 / 剥 jdbc: 后的最终 connectionString
|
|
10
|
+
base.connectionString = overrideUrl ?? conn.url;
|
|
10
11
|
}
|
|
11
12
|
else if (conn.fields) {
|
|
12
13
|
const f = conn.fields;
|
|
@@ -29,8 +30,122 @@ function poolConfig(conn) {
|
|
|
29
30
|
base.ssl = { rejectUnauthorized: false };
|
|
30
31
|
return base;
|
|
31
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* 规范化 JDBC / 多主机风格连接 URL。
|
|
35
|
+
*
|
|
36
|
+
* 背景:`jdbc:gaussdb://...` 的 `jdbc:` 是 WHATWG 非特殊 scheme,驱动内 new URL()
|
|
37
|
+
* 把 `gaussdb://...` 当不透明路径 → hostname/port 为空 → 兜底连本机;多主机
|
|
38
|
+
* authority(host:port,host:port)端口段含逗号非数字 → new URL 直接抛 Invalid URL。
|
|
39
|
+
* 故自行按文本解析 authority,不经 WHATWG 解析。
|
|
40
|
+
*/
|
|
41
|
+
export function normalizeUrl(url) {
|
|
42
|
+
const stripped = url.replace(/^jdbc:/i, '');
|
|
43
|
+
const schemeEnd = stripped.indexOf('://');
|
|
44
|
+
// 无 authority(非 URL 形态):整串原样保留,交由驱动按既有逻辑处理
|
|
45
|
+
if (schemeEnd < 0)
|
|
46
|
+
return { prefix: stripped, suffix: '' };
|
|
47
|
+
const rest = stripped.slice(schemeEnd + 3);
|
|
48
|
+
const authEnd = /[/?#]/.exec(rest)?.index ?? rest.length;
|
|
49
|
+
const authority = rest.slice(0, authEnd);
|
|
50
|
+
const suffix = rest.slice(authEnd);
|
|
51
|
+
const at = authority.lastIndexOf('@');
|
|
52
|
+
const userinfo = at >= 0 ? authority.slice(0, at + 1) : '';
|
|
53
|
+
const hostPart = authority.slice(at + 1);
|
|
54
|
+
const hosts = hostPart.split(',').map((seg) => {
|
|
55
|
+
// ponytail: 按最后一个冒号拆 host/port,仅支持 IPv4/主机名(IPv6 需方括号感知,遇到再加)
|
|
56
|
+
const ci = seg.lastIndexOf(':');
|
|
57
|
+
if (ci < 0)
|
|
58
|
+
return { host: seg };
|
|
59
|
+
const port = Number.parseInt(seg.slice(ci + 1), 10);
|
|
60
|
+
return Number.isFinite(port) ? { host: seg.slice(0, ci), port } : { host: seg };
|
|
61
|
+
});
|
|
62
|
+
const prefix = `${stripped.slice(0, schemeEnd)}://${userinfo}`;
|
|
63
|
+
// 单台不设 hosts(调用方只在多台时探测)
|
|
64
|
+
if (hosts.length <= 1)
|
|
65
|
+
return { prefix: prefix + hostPart, suffix };
|
|
66
|
+
return { prefix, hosts, suffix };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 逐台探测可用节点(移植自用户项目 GaussDB-MCP 的 probeHosts):返回第一台可连
|
|
70
|
+
* (masterOnly 时须为主节点,pg_is_in_recovery() 为 true 的备节点跳过)的主机;
|
|
71
|
+
* 全部失败返回 undefined(调用方回退首台,让 testConnect 报真实连接错误)。
|
|
72
|
+
*/
|
|
73
|
+
// ponytail: 仅适配器创建时探测一次;运行中主备切换不自动 failover,需要时在 pool error 事件里重建池再探测
|
|
74
|
+
export async function pickHost(driver, base, hosts, masterOnly) {
|
|
75
|
+
if (!driver.Client)
|
|
76
|
+
return undefined;
|
|
77
|
+
for (const h of hosts) {
|
|
78
|
+
// urlUser/urlPassword 是调用方传入的内部凭据约定键,转成驱动的 user/password 后剔除
|
|
79
|
+
const { urlUser, urlPassword, ...rest } = base;
|
|
80
|
+
const cfg = { ...rest, connectionString: undefined, host: h.host };
|
|
81
|
+
if (h.port != null)
|
|
82
|
+
cfg.port = h.port;
|
|
83
|
+
// 凭据自 base.urlUser/base.urlPassword 解出;无则不设
|
|
84
|
+
if (typeof urlUser === 'string' && urlUser !== '')
|
|
85
|
+
cfg.user = urlUser;
|
|
86
|
+
if (typeof urlPassword === 'string')
|
|
87
|
+
cfg.password = urlPassword;
|
|
88
|
+
const client = new driver.Client(cfg);
|
|
89
|
+
try {
|
|
90
|
+
await client.connect();
|
|
91
|
+
if (masterOnly) {
|
|
92
|
+
const res = await client.query('SELECT pg_is_in_recovery() AS in_recovery');
|
|
93
|
+
if (res.rows[0]?.in_recovery === true) {
|
|
94
|
+
await client.end().catch(() => { });
|
|
95
|
+
continue; // 备节点跳过
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
await client.end().catch(() => { });
|
|
99
|
+
return h.port != null ? { host: h.host, port: h.port } : { host: h.host };
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
await client.end().catch(() => { });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
32
107
|
export async function createPgLikeAdapter(kind, Driver, conn, opts) {
|
|
33
|
-
|
|
108
|
+
// GaussDB JDBC 风格 URL 驱动自带的 WHATWG 解析处理不了(jdbc: 非特殊 scheme 使
|
|
109
|
+
// hostname/port 为空 → 兜底连本机;多主机 authority 端口段含逗号 → 抛 Invalid URL),
|
|
110
|
+
// 故建池前先规范化,多主机时逐台探测选可用节点;postgresql 多主机列表剥 jdbc: 后
|
|
111
|
+
// 原样透传(pg 驱动原生支持 libpq 多主机 failover),不 probe。
|
|
112
|
+
const norm = conn.url ? normalizeUrl(conn.url) : null;
|
|
113
|
+
const joinSeg = (h) => `${h.host}${h.port != null ? ':' + h.port : ''}`;
|
|
114
|
+
const cleanUrl = norm ? norm.prefix + (norm.hosts ?? []).map(joinSeg).join(',') + norm.suffix : undefined;
|
|
115
|
+
let finalUrl = cleanUrl;
|
|
116
|
+
if (kind === 'gaussdb' && norm?.hosts && norm.hosts.length > 1) {
|
|
117
|
+
const { prefix, suffix } = norm;
|
|
118
|
+
const joinHost = (h) => prefix + joinSeg(h) + suffix;
|
|
119
|
+
const firstUrl = joinHost(norm.hosts[0]);
|
|
120
|
+
// targetServerType=master(键值大小写不敏感)→ 只接受主节点
|
|
121
|
+
const qi = suffix.indexOf('?');
|
|
122
|
+
let masterOnly = false;
|
|
123
|
+
for (const [k, v] of new URLSearchParams(qi >= 0 ? suffix.slice(qi + 1) : '')) {
|
|
124
|
+
if (k.toLowerCase() === 'targetservertype' && v.toLowerCase() === 'master')
|
|
125
|
+
masterOnly = true;
|
|
126
|
+
}
|
|
127
|
+
const base = poolConfig(conn, firstUrl);
|
|
128
|
+
// 探测凭据自 prefix userinfo 解出(按最后一个 @ / 最后一个 : 分界,decode 与
|
|
129
|
+
// percent-encode 注入互逆);仅密码无用户名时 user 不设
|
|
130
|
+
if (prefix.endsWith('@')) {
|
|
131
|
+
const body = prefix.slice(prefix.indexOf('://') + 3, -1);
|
|
132
|
+
const ci = body.lastIndexOf(':');
|
|
133
|
+
if (ci >= 0) {
|
|
134
|
+
const user = decodeURIComponent(body.slice(0, ci));
|
|
135
|
+
if (user !== '')
|
|
136
|
+
base.urlUser = user;
|
|
137
|
+
base.urlPassword = decodeURIComponent(body.slice(ci + 1));
|
|
138
|
+
}
|
|
139
|
+
else if (body !== '') {
|
|
140
|
+
base.urlUser = decodeURIComponent(body);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const picked = await pickHost(Driver, base, norm.hosts, masterOnly);
|
|
144
|
+
// 全败回退首台重组值:让 testConnect 报真实连接错误
|
|
145
|
+
finalUrl = picked ? joinHost(picked) : firstUrl;
|
|
146
|
+
}
|
|
147
|
+
const cfg = poolConfig(conn, finalUrl);
|
|
148
|
+
const pool = new Driver.Pool(cfg);
|
|
34
149
|
const readOnly = opts?.mode === 'ro';
|
|
35
150
|
// 所有池(主池 + 跨库池)统一走此初始化。
|
|
36
151
|
// pg 约定:release(err) 若无等待者会 emit pool 'error',无监听器会崩进程——
|
|
@@ -58,10 +173,11 @@ export async function createPgLikeAdapter(kind, Driver, conn, opts) {
|
|
|
58
173
|
const mainDb = () => {
|
|
59
174
|
if (conn.fields)
|
|
60
175
|
return conn.fields.database ? String(conn.fields.database) : null;
|
|
61
|
-
if (!
|
|
176
|
+
if (!finalUrl)
|
|
62
177
|
return null;
|
|
63
178
|
try {
|
|
64
|
-
|
|
179
|
+
// 规范化后的最终 URL(单台 authority)才可被 new URL 正确解析
|
|
180
|
+
return decodeURIComponent(new URL(finalUrl).pathname.replace(/^\//, '')) || null;
|
|
65
181
|
}
|
|
66
182
|
catch {
|
|
67
183
|
return null;
|
|
@@ -73,7 +189,8 @@ export async function createPgLikeAdapter(kind, Driver, conn, opts) {
|
|
|
73
189
|
const cached = dbPools.get(db);
|
|
74
190
|
if (cached)
|
|
75
191
|
return cached;
|
|
76
|
-
|
|
192
|
+
// 与主池同一份最终 cfg(多主机探测选定台后的 connectionString),仅覆盖库名
|
|
193
|
+
const created = new Driver.Pool({ ...cfg, database: db });
|
|
77
194
|
setupPool(created); // 与主池一致:error 兜底 + ro 会话只读
|
|
78
195
|
dbPools.set(db, created);
|
|
79
196
|
return created;
|
|
@@ -10,11 +10,16 @@ import * as path from 'node:path';
|
|
|
10
10
|
import { readJson, writeJsonAtomic } from './io.js';
|
|
11
11
|
/** fields 中被视为机密的键,落盘前拆出到 secrets.json */
|
|
12
12
|
const PASSWORD_KEY = 'password';
|
|
13
|
+
/** authority 段定位:scheme:// 到首个 /?#(兼容可选 jdbc: 前缀,如 jdbc:gaussdb://),
|
|
14
|
+
* 捕获组 1 为 scheme:// 部分、组 2 为 authority(多主机逗号串整体落在该段内)。
|
|
15
|
+
* redactUrl 与 mergeUrlCredentials 的字符串回退注入共用。 */
|
|
16
|
+
const AUTHORITY_RE = /^((?:jdbc:)?[a-z][a-z0-9+.-]*:\/\/)([^/?#]*)/i;
|
|
13
17
|
/** 将 URL 中的密码段替换为 ***:scheme://user:pass@host → scheme://user:***@host。
|
|
14
18
|
* 密码可能包含 / ? # @ 等字符:先取 scheme:// 到首个 /?# 的 authority 段,
|
|
15
|
-
* 在 authority 内以最后一个 @ 分界取 userinfo,再以最后一个 : 分界取密码。
|
|
19
|
+
* 在 authority 内以最后一个 @ 分界取 userinfo,再以最后一个 : 分界取密码。
|
|
20
|
+
* scheme 兼容可选 jdbc: 前缀(jdbc:gaussdb://…),无前缀行为不变。 */
|
|
16
21
|
export function redactUrl(url) {
|
|
17
|
-
const m = url.match(
|
|
22
|
+
const m = url.match(AUTHORITY_RE);
|
|
18
23
|
if (!m)
|
|
19
24
|
return url;
|
|
20
25
|
const authority = m[2] ?? '';
|
|
@@ -34,7 +39,12 @@ export function redactUrl(url) {
|
|
|
34
39
|
* - url 含脱敏标记 ':***@'(编辑回填值)→ 有 secretsUrl 以其为基底整体替换,否则报错;
|
|
35
40
|
* - 注入用 WHATWG URL 的 username/password setter(自动 percent-encode '@:/?# ' 等特殊字符),
|
|
36
41
|
* 与适配器侧 decodeURIComponent / 驱动 RFC 3986 解码严格互逆;只填一项只覆盖一项
|
|
37
|
-
* (redis 'redis://:pass@host' 无用户名形态、oracle 必填用户名校验均兼容)。
|
|
42
|
+
* (redis 'redis://:pass@host' 无用户名形态、oracle 必填用户名校验均兼容)。
|
|
43
|
+
* - WHATWG 解析失败(如多主机逗号端口 gaussdb://h1:8000,h2:8000/…)或 setter 静默无效
|
|
44
|
+
* (jdbc:gaussdb://… 为 opaque path、host 为空)时回退字符串注入:正则定位 authority 段
|
|
45
|
+
* (兼容可选 jdbc: 前缀),段内以最后一个 @ 为界替换/插入 userinfo,其余部分逐字保留;
|
|
46
|
+
* 凭据按 encodeURIComponent 编码,只填一项只覆盖一项的语义与 setter 对齐;
|
|
47
|
+
* 无 scheme://authority 形态的残缺串仍报「URL 无效」。 */
|
|
38
48
|
export function mergeUrlCredentials(opts) {
|
|
39
49
|
const { url, urlUser, urlPassword, secretsUrl } = opts;
|
|
40
50
|
if (urlUser === undefined && urlPassword === undefined)
|
|
@@ -59,7 +69,15 @@ export function mergeUrlCredentials(opts) {
|
|
|
59
69
|
u = new URL(base);
|
|
60
70
|
}
|
|
61
71
|
catch {
|
|
62
|
-
|
|
72
|
+
u = undefined; // WHATWG 解析失败:多主机逗号端口、残缺串等
|
|
73
|
+
}
|
|
74
|
+
const m = base.match(AUTHORITY_RE);
|
|
75
|
+
// 回退条件:解析失败;或 opaque path(host 为空、setter no-op)且 authority 非空。
|
|
76
|
+
// authority 为空(sqlite:// 等无主机形态)不回退,维持 setter no-op 的现状语义。
|
|
77
|
+
if (u === undefined || (u.host === '' && (m?.[2] ?? '') !== '')) {
|
|
78
|
+
if (m === null)
|
|
79
|
+
throw new Error('连接 URL 无效,无法注入用户名/密码');
|
|
80
|
+
return injectUserInfo(base, m, urlUser, urlPassword);
|
|
63
81
|
}
|
|
64
82
|
if (urlUser !== undefined && urlUser !== '')
|
|
65
83
|
u.username = urlUser;
|
|
@@ -67,6 +85,35 @@ export function mergeUrlCredentials(opts) {
|
|
|
67
85
|
u.password = urlPassword;
|
|
68
86
|
return u.toString();
|
|
69
87
|
}
|
|
88
|
+
/** 字符串回退注入:在 authority(m[2])内以最后一个 @ 为界替换/插入 userinfo。
|
|
89
|
+
* 与 WHATWG setter 语义对齐:只填一项只覆盖一项(另一项保留基底值);
|
|
90
|
+
* 两者均无效时不改动原样返回。凭据按 encodeURIComponent 编码(与适配器侧
|
|
91
|
+
* decodeURIComponent 互逆),authority 之外(path/query/fragment)逐字保留。 */
|
|
92
|
+
function injectUserInfo(base, m, urlUser, urlPassword) {
|
|
93
|
+
const whole = m[0] ?? '';
|
|
94
|
+
const authority = m[2] ?? '';
|
|
95
|
+
const user = urlUser !== undefined && urlUser !== '' ? encodeURIComponent(urlUser) : undefined;
|
|
96
|
+
const pass = urlPassword !== undefined && urlPassword !== '' ? encodeURIComponent(urlPassword) : undefined;
|
|
97
|
+
if (user === undefined && pass === undefined)
|
|
98
|
+
return base;
|
|
99
|
+
const at = authority.lastIndexOf('@');
|
|
100
|
+
const oldUserinfo = at >= 0 ? authority.slice(0, at) : '';
|
|
101
|
+
const colon = oldUserinfo.lastIndexOf(':');
|
|
102
|
+
const oldUser = colon >= 0 ? oldUserinfo.slice(0, colon) : oldUserinfo;
|
|
103
|
+
const oldPass = colon >= 0 ? oldUserinfo.slice(colon + 1) : undefined;
|
|
104
|
+
let userinfo;
|
|
105
|
+
if (user !== undefined && pass !== undefined) {
|
|
106
|
+
userinfo = `${user}:${pass}`;
|
|
107
|
+
}
|
|
108
|
+
else if (user !== undefined) {
|
|
109
|
+
userinfo = oldPass !== undefined ? `${user}:${oldPass}` : user;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
userinfo = `${oldUser}:${pass}`;
|
|
113
|
+
}
|
|
114
|
+
const hostPart = at >= 0 ? authority.slice(at + 1) : authority;
|
|
115
|
+
return whole.slice(0, whole.length - authority.length) + userinfo + '@' + hostPart + base.slice(whole.length);
|
|
116
|
+
}
|
|
70
117
|
function splitPassword(fields) {
|
|
71
118
|
if (!fields || typeof fields[PASSWORD_KEY] !== 'string') {
|
|
72
119
|
return { clean: fields, password: undefined };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-db-tool",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.7",
|
|
4
4
|
"description": "DSH community plugin: chat-operated multi-database admin tool with sidebar management, project-scoped grants, ro/rw modes and dangerous-operation confirmation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,9 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"client": {
|
|
40
40
|
"inject": [
|
|
41
|
-
"@deepseek-ai/dsh-client-locale"
|
|
42
|
-
"react-dom",
|
|
43
|
-
"react-dom/client"
|
|
41
|
+
"@deepseek-ai/dsh-client-locale"
|
|
44
42
|
],
|
|
45
43
|
"platform": "web"
|
|
46
44
|
}
|