dsh-db-tool 1.5.4 → 1.5.6
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 +86 -319
- package/dist/adapters/sql-shared/pg-like.js +123 -6
- package/dist/store/connections.js +51 -4
- package/package.json +2 -3
package/client/client.js
CHANGED
|
@@ -92,8 +92,6 @@ window.__ModuleLoader__.load({
|
|
|
92
92
|
emptyStructure: "暂无字段信息",
|
|
93
93
|
emptyData: "暂无数据",
|
|
94
94
|
retry: "重试",
|
|
95
|
-
browsePanelHint: "数据浏览已在浮动窗口打开,可继续操作其他功能。",
|
|
96
|
-
reopenBrowse: "打开数据浏览",
|
|
97
95
|
selectTableHint: "在左侧选择表后查看数据",
|
|
98
96
|
// 单元格查看/编辑(Navicat 式就地写回)
|
|
99
97
|
editCell: "编辑",
|
|
@@ -107,7 +105,6 @@ window.__ModuleLoader__.load({
|
|
|
107
105
|
truncatedNoEdit: "值已截断显示,就地编辑可能覆盖数据,请改用 SQL 控制台",
|
|
108
106
|
blobNoEdit: "二进制值不支持就地编辑",
|
|
109
107
|
redisTypeRo: "Redis 该类型不支持就地编辑",
|
|
110
|
-
cellSaved: "已保存",
|
|
111
108
|
// SQL 控制台
|
|
112
109
|
modeQuery: "只读查询",
|
|
113
110
|
modeExecute: "写入执行",
|
|
@@ -202,8 +199,6 @@ window.__ModuleLoader__.load({
|
|
|
202
199
|
emptyStructure: "No columns",
|
|
203
200
|
emptyData: "No rows",
|
|
204
201
|
retry: "Retry",
|
|
205
|
-
browsePanelHint: "Browse is open in a floating window; you can keep working here.",
|
|
206
|
-
reopenBrowse: "Open Data Browser",
|
|
207
202
|
selectTableHint: "Select a table on the left to view data",
|
|
208
203
|
// Cell view/edit (Navicat-style in-place write-back)
|
|
209
204
|
editCell: "Edit",
|
|
@@ -217,7 +212,6 @@ window.__ModuleLoader__.load({
|
|
|
217
212
|
truncatedNoEdit: "Value shown truncated; edit via SQL Console instead",
|
|
218
213
|
blobNoEdit: "Binary values cannot be edited here",
|
|
219
214
|
redisTypeRo: "This Redis type cannot be edited here",
|
|
220
|
-
cellSaved: "Saved",
|
|
221
215
|
modeQuery: "query (read-only)",
|
|
222
216
|
modeExecute: "execute (write)",
|
|
223
217
|
modeScript: "script",
|
|
@@ -324,22 +318,25 @@ window.__ModuleLoader__.load({
|
|
|
324
318
|
if (styleEl) return;
|
|
325
319
|
styleEl = document.createElement("style");
|
|
326
320
|
styleEl.textContent = [
|
|
327
|
-
"/* ===== Apple-style Design Tokens(dark 基线,light 经 media query 反转;.dbt-browse-root
|
|
321
|
+
"/* ===== Apple-style Design Tokens(dark 基线,light 经 media query 反转;.dbt-browse-root 并入以承载内嵌浏览面板与单元格浮层) ===== */",
|
|
328
322
|
".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;}",
|
|
329
323
|
"@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);}}",
|
|
330
|
-
//
|
|
324
|
+
// 浏览面板根自持文字色:PiP 独立文档挂 body 后无侧栏祖先 color 可继承(内嵌/浮窗两态共用)
|
|
331
325
|
".dbt-browse-root{color:var(--dbt-text);}",
|
|
332
326
|
"/* ===== 根容器 ===== */",
|
|
333
|
-
".dbt-panel{font-size:13px;line-height:1.45;display:flex;flex-direction:column;gap:
|
|
327
|
+
".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;}",
|
|
334
328
|
"/* ===== iOS segmented control(顶部 tab) ===== */",
|
|
335
329
|
".dbt-tabs{display:flex;gap:2px;background:var(--dbt-surface);border-radius:var(--dbt-radius-ctrl);padding:2px;}",
|
|
336
330
|
".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;}",
|
|
337
331
|
".dbt-tabs button.active{background:var(--dbt-seg-active);font-weight:600;box-shadow:0 1px 3px rgba(0,0,0,.12);}",
|
|
338
332
|
"/* ===== inset grouped 卡片 ===== */",
|
|
339
|
-
".dbt-card{background:var(--dbt-surface);border:none;border-radius:var(--dbt-radius-card);padding:12px;display:flex;flex-direction:column;gap:
|
|
333
|
+
".dbt-card{background:var(--dbt-surface);border:none;border-radius:var(--dbt-radius-card);padding:12px;display:flex;flex-direction:column;gap:8px;}",
|
|
340
334
|
".dbt-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}",
|
|
335
|
+
"/* ===== 共享列布局(T1 逐步替换各视图内联 flex-column) ===== */",
|
|
336
|
+
".dbt-col{display:flex;flex-direction:column;gap:8px;}",
|
|
337
|
+
".dbt-col.dbt-col-tight{gap:2px;}",
|
|
341
338
|
"/* ===== 填充式无边框输入 ===== */",
|
|
342
|
-
".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:
|
|
339
|
+
".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);}",
|
|
343
340
|
".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;}",
|
|
344
341
|
".dbt-card textarea{font-family:var(--dbt-mono);min-height:96px;resize:vertical;}",
|
|
345
342
|
"/* ===== 按钮 ===== */",
|
|
@@ -352,24 +349,25 @@ window.__ModuleLoader__.load({
|
|
|
352
349
|
".dbt-btn.danger:hover{background:rgba(255,69,58,.12);}",
|
|
353
350
|
".dbt-btn:disabled{opacity:.4;cursor:default;transform:none;}",
|
|
354
351
|
"/* ===== 文本反馈 ===== */",
|
|
355
|
-
".dbt-
|
|
352
|
+
".dbt-panel strong{font-size:13px;font-weight:600;}",
|
|
353
|
+
".dbt-muted,.dbt-readhint{color:var(--dbt-text-secondary);font-size:11px;}",
|
|
356
354
|
".dbt-err{color:var(--dbt-danger);font-size:12px;white-space:pre-wrap;}",
|
|
357
355
|
".dbt-msg{color:var(--dbt-success);font-size:12px;white-space:pre-wrap;}",
|
|
358
356
|
"/* ===== 行 hairline 表格 + 毛玻璃 sticky 表头 ===== */",
|
|
359
357
|
".dbt-table{width:100%;border-collapse:collapse;font-size:12px;}",
|
|
360
|
-
".dbt-table th,.dbt-table td{border:none;border-bottom:1px solid var(--dbt-separator);padding:
|
|
361
|
-
".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:
|
|
358
|
+
".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;}",
|
|
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:12px;}",
|
|
362
360
|
".dbt-table td{font-family:var(--dbt-mono);}",
|
|
363
361
|
".dbt-tablewrap{border:none;border-radius:var(--dbt-radius-card);overflow:auto;max-height:320px;background:var(--dbt-surface);}",
|
|
364
|
-
"/* =====
|
|
362
|
+
"/* ===== 浏览面板表格:列分明(列间 hairline + 表头加深一档 + 行 hover) ===== */",
|
|
365
363
|
".dbt-browse-root .dbt-table th + th,.dbt-browse-root .dbt-table td + td{border-left:1px solid var(--dbt-separator);}",
|
|
366
|
-
".dbt-browse-root .dbt-table th{background:rgba(30,30,32,.85);
|
|
367
|
-
".dbt-browse-root .dbt-table td{font-size:
|
|
364
|
+
".dbt-browse-root .dbt-table th{background:rgba(30,30,32,.85);}",
|
|
365
|
+
".dbt-browse-root .dbt-table td{font-size:12px;}",
|
|
368
366
|
".dbt-browse-root .dbt-table tbody tr:hover td{background:var(--dbt-surface-strong);}",
|
|
369
367
|
"@media (prefers-color-scheme: light){.dbt-browse-root .dbt-table th{background:rgba(242,242,247,.9);}}",
|
|
370
368
|
"/* ===== 对话框(macOS alert 材质) ===== */",
|
|
371
369
|
".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;}",
|
|
372
|
-
".dbt-dialog{background:var(--dbt-dialog-bg);color:inherit;border:1px solid var(--dbt-separator);border-radius:
|
|
370
|
+
".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);}",
|
|
373
371
|
".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;}",
|
|
374
372
|
".dbt-dialog .dbt-row{justify-content:flex-end;}",
|
|
375
373
|
"/* ===== 授权/连接列表:inset grouped(一张卡多行 hairline) ===== */",
|
|
@@ -379,7 +377,7 @@ window.__ModuleLoader__.load({
|
|
|
379
377
|
".dbt-listrow:hover{background:var(--dbt-surface-strong);}",
|
|
380
378
|
".dbt-tree{display:flex;flex-direction:column;}",
|
|
381
379
|
"/* ===== Navicat 式对象树(数据浏览) ===== */",
|
|
382
|
-
".dbt-treerow{display:flex;gap:6px;align-items:center;padding:
|
|
380
|
+
".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);}",
|
|
383
381
|
".dbt-treerow:last-child{border-bottom:none;}",
|
|
384
382
|
".dbt-treerow:hover{background:var(--dbt-surface-strong);}",
|
|
385
383
|
".dbt-treerow.active{color:var(--dbt-accent);font-weight:600;}",
|
|
@@ -387,48 +385,39 @@ window.__ModuleLoader__.load({
|
|
|
387
385
|
".dbt-chev.open{transform:rotate(90deg);}",
|
|
388
386
|
".dbt-chev.leaf{visibility:hidden;}",
|
|
389
387
|
".dbt-treename{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
390
|
-
/* ===== Navicat
|
|
391
|
-
".dbt-browse-
|
|
392
|
-
".dbt-browse-
|
|
393
|
-
".dbt-browse-
|
|
394
|
-
".dbt-browse-
|
|
395
|
-
".dbt-browse-
|
|
396
|
-
".dbt-browse-
|
|
397
|
-
".dbt-browse-
|
|
398
|
-
".dbt-browse-
|
|
399
|
-
".dbt-browse-
|
|
400
|
-
".dbt-browse-main{flex:1;min-width:0;display:flex;flex-direction:column;}",
|
|
401
|
-
".dbt-browse-tabs{flex:none;padding:10px 14px 0;}",
|
|
402
|
-
".dbt-browse-content{flex:1;min-height:0;position:relative;display:flex;flex-direction:column;gap:10px;padding:12px 14px;overflow:hidden;}",
|
|
403
|
-
".dbt-browse-tablewrap{flex:1;min-height:0;overflow:auto;border:none;border-radius:var(--dbt-radius-card);background:var(--dbt-surface);}",
|
|
404
|
-
".dbt-browse-footer{flex:none;display:flex;align-items:center;gap:8px;padding:10px 14px;border-top:1px solid var(--dbt-separator);}",
|
|
405
|
-
".dbt-browse-empty{flex:1;display:flex;align-items:center;justify-content:center;color:var(--dbt-text-secondary);font-size:13px;}",
|
|
406
|
-
".dbt-browse-errorbox{flex:1;display:flex;flex-direction:column;gap:10px;align-items:flex-start;justify-content:center;}",
|
|
388
|
+
/* ===== Navicat 式浏览(内嵌面板,纵向流式;浮窗壳类已随 DOM 回收) ===== */
|
|
389
|
+
".dbt-browse-body{display:flex;flex-direction:column;gap:10px;min-height:0;}",
|
|
390
|
+
".dbt-browse-tree{flex:none;max-height:280px;overflow-y:auto;border-bottom:1px solid var(--dbt-separator);}",
|
|
391
|
+
".dbt-browse-main{flex:1;min-height:0;display:flex;flex-direction:column;gap:10px;}",
|
|
392
|
+
".dbt-browse-tabs{flex:none;}",
|
|
393
|
+
".dbt-browse-content{flex:1;min-height:0;position:relative;display:flex;flex-direction:column;gap:10px;padding:12px;}",
|
|
394
|
+
".dbt-browse-tablewrap{flex:none;max-height:320px;overflow:auto;border:none;border-radius:var(--dbt-radius-card);background:var(--dbt-surface);}",
|
|
395
|
+
".dbt-browse-footer{flex:none;display:flex;align-items:center;gap:8px;padding:10px 12px;border-top:1px solid var(--dbt-separator);}",
|
|
396
|
+
".dbt-browse-empty{display:flex;align-items:center;justify-content:center;color:var(--dbt-text-secondary);font-size:13px;padding:24px 12px;}",
|
|
397
|
+
".dbt-browse-errorbox{display:flex;flex-direction:column;gap:8px;align-items:flex-start;}",
|
|
407
398
|
".dbt-browse-skel{display:flex;flex-direction:column;gap:8px;}",
|
|
408
399
|
".dbt-browse-skelrow{display:flex;gap:8px;}",
|
|
409
400
|
".dbt-browse-skelcell{flex:1;height:24px;background:var(--dbt-surface-strong);border-radius:var(--dbt-radius-pill);}",
|
|
410
|
-
"/* =====
|
|
401
|
+
"/* ===== 浏览面板:单元格查看/编辑浮层(定位锚点为 .dbt-browse-content) ===== */",
|
|
411
402
|
".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;}",
|
|
412
403
|
".dbt-cellbtn:focus-visible{outline:2px solid var(--dbt-accent);outline-offset:-1px;border-radius:2px;}",
|
|
413
404
|
".dbt-cellpop{position:absolute;z-index:20;width:min(460px,100%);background:var(--dbt-dialog-bg);border:1px solid var(--dbt-separator);border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.3);-webkit-backdrop-filter:blur(20px) saturate(180%);backdrop-filter:blur(20px) saturate(180%);padding:12px;display:flex;flex-direction:column;gap:8px;box-sizing:border-box;font-size:13px;}",
|
|
414
|
-
".dbt-cellpop-value{font-family:var(--dbt-mono);font-size:
|
|
405
|
+
".dbt-cellpop-value{font-family:var(--dbt-mono);font-size:12px;white-space:pre-wrap;word-break:break-all;max-height:240px;overflow:auto;background:var(--dbt-surface-strong);border-radius:var(--dbt-radius-ctrl);padding:8px;}",
|
|
415
406
|
".dbt-cellpop-actions{display:flex;gap:8px;align-items:center;}",
|
|
416
407
|
".dbt-celledit{display:flex;flex-direction:column;gap:8px;}",
|
|
417
|
-
".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:
|
|
408
|
+
".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;}",
|
|
418
409
|
".dbt-celledit textarea:focus{outline:2px solid var(--dbt-accent);outline-offset:-1px;}",
|
|
419
410
|
".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;}",
|
|
420
|
-
".dbt-readhint{color:var(--dbt-text-secondary);font-size:11px;}",
|
|
421
411
|
"/* ===== 视图切换入场(仅 transform/opacity;reduced-motion 全局已禁) ===== */",
|
|
422
412
|
"@keyframes dbt-in{from{opacity:0;transform:translateY(4px);}to{opacity:1;transform:none;}}",
|
|
423
413
|
".dbt-view{display:flex;flex-direction:column;gap:10px;animation:dbt-in .22s var(--dbt-ease);}",
|
|
424
414
|
"/* ===== mini segmented(ro/rw 等切换) ===== */",
|
|
425
415
|
".dbt-seg{display:flex;gap:2px;background:var(--dbt-surface);border-radius:var(--dbt-radius-ctrl);padding:2px;}",
|
|
426
|
-
".dbt-seg button{flex:none;border:none;background:transparent;color:inherit;font-size:
|
|
416
|
+
".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);}",
|
|
427
417
|
".dbt-seg button.active{background:var(--dbt-seg-active);font-weight:600;box-shadow:0 1px 3px rgba(0,0,0,.12);}",
|
|
428
|
-
".dbt-browse-root .dbt-seg button{font-size:12px;padding:4px 12px;}",
|
|
429
418
|
"/* ===== 降级:透明度减弱 / 动效减弱 ===== */",
|
|
430
|
-
"@media (prefers-reduced-transparency: reduce){.dbt-overlay{backdrop-filter:none;-webkit-backdrop-filter:none;background:rgba(0,0,0,.55);}.dbt-dialog,.dbt-
|
|
431
|
-
"@media (prefers-color-scheme: light) and (prefers-reduced-transparency: reduce){.dbt-dialog,.dbt-
|
|
419
|
+
"@media (prefers-reduced-transparency: reduce){.dbt-overlay{backdrop-filter:none;-webkit-backdrop-filter:none;background:rgba(0,0,0,.55);}.dbt-dialog,.dbt-cellpop{backdrop-filter:none;-webkit-backdrop-filter:none;background:#2c2c2e;}.dbt-table th{backdrop-filter:none;-webkit-backdrop-filter:none;background:#1e1e20;}.dbt-browse-root .dbt-table th{background:#1e1e20;}}",
|
|
420
|
+
"@media (prefers-color-scheme: light) and (prefers-reduced-transparency: reduce){.dbt-dialog,.dbt-cellpop{background:#f5f5f7;}.dbt-table th,.dbt-browse-root .dbt-table th{background:#f2f2f7;}}",
|
|
432
421
|
"@media (prefers-reduced-motion: reduce){.dbt-panel *,.dbt-browse-root *{transition:none!important;animation:none!important;}.dbt-btn:active{transform:none;}}",
|
|
433
422
|
].join("\n");
|
|
434
423
|
document.head.appendChild(styleEl);
|
|
@@ -454,7 +443,8 @@ window.__ModuleLoader__.load({
|
|
|
454
443
|
React.createElement("div", { className: "dbt-muted" }, t("confirmHint")),
|
|
455
444
|
React.createElement(
|
|
456
445
|
"div",
|
|
457
|
-
|
|
446
|
+
// 末对齐由样式层 .dbt-dialog .dbt-row 规则承担,无需内联 style
|
|
447
|
+
{ className: "dbt-row" },
|
|
458
448
|
React.createElement("button", { className: "dbt-btn danger", onClick: () => done(true) }, t("confirmRun")),
|
|
459
449
|
React.createElement("button", { className: "dbt-btn", onClick: () => done(false) }, t("confirmCancel")),
|
|
460
450
|
),
|
|
@@ -462,51 +452,6 @@ window.__ModuleLoader__.load({
|
|
|
462
452
|
);
|
|
463
453
|
}
|
|
464
454
|
|
|
465
|
-
// --- 弹窗挂载:优先 createPortal 挂 document.body(避免侧栏容器裁剪);宿主未提供 react-dom 时回退 Panel 内 fixed overlay(DangerDialog 同模式) ---
|
|
466
|
-
// 三重降级解析(模块级缓存,全模块一次):①模块加载时同步 require(dsh.client.inject 声明 react-dom 后应成功)
|
|
467
|
-
// ②宿主模块系统异步 import(ctx.modules,宿主 rc.8+ 提供)③均不可用则保持 Panel 内回退渲染。
|
|
468
|
-
let portalImpl = null; // 解析成功的 createPortal(缓存,全模块一次)
|
|
469
|
-
let portalTried = false; // 是否已尝试解析(异步 import 只发起一次)
|
|
470
|
-
let portalPending = null; // 订阅解析成功的回调列表
|
|
471
|
-
try {
|
|
472
|
-
var rd = require("react-dom");
|
|
473
|
-
if (rd && typeof rd.createPortal === "function") { portalImpl = rd.createPortal.bind(rd); portalTried = true; }
|
|
474
|
-
} catch (e) { /* 留 null,等异步解析 */ }
|
|
475
|
-
function resolvePortalAsync(ctx) {
|
|
476
|
-
if (portalTried) return;
|
|
477
|
-
portalTried = true;
|
|
478
|
-
if (portalImpl) return;
|
|
479
|
-
var mods = ctx && ctx.modules;
|
|
480
|
-
var p = mods && typeof mods.import === "function" ? mods.import("react-dom") : null;
|
|
481
|
-
if (p && typeof p.then === "function") {
|
|
482
|
-
p.then(function (m) {
|
|
483
|
-
if (m && typeof m.createPortal === "function") {
|
|
484
|
-
portalImpl = m.createPortal.bind(m);
|
|
485
|
-
var waiters = portalPending || []; portalPending = null;
|
|
486
|
-
for (var i = 0; i < waiters.length; i++) waiters[i]();
|
|
487
|
-
}
|
|
488
|
-
}).catch(function () { /* 解析失败保持回退渲染 */ });
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
function onPortalReady(cb) {
|
|
492
|
-
if (portalImpl) { cb(); return; }
|
|
493
|
-
(portalPending = portalPending || []).push(cb);
|
|
494
|
-
}
|
|
495
|
-
function mountDialog(children) {
|
|
496
|
-
return (portalImpl && !portalDisabled) ? portalImpl(children, document.body) : children;
|
|
497
|
-
}
|
|
498
|
-
// PiP 独立窗口渲染时禁 portal:PiP root 本身就挂在 pipDoc.body 上,再 portal 回主 document 会把 DOM 拉出独立窗口
|
|
499
|
-
let portalDisabled = false;
|
|
500
|
-
// createRoot 多级探测(PiP 独立窗口需要独立 React root):react-dom → react-dom/client
|
|
501
|
-
let reactCreateRoot = null;
|
|
502
|
-
try {
|
|
503
|
-
var rd2 = require("react-dom");
|
|
504
|
-
reactCreateRoot = rd2 && typeof rd2.createRoot === "function" ? rd2.createRoot.bind(rd2) : null;
|
|
505
|
-
if (!reactCreateRoot) {
|
|
506
|
-
try { var rdc = require("react-dom/client"); reactCreateRoot = rdc && typeof rdc.createRoot === "function" ? rdc.createRoot.bind(rdc) : null; } catch (e2) { /* 保持 null */ }
|
|
507
|
-
}
|
|
508
|
-
} catch (e) { /* 保持 null,PiP 不可用走 DOM 浮窗降级 */ }
|
|
509
|
-
|
|
510
455
|
/* ---------------- 连接管理 ---------------- */
|
|
511
456
|
const EMPTY_FORM = { id: "", kind: "mysql", name: "", mode: "url", url: "", urlUser: "", urlPassword: "", host: "", port: "", user: "", password: "", database: "", ssl: false };
|
|
512
457
|
|
|
@@ -840,7 +785,7 @@ window.__ModuleLoader__.load({
|
|
|
840
785
|
draftTest && draftTest.ok === null ? t("testing") : t("test")),
|
|
841
786
|
React.createElement("button", { className: "dbt-btn", onClick: props.onCancel }, t("cancel")),
|
|
842
787
|
draftTest && draftTest.ok !== null
|
|
843
|
-
? React.createElement("span", {
|
|
788
|
+
? React.createElement("span", { className: draftTest.ok ? "dbt-msg" : "dbt-err" }, draftTest.msg)
|
|
844
789
|
: null,
|
|
845
790
|
),
|
|
846
791
|
);
|
|
@@ -932,7 +877,7 @@ window.__ModuleLoader__.load({
|
|
|
932
877
|
{ className: "dbt-muted", style: { fontFamily: MONO_FONT, fontSize: 11, wordBreak: "break-all" } },
|
|
933
878
|
c.safeUrl || (c.host + ":" + (c.port || "")),
|
|
934
879
|
) : null,
|
|
935
|
-
testInfo[c.id] ? React.createElement("div", {
|
|
880
|
+
testInfo[c.id] ? React.createElement("div", { className: testInfo[c.id].startsWith("✓") ? "dbt-msg" : "dbt-err" }, testInfo[c.id]) : null,
|
|
936
881
|
),
|
|
937
882
|
// 右列:动作按钮(次要语义,danger 仅删除)
|
|
938
883
|
React.createElement(
|
|
@@ -1012,7 +957,7 @@ window.__ModuleLoader__.load({
|
|
|
1012
957
|
}
|
|
1013
958
|
|
|
1014
959
|
/* ---------------- 数据浏览 ---------------- */
|
|
1015
|
-
// Navicat 式对象树:连接 ▸ 库/[schema] ▸
|
|
960
|
+
// Navicat 式对象树:连接 ▸ 库/[schema] ▸ 表,懒加载展开(浏览面板唯一树实例,展开状态随面板生命周期)
|
|
1016
961
|
function BrowseTree(props) {
|
|
1017
962
|
const { conns, projectPath, sel, onSelect, autoExpand } = props;
|
|
1018
963
|
// sel 由父组件持有(弹窗内 active 高亮与初始选中都依赖);onSelect(选中记录, 触发元素) 上报
|
|
@@ -1160,7 +1105,7 @@ window.__ModuleLoader__.load({
|
|
|
1160
1105
|
return React.createElement(
|
|
1161
1106
|
"div",
|
|
1162
1107
|
{ style: { display: "flex", flexDirection: "column", gap: 8 } },
|
|
1163
|
-
// Navicat
|
|
1108
|
+
// Navicat 式对象树:点击展开连接/库,点击表上报选中(由父组件就地预览)
|
|
1164
1109
|
conns.length === 0 ? React.createElement("div", { className: "dbt-muted" }, t("noConns")) :
|
|
1165
1110
|
React.createElement("div", { className: "dbt-group" },
|
|
1166
1111
|
React.createElement("div", { className: "dbt-tree" }, treeRows)),
|
|
@@ -1343,30 +1288,18 @@ window.__ModuleLoader__.load({
|
|
|
1343
1288
|
);
|
|
1344
1289
|
}
|
|
1345
1290
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
//
|
|
1350
|
-
|
|
1351
|
-
const [portalReady, setPortalReady] = React.useState(!!portalImpl);
|
|
1352
|
-
React.useEffect(() => {
|
|
1353
|
-
resolvePortalAsync(ctx);
|
|
1354
|
-
onPortalReady(function () { setPortalReady(true); });
|
|
1355
|
-
// 诊断:一次性输出(帮助真机排查 iframe 隔离与 portal 解析结果)
|
|
1356
|
-
if (!window.__dbtPortalDiag) {
|
|
1357
|
-
window.__dbtPortalDiag = true;
|
|
1358
|
-
console.info("[dbt] portal:", portalImpl ? "sync/async ok" : "unavailable", "sameDoc:", window.top === window.self);
|
|
1359
|
-
}
|
|
1360
|
-
}, []);
|
|
1361
|
-
const [sel, setSel] = React.useState(null); // 弹窗内当前选中(null=未选表,右栏显示引导空态)
|
|
1291
|
+
// Navicat 式浏览面板(内嵌侧边栏):上半对象树选库-表,下半结构/数据预览,点表即看,不再弹窗
|
|
1292
|
+
function BrowsePane(props) {
|
|
1293
|
+
const { conns, projectPath, grants, askConfirm } = props;
|
|
1294
|
+
// grants(editable 判定)与 askConfirm(单元格写回确认)下传 PreviewGrid
|
|
1295
|
+
const [sel, setSel] = React.useState(null); // 面板内当前选中(null=未选表,内容区显示引导空态)
|
|
1362
1296
|
const [view, setView] = React.useState("structure"); // structure | preview(纯视图切换,不影响数据加载)
|
|
1363
1297
|
const [schema, setSchema] = React.useState([]);
|
|
1364
1298
|
const [preview, setPreview] = React.useState(null); // QueryResult
|
|
1365
1299
|
const [page, setPage] = React.useState(1);
|
|
1366
1300
|
const [busy, setBusy] = React.useState("");
|
|
1367
|
-
const [loadErr, setLoadErr] = React.useState(""); //
|
|
1301
|
+
const [loadErr, setLoadErr] = React.useState(""); // 内容区加载失败(就地显示 + 重试)
|
|
1368
1302
|
const openSeq = React.useRef(0); // 请求序号守卫
|
|
1369
|
-
const closeRef = React.useRef(null);
|
|
1370
1303
|
|
|
1371
1304
|
// 请求序号守卫(沿用面板):快速切表/翻页时丢弃晚到旧响应,防止旧数据覆盖新选中项
|
|
1372
1305
|
const openTable = React.useCallback((s, pg) => {
|
|
@@ -1383,59 +1316,16 @@ window.__ModuleLoader__.load({
|
|
|
1383
1316
|
if (seq !== openSeq.current) return; // 旧请求晚到,丢弃
|
|
1384
1317
|
setSchema(sch || []); setPreview(prev); setPage(pg || 1);
|
|
1385
1318
|
})
|
|
1386
|
-
//
|
|
1319
|
+
// 面板内错误就地显示(不走面板顶部错误区)
|
|
1387
1320
|
.catch((e) => { if (seq === openSeq.current) setLoadErr(String(e && e.message ? e.message : e)); })
|
|
1388
1321
|
.finally(() => { if (seq === openSeq.current) setBusy(""); });
|
|
1389
1322
|
}, [projectPath]);
|
|
1390
1323
|
React.useEffect(() => { if (sel) openTable(sel, 1); }, [sel]); // eslint-disable-line
|
|
1391
1324
|
|
|
1392
|
-
//
|
|
1393
|
-
// 经容器 onKeyDown 触发(stopPropagation 防止宿主级快捷键同时响应)。初始焦点=关闭按钮
|
|
1394
|
-
React.useEffect(() => {
|
|
1395
|
-
if (closeRef.current) closeRef.current.focus();
|
|
1396
|
-
}, []);
|
|
1397
|
-
|
|
1398
|
-
// 拖动定位:首帧给个保守值,mount 后按浮窗实测尺寸居中(坐标基准取浮窗所在 document 的视口——DOM 浮窗为主视口,PiP 独立窗口为 PiP 视口)
|
|
1399
|
-
const dialogRef2 = React.useRef(null);
|
|
1400
|
-
const [pos, setPos] = React.useState(() => ({ top: 72, left: 12 }));
|
|
1401
|
-
React.useEffect(() => {
|
|
1402
|
-
const el = dialogRef2.current;
|
|
1403
|
-
if (!el) return;
|
|
1404
|
-
const view = (el.ownerDocument && el.ownerDocument.defaultView) || window;
|
|
1405
|
-
const w = view.innerWidth || 1200, h = view.innerHeight || 800;
|
|
1406
|
-
setPos({
|
|
1407
|
-
top: Math.max(0, Math.round((h - el.offsetHeight) / 2)),
|
|
1408
|
-
left: Math.max(0, Math.round((w - el.offsetWidth) / 2)),
|
|
1409
|
-
});
|
|
1410
|
-
}, []);
|
|
1411
|
-
const draggingRef = React.useRef(null); // {dx, dy}
|
|
1412
|
-
const [dragging, setDragging] = React.useState(false);
|
|
1413
|
-
function stopDrag() { draggingRef.current = null; setDragging(false); }
|
|
1414
|
-
function onHeaderPointerDown(e) {
|
|
1415
|
-
// header 内可点元素(关闭按钮)不启动拖动,否则 setPointerCapture 会把 click 重定向到 header 使按钮失效
|
|
1416
|
-
if (e.target.closest && e.target.closest("button")) return;
|
|
1417
|
-
draggingRef.current = { dx: e.clientX - pos.left, dy: e.clientY - pos.top };
|
|
1418
|
-
e.currentTarget.setPointerCapture(e.pointerId);
|
|
1419
|
-
setDragging(true);
|
|
1420
|
-
}
|
|
1421
|
-
function onHeaderPointerMove(e) {
|
|
1422
|
-
if (!draggingRef.current) return;
|
|
1423
|
-
const view = (e.currentTarget.ownerDocument && e.currentTarget.ownerDocument.defaultView) || window;
|
|
1424
|
-
const h = view.innerHeight || 800, w = view.innerWidth || 1200;
|
|
1425
|
-
// clamp 全部用浮窗实测尺寸:整体保持在视口内(窄视口下常量估宽会把窗拖丢)
|
|
1426
|
-
const dlg = e.currentTarget.parentElement;
|
|
1427
|
-
const dlgH = (dlg && dlg.offsetHeight) || 720;
|
|
1428
|
-
const dlgW = (dlg && dlg.offsetWidth) || 1100;
|
|
1429
|
-
setPos({
|
|
1430
|
-
top: Math.min(Math.max(e.clientY - draggingRef.current.dy, 0), Math.max(0, h - dlgH)),
|
|
1431
|
-
left: Math.min(Math.max(e.clientX - draggingRef.current.dx, 0), Math.max(0, w - dlgW)),
|
|
1432
|
-
});
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
// 右栏互斥状态:未选中引导 / 加载骨架 / 错误重试 / 空态 / 表格
|
|
1325
|
+
// 内容区互斥状态:未选中引导 / 加载骨架 / 错误重试 / 空态 / 表格
|
|
1436
1326
|
function rightPane() {
|
|
1437
1327
|
if (!sel && busy !== "open" && !loadErr) {
|
|
1438
|
-
//
|
|
1328
|
+
// 未选表:显示引导空态(内嵌面板可直接浏览,不强制先选表)
|
|
1439
1329
|
return React.createElement("div", { className: "dbt-browse-empty" }, t("selectTableHint"));
|
|
1440
1330
|
}
|
|
1441
1331
|
if (busy === "open") {
|
|
@@ -1445,7 +1335,7 @@ window.__ModuleLoader__.load({
|
|
|
1445
1335
|
[0, 1, 2, 3, 4, 5].map((c) => React.createElement("div", { className: "dbt-browse-skelcell", key: c })))));
|
|
1446
1336
|
}
|
|
1447
1337
|
if (loadErr) {
|
|
1448
|
-
// 错误就地显示 +
|
|
1338
|
+
// 错误就地显示 + 重试(内嵌面板保持就地呈现,不走 props.onError)
|
|
1449
1339
|
return React.createElement("div", { className: "dbt-browse-errorbox" },
|
|
1450
1340
|
React.createElement("div", { className: "dbt-err" }, t("error") + ": " + loadErr),
|
|
1451
1341
|
React.createElement("button", { className: "dbt-btn", onClick: () => openTable(sel, 1) }, t("retry")));
|
|
@@ -1477,143 +1367,49 @@ window.__ModuleLoader__.load({
|
|
|
1477
1367
|
});
|
|
1478
1368
|
}
|
|
1479
1369
|
|
|
1480
|
-
//
|
|
1481
|
-
return
|
|
1370
|
+
// 内嵌面板纵向布局:上半对象树(限高滚动,容器类名留给样式层),下半预览主区 + 分页
|
|
1371
|
+
return React.createElement(
|
|
1482
1372
|
"div",
|
|
1483
|
-
{
|
|
1484
|
-
className: "dbt-browse-root dbt-browse-dialog" + (dragging ? " dragging" : ""),
|
|
1485
|
-
ref: dialogRef2,
|
|
1486
|
-
style: { position: "fixed", top: pos.top, left: pos.left },
|
|
1487
|
-
role: "dialog", "aria-labelledby": "dbt-browse-title",
|
|
1488
|
-
onKeyDown: (e) => {
|
|
1489
|
-
if (e.key === "Escape") { e.stopPropagation(); onClose(); }
|
|
1490
|
-
},
|
|
1491
|
-
},
|
|
1492
|
-
// ---- header:面包屑 + 关闭(整条 header 可拖动)----
|
|
1373
|
+
{ className: "dbt-view" },
|
|
1493
1374
|
React.createElement(
|
|
1494
1375
|
"div",
|
|
1495
|
-
{
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
conn: (conns.find((c) => c.id === sel.connId) || {}).name || sel.connId,
|
|
1506
|
-
db: sel.schemaName ? sel.db + "." + sel.schemaName : sel.db,
|
|
1507
|
-
table: sel.table.name,
|
|
1508
|
-
})),
|
|
1509
|
-
React.createElement(
|
|
1510
|
-
"button",
|
|
1511
|
-
{ className: "dbt-browse-close", ref: closeRef, "aria-label": t("close"), onClick: onClose },
|
|
1512
|
-
React.createElement("svg", { width: 12, height: 12, viewBox: "0 0 12 12", fill: "none", "aria-hidden": "true" },
|
|
1513
|
-
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" })),
|
|
1514
|
-
),
|
|
1515
|
-
),
|
|
1516
|
-
// ---- 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)
|
|
1517
1386
|
React.createElement(
|
|
1518
1387
|
"div",
|
|
1519
|
-
{ className: "dbt-browse-
|
|
1520
|
-
React.createElement(
|
|
1521
|
-
"div",
|
|
1522
|
-
{ className: "dbt-browse-tree" },
|
|
1523
|
-
React.createElement(BrowseTree, {
|
|
1524
|
-
conns, projectPath, sel, autoExpand: true, // 本任务后弹窗树是唯一树实例,挂载即展开第一层级
|
|
1525
|
-
onSelect: (s) => setSel(s), // 弹窗内树切表
|
|
1526
|
-
}),
|
|
1527
|
-
),
|
|
1388
|
+
{ className: "dbt-browse-tabs" },
|
|
1528
1389
|
React.createElement(
|
|
1529
1390
|
"div",
|
|
1530
|
-
{ className: "dbt-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
{ className: "dbt-browse-tabs" },
|
|
1535
|
-
React.createElement(
|
|
1536
|
-
"div",
|
|
1537
|
-
{ className: "dbt-seg" },
|
|
1538
|
-
["structure", "preview"].map((v) =>
|
|
1539
|
-
React.createElement("button", { key: v, className: view === v ? "active" : "", onClick: () => setView(v) },
|
|
1540
|
-
v === "structure" ? t("structure") : t("preview"))),
|
|
1541
|
-
),
|
|
1542
|
-
),
|
|
1543
|
-
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"))),
|
|
1544
1395
|
),
|
|
1545
1396
|
),
|
|
1546
|
-
//
|
|
1547
|
-
|
|
1548
|
-
"div",
|
|
1549
|
-
{ className: "dbt-browse-footer" },
|
|
1550
|
-
React.createElement("button", { className: "dbt-btn", disabled: page <= 1 || busy === "open", onClick: () => openTable(sel, page - 1) }, "‹ " + t("prevPage")),
|
|
1551
|
-
React.createElement("span", { className: "dbt-muted" }, t("pageInfo", { page })),
|
|
1552
|
-
React.createElement("span", { style: { flex: 1 } }),
|
|
1553
|
-
preview && preview.truncated ? React.createElement("span", { className: "dbt-muted" }, t("previewTruncated")) : null,
|
|
1554
|
-
React.createElement("button", { className: "dbt-btn", disabled: (preview && preview.truncated) === false || busy === "open", onClick: () => openTable(sel, page + 1) }, t("nextPage") + " ›"),
|
|
1555
|
-
) : null,
|
|
1397
|
+
// 限高兜底:无固定高度弹窗壳后给内容区可视上限,预览表在容器内滚动、单元格浮层贴底可见
|
|
1398
|
+
React.createElement("div", { className: "dbt-browse-content", style: { maxHeight: 420 } }, rightPane()),
|
|
1556
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,
|
|
1557
1410
|
);
|
|
1558
1411
|
}
|
|
1559
1412
|
|
|
1560
|
-
/* ---------------- PiP 独立窗口(Document Picture-in-Picture) ---------------- */
|
|
1561
|
-
// OS 级独立窗口:可拖出桌面端主窗口、系统级置顶。内容为完整数据浏览 UI(同 JS 环境复用
|
|
1562
|
-
// BrowseDialog/DangerDialog 与 mock 之外的宿主 fetch 同源);DOM 浮窗作为不可用时的降级。
|
|
1563
|
-
// 打开成功返回 pipWindow,失败返回 null(调用方降级为 DOM 浮窗)。
|
|
1564
|
-
function PipBrowseApp(props) { // {ctx, conns, projectPath, grants, onClose}
|
|
1565
|
-
// 危险操作确认渲染在 PiP 窗口内(Panel 的确认对话框在主窗口,PiP 用户看不见)
|
|
1566
|
-
const [confirmReq, setConfirmReq] = React.useState(null);
|
|
1567
|
-
const askConfirm = React.useCallback((info) => new Promise((resolve) => {
|
|
1568
|
-
setConfirmReq(Object.assign({}, info, { resolve }));
|
|
1569
|
-
}), []);
|
|
1570
|
-
return React.createElement(
|
|
1571
|
-
React.Fragment,
|
|
1572
|
-
null,
|
|
1573
|
-
React.createElement(BrowseDialog, {
|
|
1574
|
-
ctx: props.ctx, conns: props.conns, projectPath: props.projectPath,
|
|
1575
|
-
grants: props.grants, askConfirm: askConfirm, onClose: props.onClose,
|
|
1576
|
-
}),
|
|
1577
|
-
confirmReq ? React.createElement(DangerDialog, { challenge: confirmReq, onClose: () => setConfirmReq(null) }) : null,
|
|
1578
|
-
);
|
|
1579
|
-
}
|
|
1580
|
-
function openBrowsePip(ctx, deps) { // deps: {conns, projectPath, grants, onClosed} → 返回 pipWindow | null
|
|
1581
|
-
try {
|
|
1582
|
-
const dpip = window.documentPictureInPicture;
|
|
1583
|
-
if (!dpip || typeof dpip.requestWindow !== "function" || !reactCreateRoot || !portalImpl) return null;
|
|
1584
|
-
// PiP 只能在用户手势内同步发起 requestWindow,Promise then 里再做 DOM 装配
|
|
1585
|
-
return dpip.requestWindow({ width: 1100, height: 720 }).then((w) => {
|
|
1586
|
-
const doc = w.document;
|
|
1587
|
-
// 相对地址基准:PiP 文档是 about:blank,fetch 的相对 API 路径须按主 origin 解析
|
|
1588
|
-
const base = doc.createElement("base");
|
|
1589
|
-
base.href = window.location.origin + "/";
|
|
1590
|
-
doc.head.appendChild(base);
|
|
1591
|
-
// 样式:整套 --dbt-* token 与规则注入 PiP 文档
|
|
1592
|
-
if (styleEl) doc.head.appendChild(doc.importNode(styleEl, true));
|
|
1593
|
-
doc.body.style.margin = "0";
|
|
1594
|
-
portalDisabled = true; // PiP root 已在 pipDoc.body 上,禁 portal 防 DOM 被拉回主窗口
|
|
1595
|
-
const root = reactCreateRoot(doc.body);
|
|
1596
|
-
const closePip = () => {
|
|
1597
|
-
try { root.unmount(); } catch (e) { /* 已卸载 */ }
|
|
1598
|
-
portalDisabled = false;
|
|
1599
|
-
try { w.close(); } catch (e2) { /* 已关闭 */ }
|
|
1600
|
-
};
|
|
1601
|
-
root.render(React.createElement(PipBrowseApp, {
|
|
1602
|
-
ctx, conns: deps.conns, projectPath: deps.projectPath, grants: deps.grants,
|
|
1603
|
-
onClose: () => { closePip(); deps.onClosed(); },
|
|
1604
|
-
}));
|
|
1605
|
-
w.addEventListener("pagehide", () => { // 用户点 PiP 窗口系统关闭钮
|
|
1606
|
-
portalDisabled = false;
|
|
1607
|
-
try { root.unmount(); } catch (e) { /* 已卸载 */ }
|
|
1608
|
-
deps.onClosed();
|
|
1609
|
-
});
|
|
1610
|
-
return w;
|
|
1611
|
-
}).catch(() => null);
|
|
1612
|
-
} catch (e) {
|
|
1613
|
-
return Promise.resolve(null);
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
1413
|
/* ---------------- SQL 控制台 ---------------- */
|
|
1618
1414
|
// Apple 等宽字体栈(规范 §1):优先样式层 --dbt-mono token,fallback 内联栈
|
|
1619
1415
|
const MONO_FONT = "var(--dbt-mono, ui-monospace, \"SF Mono\", Menlo, Consolas, monospace)";
|
|
@@ -1740,23 +1536,6 @@ window.__ModuleLoader__.load({
|
|
|
1740
1536
|
const [error, setError] = React.useState("");
|
|
1741
1537
|
const [message, setMessage] = React.useState("");
|
|
1742
1538
|
const [confirmReq, setConfirmReq] = React.useState(null); // {statement,danger,reason,resolve}
|
|
1743
|
-
// 数据浏览浮窗开关:由 Panel 层持有(不随 view 切换卸载),跨 tab 常驻
|
|
1744
|
-
const [browseOpen, setBrowseOpen] = React.useState(false);
|
|
1745
|
-
// PiP 独立窗口句柄:优先于 DOM 浮窗(OS 级窗口,可拖出桌面端主窗口);存在时 DOM 浮窗不渲染
|
|
1746
|
-
const [pipWin, setPipWin] = React.useState(null);
|
|
1747
|
-
function openBrowse() {
|
|
1748
|
-
if (pipWin) { try { pipWin.focus(); } catch (e) { /* 已关闭 */ } return; }
|
|
1749
|
-
// requestWindow 必须在用户手势内同步发起(本函数仅由 onClick 直调)
|
|
1750
|
-
const p = openBrowsePip(props.ctx, {
|
|
1751
|
-
conns, projectPath, grants,
|
|
1752
|
-
onClosed: () => setPipWin(null),
|
|
1753
|
-
});
|
|
1754
|
-
if (p && typeof p.then === "function") {
|
|
1755
|
-
p.then((w) => { if (w) setPipWin(w); else setBrowseOpen(true); });
|
|
1756
|
-
} else {
|
|
1757
|
-
setBrowseOpen(true);
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
1539
|
|
|
1761
1540
|
const askConfirm = React.useCallback((info) => new Promise((resolve) => {
|
|
1762
1541
|
setConfirmReq(Object.assign({}, info, { resolve }));
|
|
@@ -1811,10 +1590,9 @@ window.__ModuleLoader__.load({
|
|
|
1811
1590
|
"div",
|
|
1812
1591
|
{ className: "dbt-tabs" },
|
|
1813
1592
|
[["manage", t("viewManage")], ["grants", t("viewGrants")], ["browse", t("viewBrowse")], ["console", t("viewConsole")]].map(([v, label]) =>
|
|
1814
|
-
// browse tab:切视图的同时直接打开浮窗(单击即用,不必再进树点表)
|
|
1815
1593
|
React.createElement("button", {
|
|
1816
1594
|
key: v, className: view === v ? "active" : "",
|
|
1817
|
-
onClick: () =>
|
|
1595
|
+
onClick: () => setView(v),
|
|
1818
1596
|
}, label)),
|
|
1819
1597
|
),
|
|
1820
1598
|
(projectEdited || !projectPath)
|
|
@@ -1850,25 +1628,14 @@ window.__ModuleLoader__.load({
|
|
|
1850
1628
|
React.createElement("div", { className: "dbt-view", key: view },
|
|
1851
1629
|
view === "manage" ? React.createElement(ManageView, shared) : null,
|
|
1852
1630
|
view === "grants" ? React.createElement(GrantsView, shared) : null,
|
|
1853
|
-
// browse
|
|
1854
|
-
view === "browse" ? React.createElement(
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
// 主提示用正常文字色(dbt-muted 11px 次要色压深底对比不足,视觉审查命中)
|
|
1858
|
-
React.createElement("div", { style: { fontSize: 13 } }, t("browsePanelHint")),
|
|
1859
|
-
React.createElement("div", { className: "dbt-row" },
|
|
1860
|
-
React.createElement("button", { className: "dbt-btn primary", onClick: openBrowse }, t("reopenBrowse"))),
|
|
1861
|
-
) : null,
|
|
1631
|
+
// browse:内嵌浏览面板(对象树 + 结构/数据预览,点表即看)
|
|
1632
|
+
view === "browse" ? React.createElement(BrowsePane, {
|
|
1633
|
+
ctx: props.ctx, conns, projectPath, grants, askConfirm,
|
|
1634
|
+
}) : null,
|
|
1862
1635
|
view === "console" ? React.createElement(ConsoleView, Object.assign({}, shared, { askConfirm })) : null,
|
|
1863
1636
|
),
|
|
1864
1637
|
React.createElement("div", { className: "dbt-muted" }, t("footerHint")),
|
|
1865
1638
|
confirmReq ? React.createElement(DangerDialog, { challenge: confirmReq, onClose: () => setConfirmReq(null) }) : null,
|
|
1866
|
-
// 浮窗常驻:portal 到 body 且在 dbt-view 外,切 tab 时不消失
|
|
1867
|
-
browseOpen ? React.createElement(BrowseDialog, {
|
|
1868
|
-
conns, projectPath, grants, askConfirm,
|
|
1869
|
-
ctx: props.ctx, // 宿主 ctx 透传给浮窗,供 portal 异步解析(ctx.modules.import)
|
|
1870
|
-
onClose: () => setBrowseOpen(false),
|
|
1871
|
-
}) : null,
|
|
1872
1639
|
);
|
|
1873
1640
|
}
|
|
1874
1641
|
|
|
@@ -1891,7 +1658,7 @@ window.__ModuleLoader__.load({
|
|
|
1891
1658
|
key: "db-tool-" + String(localeKey || "zh"),
|
|
1892
1659
|
visible: props.visible,
|
|
1893
1660
|
scope: props.scope,
|
|
1894
|
-
ctx: ctx, // 宿主 ctx 下传,Panel 透传给
|
|
1661
|
+
ctx: ctx, // 宿主 ctx 下传,Panel 透传给 BrowsePane
|
|
1895
1662
|
});
|
|
1896
1663
|
}
|
|
1897
1664
|
|
|
@@ -1914,7 +1681,7 @@ window.__ModuleLoader__.load({
|
|
|
1914
1681
|
return ctx.betterSidebar.registerTab({
|
|
1915
1682
|
id: TAB_ID,
|
|
1916
1683
|
title: function () { return t("tabTitle"); },
|
|
1917
|
-
icon
|
|
1684
|
+
icon,
|
|
1918
1685
|
order: 45,
|
|
1919
1686
|
single: true,
|
|
1920
1687
|
component: function (p) {
|
|
@@ -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.6",
|
|
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,8 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"client": {
|
|
40
40
|
"inject": [
|
|
41
|
-
"@deepseek-ai/dsh-client-locale"
|
|
42
|
-
"react-dom"
|
|
41
|
+
"@deepseek-ai/dsh-client-locale"
|
|
43
42
|
],
|
|
44
43
|
"platform": "web"
|
|
45
44
|
}
|