dsh-db-tool 1.5.0 → 1.5.1
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 +476 -91
- package/dist/adapters/mongodb/index.js +36 -7
- package/package.json +1 -1
package/client/client.js
CHANGED
|
@@ -14,7 +14,9 @@ window.__ModuleLoader__.load({
|
|
|
14
14
|
* ============================================================ */
|
|
15
15
|
|
|
16
16
|
// --- i18n(dsh-better-sidebar 同款模式)---
|
|
17
|
-
|
|
17
|
+
// i18n 字典/HTTP 封装/CSS/对话框均无单元断言,仅变异下方纯函数层(Stryker 区段指令需精确匹配,不得带后缀文本)
|
|
18
|
+
// Stryker disable all
|
|
19
|
+
const LOCALE_NS = "dbTool";
|
|
18
20
|
const zh = {
|
|
19
21
|
tabTitle: "数据库",
|
|
20
22
|
projectLabel: "项目: {path}",
|
|
@@ -90,6 +92,22 @@ window.__ModuleLoader__.load({
|
|
|
90
92
|
emptyStructure: "暂无字段信息",
|
|
91
93
|
emptyData: "暂无数据",
|
|
92
94
|
retry: "重试",
|
|
95
|
+
browsePanelHint: "数据浏览已在浮动窗口打开,可继续操作其他功能。",
|
|
96
|
+
reopenBrowse: "打开数据浏览",
|
|
97
|
+
selectTableHint: "在左侧选择表后查看数据",
|
|
98
|
+
// 单元格查看/编辑(Navicat 式就地写回)
|
|
99
|
+
editCell: "编辑",
|
|
100
|
+
cellValue: "完整值",
|
|
101
|
+
copyBtn: "复制",
|
|
102
|
+
copied: "已复制",
|
|
103
|
+
setNull: "设为 NULL",
|
|
104
|
+
cellSaving: "保存中…",
|
|
105
|
+
readOnlyRo: "只读授权,不可编辑",
|
|
106
|
+
noPkHint: "该表无主键,不支持就地编辑",
|
|
107
|
+
truncatedNoEdit: "值已截断显示,就地编辑可能覆盖数据,请改用 SQL 控制台",
|
|
108
|
+
blobNoEdit: "二进制值不支持就地编辑",
|
|
109
|
+
redisTypeRo: "Redis 该类型不支持就地编辑",
|
|
110
|
+
cellSaved: "已保存",
|
|
93
111
|
// SQL 控制台
|
|
94
112
|
modeQuery: "只读查询",
|
|
95
113
|
modeExecute: "写入执行",
|
|
@@ -184,6 +202,22 @@ window.__ModuleLoader__.load({
|
|
|
184
202
|
emptyStructure: "No columns",
|
|
185
203
|
emptyData: "No rows",
|
|
186
204
|
retry: "Retry",
|
|
205
|
+
browsePanelHint: "Browse is open in a floating window; you can keep working here.",
|
|
206
|
+
reopenBrowse: "Open Data Browser",
|
|
207
|
+
selectTableHint: "Select a table on the left to view data",
|
|
208
|
+
// Cell view/edit (Navicat-style in-place write-back)
|
|
209
|
+
editCell: "Edit",
|
|
210
|
+
cellValue: "Full value",
|
|
211
|
+
copyBtn: "Copy",
|
|
212
|
+
copied: "Copied",
|
|
213
|
+
setNull: "Set NULL",
|
|
214
|
+
cellSaving: "Saving…",
|
|
215
|
+
readOnlyRo: "Read-only grant; editing disabled",
|
|
216
|
+
noPkHint: "No primary key; in-place editing unavailable",
|
|
217
|
+
truncatedNoEdit: "Value shown truncated; edit via SQL Console instead",
|
|
218
|
+
blobNoEdit: "Binary values cannot be edited here",
|
|
219
|
+
redisTypeRo: "This Redis type cannot be edited here",
|
|
220
|
+
cellSaved: "Saved",
|
|
187
221
|
modeQuery: "query (read-only)",
|
|
188
222
|
modeExecute: "execute (write)",
|
|
189
223
|
modeScript: "script",
|
|
@@ -327,6 +361,12 @@ window.__ModuleLoader__.load({
|
|
|
327
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:11px;}",
|
|
328
362
|
".dbt-table td{font-family:var(--dbt-mono);}",
|
|
329
363
|
".dbt-tablewrap{border:none;border-radius:var(--dbt-radius-card);overflow:auto;max-height:320px;background:var(--dbt-surface);}",
|
|
364
|
+
"/* ===== 浏览弹窗表格:列分明(列间 hairline + 表头加深一档 + 行 hover) ===== */",
|
|
365
|
+
".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);font-size:12px;}",
|
|
367
|
+
".dbt-browse-root .dbt-table td{font-size:13px;padding:7px 10px;}",
|
|
368
|
+
".dbt-browse-root .dbt-table tbody tr:hover td{background:var(--dbt-surface-strong);}",
|
|
369
|
+
"@media (prefers-color-scheme: light){.dbt-browse-root .dbt-table th{background:rgba(242,242,247,.9);}}",
|
|
330
370
|
"/* ===== 对话框(macOS alert 材质) ===== */",
|
|
331
371
|
".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;}",
|
|
332
372
|
".dbt-dialog{background:var(--dbt-dialog-bg);color:inherit;border:1px solid var(--dbt-separator);border-radius:14px;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);}",
|
|
@@ -339,7 +379,7 @@ window.__ModuleLoader__.load({
|
|
|
339
379
|
".dbt-listrow:hover{background:var(--dbt-surface-strong);}",
|
|
340
380
|
".dbt-tree{display:flex;flex-direction:column;}",
|
|
341
381
|
"/* ===== Navicat 式对象树(数据浏览) ===== */",
|
|
342
|
-
".dbt-treerow{display:flex;gap:6px;align-items:center;padding:
|
|
382
|
+
".dbt-treerow{display:flex;gap:6px;align-items:center;padding:8px 14px;font-size:14px;cursor:pointer;user-select:none;border-bottom:1px solid var(--dbt-separator);transition:background .15s var(--dbt-ease);}",
|
|
343
383
|
".dbt-treerow:last-child{border-bottom:none;}",
|
|
344
384
|
".dbt-treerow:hover{background:var(--dbt-surface-strong);}",
|
|
345
385
|
".dbt-treerow.active{color:var(--dbt-accent);font-weight:600;}",
|
|
@@ -348,9 +388,10 @@ window.__ModuleLoader__.load({
|
|
|
348
388
|
".dbt-chev.leaf{visibility:hidden;}",
|
|
349
389
|
".dbt-treename{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
350
390
|
/* ===== Navicat 式浏览弹窗 ===== */
|
|
351
|
-
".dbt-browse-dialog{
|
|
352
|
-
".dbt-browse-
|
|
353
|
-
".dbt-browse-
|
|
391
|
+
".dbt-browse-dialog{position:fixed;width:min(1100px,calc(100vw - 48px));height:min(720px,calc(100vh - 48px));min-width:min(680px,calc(100vw - 16px));min-height:min(420px,calc(100vh - 16px));background:var(--dbt-dialog-bg);color:inherit;border:1px solid var(--dbt-separator);border-radius:16px;box-shadow:0 24px 64px rgba(0,0,0,.32),0 4px 16px rgba(0,0,0,.18);-webkit-backdrop-filter:blur(20px) saturate(180%);backdrop-filter:blur(20px) saturate(180%);display:flex;flex-direction:column;overflow:hidden;box-sizing:border-box;font-size:14px;animation:dbt-in .22s var(--dbt-ease);}",
|
|
392
|
+
".dbt-browse-dialog.dragging{user-select:none;}",
|
|
393
|
+
".dbt-browse-header{flex:none;display:flex;align-items:center;gap:10px;padding:12px 16px;border-bottom:1px solid var(--dbt-separator);}",
|
|
394
|
+
".dbt-browse-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600;font-size:17px;}",
|
|
354
395
|
".dbt-browse-close{flex:none;width:26px;height:26px;display:flex;align-items:center;justify-content:center;border:none;background:transparent;color:var(--dbt-text-secondary);border-radius:var(--dbt-radius-pill);cursor:pointer;transition:all .18s var(--dbt-ease);}",
|
|
355
396
|
".dbt-browse-close:hover{background:var(--dbt-surface-strong);color:inherit;}",
|
|
356
397
|
".dbt-browse-close:active{transform:scale(.92);}",
|
|
@@ -358,14 +399,25 @@ window.__ModuleLoader__.load({
|
|
|
358
399
|
".dbt-browse-tree{flex:none;width:260px;overflow-y:auto;border-right:1px solid var(--dbt-separator);}",
|
|
359
400
|
".dbt-browse-main{flex:1;min-width:0;display:flex;flex-direction:column;}",
|
|
360
401
|
".dbt-browse-tabs{flex:none;padding:10px 14px 0;}",
|
|
361
|
-
".dbt-browse-content{flex:1;min-height:0;display:flex;flex-direction:column;gap:10px;padding:12px 14px;overflow:hidden;}",
|
|
402
|
+
".dbt-browse-content{flex:1;min-height:0;position:relative;display:flex;flex-direction:column;gap:10px;padding:12px 14px;overflow:hidden;}",
|
|
362
403
|
".dbt-browse-tablewrap{flex:1;min-height:0;overflow:auto;border:none;border-radius:var(--dbt-radius-card);background:var(--dbt-surface);}",
|
|
363
404
|
".dbt-browse-footer{flex:none;display:flex;align-items:center;gap:8px;padding:10px 14px;border-top:1px solid var(--dbt-separator);}",
|
|
364
|
-
".dbt-browse-empty{flex:1;display:flex;align-items:center;justify-content:center;color:var(--dbt-text-secondary);font-size:
|
|
405
|
+
".dbt-browse-empty{flex:1;display:flex;align-items:center;justify-content:center;color:var(--dbt-text-secondary);font-size:13px;}",
|
|
365
406
|
".dbt-browse-errorbox{flex:1;display:flex;flex-direction:column;gap:10px;align-items:flex-start;justify-content:center;}",
|
|
366
407
|
".dbt-browse-skel{display:flex;flex-direction:column;gap:8px;}",
|
|
367
408
|
".dbt-browse-skelrow{display:flex;gap:8px;}",
|
|
368
409
|
".dbt-browse-skelcell{flex:1;height:24px;background:var(--dbt-surface-strong);border-radius:var(--dbt-radius-pill);}",
|
|
410
|
+
"/* ===== 浏览弹窗:单元格查看/编辑浮层(定位锚点为 .dbt-browse-content) ===== */",
|
|
411
|
+
".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
|
+
".dbt-cellbtn:focus-visible{outline:2px solid var(--dbt-accent);outline-offset:-1px;border-radius:2px;}",
|
|
413
|
+
".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:13px;white-space:pre-wrap;word-break:break-all;max-height:240px;overflow:auto;background:var(--dbt-surface-strong);border-radius:8px;padding:8px;}",
|
|
415
|
+
".dbt-cellpop-actions{display:flex;gap:8px;align-items:center;}",
|
|
416
|
+
".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:8px;padding:8px;}",
|
|
418
|
+
".dbt-celledit textarea:focus{outline:2px solid var(--dbt-accent);outline-offset:-1px;}",
|
|
419
|
+
".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;}",
|
|
369
421
|
"/* ===== 视图切换入场(仅 transform/opacity;reduced-motion 全局已禁) ===== */",
|
|
370
422
|
"@keyframes dbt-in{from{opacity:0;transform:translateY(4px);}to{opacity:1;transform:none;}}",
|
|
371
423
|
".dbt-view{display:flex;flex-direction:column;gap:10px;animation:dbt-in .22s var(--dbt-ease);}",
|
|
@@ -373,9 +425,10 @@ window.__ModuleLoader__.load({
|
|
|
373
425
|
".dbt-seg{display:flex;gap:2px;background:var(--dbt-surface);border-radius:var(--dbt-radius-ctrl);padding:2px;}",
|
|
374
426
|
".dbt-seg button{flex:none;border:none;background:transparent;color:inherit;font-size:11px;padding:3px 10px;border-radius:var(--dbt-radius-pill);cursor:pointer;transition:all .18s var(--dbt-ease);}",
|
|
375
427
|
".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;}",
|
|
376
429
|
"/* ===== 降级:透明度减弱 / 动效减弱 ===== */",
|
|
377
|
-
"@media (prefers-reduced-transparency: reduce){.dbt-overlay{backdrop-filter:none;-webkit-backdrop-filter:none;background:rgba(0,0,0,.55);}.dbt-dialog,.dbt-browse-dialog{backdrop-filter:none;-webkit-backdrop-filter:none;background:#2c2c2e;}.dbt-table th{backdrop-filter:none;-webkit-backdrop-filter:none;background:#1e1e20;}}",
|
|
378
|
-
"@media (prefers-color-scheme: light) and (prefers-reduced-transparency: reduce){.dbt-dialog,.dbt-browse-dialog{background:#f5f5f7;}.dbt-table th{background:#f2f2f7;}}",
|
|
430
|
+
"@media (prefers-reduced-transparency: reduce){.dbt-overlay{backdrop-filter:none;-webkit-backdrop-filter:none;background:rgba(0,0,0,.55);}.dbt-dialog,.dbt-browse-dialog,.dbt-cellpop{backdrop-filter:none;-webkit-backdrop-filter:none;}.dbt-dialog,.dbt-browse-dialog{background:#2c2c2e;}.dbt-cellpop{background:#2c2c2e;}.dbt-table th{backdrop-filter:none;-webkit-backdrop-filter:none;background:#1e1e20;}.dbt-browse-root .dbt-table th{background:#1e1e20;}}",
|
|
431
|
+
"@media (prefers-color-scheme: light) and (prefers-reduced-transparency: reduce){.dbt-dialog,.dbt-browse-dialog,.dbt-cellpop{background:#f5f5f7;}.dbt-table th,.dbt-browse-root .dbt-table th{background:#f2f2f7;}}",
|
|
379
432
|
"@media (prefers-reduced-motion: reduce){.dbt-panel *,.dbt-browse-root *{transition:none!important;animation:none!important;}.dbt-btn:active{transform:none;}}",
|
|
380
433
|
].join("\n");
|
|
381
434
|
document.head.appendChild(styleEl);
|
|
@@ -447,6 +500,136 @@ window.__ModuleLoader__.load({
|
|
|
447
500
|
);
|
|
448
501
|
}
|
|
449
502
|
|
|
503
|
+
/* ---------------- 单元格写回:方言与命令构造(纯函数层,PreviewGrid 与测试共用) ---------------- */
|
|
504
|
+
// Stryker restore all(以下至 buildMongoOp 为变异目标区)
|
|
505
|
+
// 统一返回形状:{ ok:true, ops:[{statement, params?, database?}] }(多条顺序执行,如 redis 两步)
|
|
506
|
+
// 或 { ok:false, error:"<i18n key>" }(error 为 key 名,组件侧 t() 本地化;null 表示通用错误)
|
|
507
|
+
|
|
508
|
+
// 方言判定:q=问号占位符(mysql/sqlite)、dollar=$n(postgresql/gaussdb)、colon=:n(oracle/dmdb)
|
|
509
|
+
function dialectOf(kind) {
|
|
510
|
+
if (kind === "mysql" || kind === "sqlite") return "q";
|
|
511
|
+
if (kind === "postgresql" || kind === "gaussdb") return "dollar";
|
|
512
|
+
if (kind === "oracle" || kind === "dmdb") return "colon";
|
|
513
|
+
if (kind === "redis") return "redis";
|
|
514
|
+
if (kind === "mongodb") return "mongo";
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// 标识符引用:mysql 用反引号(内部双写转义),其余统一双引号(内部双写转义)
|
|
519
|
+
function quoteIdent(kind, name) {
|
|
520
|
+
const q = kind === "mysql" ? "`" : "\"";
|
|
521
|
+
return q + String(name).split(q).join(q + q) + q;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// 单元格文本 → 写回值:JSON 可解析且结果非字符串才用解析值(类型不漂移:显示 "123" 保持字符串,
|
|
525
|
+
// 数字列显示 123 → parse 得 number 123)
|
|
526
|
+
function parseCellText(text) {
|
|
527
|
+
try {
|
|
528
|
+
const v = JSON.parse(text);
|
|
529
|
+
if (typeof v !== "string") return v;
|
|
530
|
+
} catch (e) { /* 非 JSON 按原字符串 */ }
|
|
531
|
+
return text;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// 截断/BLOB 标记(后端 normalize 产物):命中即禁止就地编辑,防旧值覆盖真实数据
|
|
535
|
+
function isTruncatedCell(text) {
|
|
536
|
+
const s = String(text);
|
|
537
|
+
// sql-shared 嵌套 JSON 以 "…[已截断]" 结尾;redis/mongo/oracle/dmdb 的 "…[截断,共N字符]"
|
|
538
|
+
// 结尾是"字符]",endsWith 永不命中,故该形态用 includes 检测
|
|
539
|
+
return s.endsWith("…[已截断]") || s.includes("…[截断,共");
|
|
540
|
+
}
|
|
541
|
+
function isBlobCell(text) {
|
|
542
|
+
const s = String(text);
|
|
543
|
+
return s.startsWith("[BLOB ") && s.endsWith("bytes]");
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// SQL 6 库 UPDATE 构造:全限定表名 + 方言占位符 + 主键 WHERE;params 顺序 = [新值(非 NULL 时), ...pkValues]
|
|
547
|
+
function buildUpdate(kind, tableName, dbRef, schemaName, colName, newRawText, isNull, pkCols, pkValues) {
|
|
548
|
+
if (!Array.isArray(pkCols) || pkCols.length === 0 || !colName || !tableName) {
|
|
549
|
+
return { ok: false, error: "noPkHint" };
|
|
550
|
+
}
|
|
551
|
+
// 表全限定:pg/gauss 用 schema 前缀(库由 execute 的 database 参数路由,语句内不写库名);
|
|
552
|
+
// mysql 带库名(adapter 无 database 路由);oracle/dmdb 的 dbRef 即 OWNER;sqlite 仅表名
|
|
553
|
+
let table;
|
|
554
|
+
if (kind === "postgresql" || kind === "gaussdb") {
|
|
555
|
+
table = schemaName
|
|
556
|
+
? quoteIdent(kind, schemaName) + "." + quoteIdent(kind, tableName)
|
|
557
|
+
: quoteIdent(kind, tableName);
|
|
558
|
+
} else if (kind === "mysql" || kind === "oracle" || kind === "dmdb") {
|
|
559
|
+
table = quoteIdent(kind, dbRef) + "." + quoteIdent(kind, tableName);
|
|
560
|
+
} else {
|
|
561
|
+
table = quoteIdent(kind, tableName);
|
|
562
|
+
}
|
|
563
|
+
const dialect = dialectOf(kind);
|
|
564
|
+
// 占位符编号按 params 顺序:新值(非 NULL 时占 1 号)→ 主键逐列
|
|
565
|
+
const params = [];
|
|
566
|
+
const ph = (i) => (dialect === "dollar" ? "$" + i : dialect === "colon" ? ":" + i : "?");
|
|
567
|
+
let setSql;
|
|
568
|
+
if (isNull) {
|
|
569
|
+
setSql = quoteIdent(kind, colName) + " = NULL";
|
|
570
|
+
} else {
|
|
571
|
+
params.push(parseCellText(newRawText));
|
|
572
|
+
setSql = quoteIdent(kind, colName) + " = " + ph(params.length);
|
|
573
|
+
}
|
|
574
|
+
const wheres = pkCols.map((c, i) => quoteIdent(kind, c) + " = " + ph((isNull ? 0 : 1) + i + 1));
|
|
575
|
+
for (const v of (pkValues || [])) params.push(v);
|
|
576
|
+
return {
|
|
577
|
+
ok: true,
|
|
578
|
+
ops: [{
|
|
579
|
+
statement: "UPDATE " + table + " SET " + setSql + " WHERE " + wheres.join(" AND "),
|
|
580
|
+
params,
|
|
581
|
+
database: kind === "sqlite" ? undefined : dbRef,
|
|
582
|
+
}],
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Redis 写命令构造(命令数组 JSON 化后经 execute 透传):仅数据位(value/score/member)可改,
|
|
587
|
+
// 键名/字段名只读;list/stream 与 NULL 写入不支持
|
|
588
|
+
function buildRedisOp(tableName, tableType, columns, rowValues, colName, newRawText, isNull) {
|
|
589
|
+
if (tableType === "list" || tableType === "stream" || isNull) {
|
|
590
|
+
return { ok: false, error: "redisTypeRo" };
|
|
591
|
+
}
|
|
592
|
+
const valueOf = (col) => rowValues[columns.indexOf(col)];
|
|
593
|
+
const one = (args) => ({ ok: true, ops: [{ statement: JSON.stringify(args) }] });
|
|
594
|
+
const two = (a, b) => ({ ok: true, ops: [{ statement: JSON.stringify(a) }, { statement: JSON.stringify(b) }] });
|
|
595
|
+
if (tableType === "string") {
|
|
596
|
+
if (colName === "value") return one(["SET", tableName, newRawText]);
|
|
597
|
+
if (colName === "key") return { ok: false, error: "readOnlyRo" };
|
|
598
|
+
} else if (tableType === "hash") {
|
|
599
|
+
if (colName === "value") return one(["HSET", tableName, valueOf("field"), newRawText]);
|
|
600
|
+
if (colName === "field") return { ok: false, error: "readOnlyRo" };
|
|
601
|
+
} else if (tableType === "zset") {
|
|
602
|
+
if (colName === "score") {
|
|
603
|
+
const score = Number(newRawText);
|
|
604
|
+
if (!Number.isFinite(score)) return { ok: false, error: null };
|
|
605
|
+
return one(["ZADD", tableName, score, valueOf("member")]);
|
|
606
|
+
}
|
|
607
|
+
if (colName === "member") {
|
|
608
|
+
// member 是有序集身份值:两步「删旧加新」,score 保持原值
|
|
609
|
+
return two(["ZREM", tableName, valueOf("member")], ["ZADD", tableName, valueOf("score"), newRawText]);
|
|
610
|
+
}
|
|
611
|
+
} else if (tableType === "set") {
|
|
612
|
+
if (colName === "member") return two(["SREM", tableName, valueOf("member")], ["SADD", tableName, newRawText]);
|
|
613
|
+
}
|
|
614
|
+
return { ok: false, error: "redisTypeRo" };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// MongoDB updateOne 命令文档构造:$set 单字段;filter 的 _id 按主键类型用 $oid 表达,
|
|
618
|
+
// date 列字符串值包 $date(Extended JSON 由服务端 reviveEjson 复活为 BSON 类型)
|
|
619
|
+
function buildMongoOp(tableName, idValue, idIsObjectId, colName, newRawText, isNull, colDataType) {
|
|
620
|
+
if (colName === "_id") return { ok: false, error: "readOnlyRo" };
|
|
621
|
+
let value = isNull ? null : parseCellText(newRawText);
|
|
622
|
+
if (!isNull && typeof value === "string" && typeof colDataType === "string" && colDataType.startsWith("date")) {
|
|
623
|
+
value = { "$date": value };
|
|
624
|
+
}
|
|
625
|
+
const filter = idIsObjectId ? { _id: { "$oid": idValue } } : { _id: idValue };
|
|
626
|
+
return {
|
|
627
|
+
ok: true,
|
|
628
|
+
ops: [{ statement: JSON.stringify({ updateOne: tableName, filter, update: { "$set": { [colName]: value } } }) }],
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Stryker disable all(buildMongoOp 之后:以下 auditTable/KIND_DEFAULTS/buildEditForm 为连接表单工具,无单元断言)
|
|
450
633
|
function auditTable(audit) {
|
|
451
634
|
return resultTable(
|
|
452
635
|
[t("auditTime"), t("auditAction"), t("auditConn"), t("auditDetail")],
|
|
@@ -506,7 +689,9 @@ window.__ModuleLoader__.load({
|
|
|
506
689
|
};
|
|
507
690
|
}
|
|
508
691
|
|
|
509
|
-
|
|
692
|
+
// UI 组件渲染层无单元断言(纯函数层止于此),禁用至文件尾
|
|
693
|
+
// Stryker disable all
|
|
694
|
+
function ConnForm(props) {
|
|
510
695
|
// 新建(无 initial)默认分字段模式并预填官方默认值;编辑保持用户数据原样
|
|
511
696
|
const [form, setForm] = React.useState(() => props.initial || freshForm());
|
|
512
697
|
const [dirty, setDirty] = React.useState(() => new Set()); // 用户手改过的字段(切 kind 时保留)
|
|
@@ -789,9 +974,9 @@ window.__ModuleLoader__.load({
|
|
|
789
974
|
}
|
|
790
975
|
|
|
791
976
|
/* ---------------- 数据浏览 ---------------- */
|
|
792
|
-
// Navicat 式对象树:连接 ▸ 库/[schema] ▸
|
|
977
|
+
// Navicat 式对象树:连接 ▸ 库/[schema] ▸ 表,懒加载展开(弹窗唯一实例,展开状态随弹窗生命周期)
|
|
793
978
|
function BrowseTree(props) {
|
|
794
|
-
const { conns, projectPath, sel, onSelect } = props;
|
|
979
|
+
const { conns, projectPath, sel, onSelect, autoExpand } = props;
|
|
795
980
|
// sel 由父组件持有(弹窗内 active 高亮与初始选中都依赖);onSelect(选中记录, 触发元素) 上报
|
|
796
981
|
const [open, setOpen] = React.useState({}); // "c:<id>" | "d:<id>/<db>" -> bool
|
|
797
982
|
const [loading, setLoading] = React.useState({});
|
|
@@ -828,6 +1013,14 @@ window.__ModuleLoader__.load({
|
|
|
828
1013
|
setDbs((m) => Object.assign({}, m, { [c.id]: list || [] }));
|
|
829
1014
|
}));
|
|
830
1015
|
}
|
|
1016
|
+
// autoExpand:挂载后自动展开第一层级(连接节点),复用 toggleConn 的展开+懒加载;
|
|
1017
|
+
// 加载失败仍走既有就地错误行机制。声明在清缓存 effect 之后:项目切换时先清 open 再重新展开
|
|
1018
|
+
React.useEffect(() => {
|
|
1019
|
+
if (!autoExpand) return;
|
|
1020
|
+
for (const c of conns) {
|
|
1021
|
+
if (!open["c:" + c.id]) toggleConn(c);
|
|
1022
|
+
}
|
|
1023
|
+
}, [conns, projectPath]); // eslint-disable-line
|
|
831
1024
|
// PG/GaussDB 官方层级为 数据库 → 模式(schema) → 表:库节点下先列 schema 再列表
|
|
832
1025
|
const HAS_SCHEMAS = { postgresql: true, gaussdb: true };
|
|
833
1026
|
function toggleDb(c, d) {
|
|
@@ -936,40 +1129,186 @@ window.__ModuleLoader__.load({
|
|
|
936
1129
|
);
|
|
937
1130
|
}
|
|
938
1131
|
|
|
939
|
-
//
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1132
|
+
// Navicat 式浏览弹窗:左树选库-表,右看字段+数据(非模态可拖动浮动窗,由 Panel 层持有跨 tab 常驻)
|
|
1133
|
+
|
|
1134
|
+
/* ---------------- PreviewGrid:数据预览网格(Navicat 式单元格查看/编辑) ---------------- */
|
|
1135
|
+
// props 契约:{ preview:{columns,rows,rowCount,truncated?}, schema:ColumnInfo[](SQL/mongo;redis 为
|
|
1136
|
+
// type/encoding/ttl/length,无 PRI), kind, editable:rw 授权布尔, tableType:sel.table.type(redis 键类型),
|
|
1137
|
+
// tableName, dbRef, schemaName, connId, projectPath, onSaved:保存成功回调, askConfirm:runGuarded 确认回调 }
|
|
1138
|
+
// redis 各键类型可就地编辑的数据列(与 buildRedisOp 分支保持一致;键名/字段名等定位列只读)
|
|
1139
|
+
const REDIS_EDITABLE_COLS = {
|
|
1140
|
+
string: ["value"],
|
|
1141
|
+
hash: ["value"],
|
|
1142
|
+
zset: ["score", "member"],
|
|
1143
|
+
set: ["member"],
|
|
1144
|
+
};
|
|
1145
|
+
function PreviewGrid(props) {
|
|
1146
|
+
const { preview, schema, kind, editable, tableType, tableName, dbRef, schemaName, connId, projectPath, onSaved, askConfirm } = props;
|
|
1147
|
+
const [pop, setPop] = React.useState(null); // {rowIdx, colName, cell} 单元格浮层
|
|
1148
|
+
const [editing, setEditing] = React.useState(false);
|
|
1149
|
+
const [text, setText] = React.useState(""); // 编辑态 textarea 值(进入浮层时就位)
|
|
1150
|
+
const [editNull, setEditNull] = React.useState(false); // 「设为 NULL」勾选(null 单元格默认勾选)
|
|
1151
|
+
const [cellErr, setCellErr] = React.useState("");
|
|
1152
|
+
const [busy, setBusy] = React.useState(false);
|
|
1153
|
+
const [copied, setCopied] = React.useState(false);
|
|
1154
|
+
const isMongo = kind === "mongodb";
|
|
1155
|
+
// SQL 系主键列名(schema 中 key==="PRI");mongo 由 _id 单列承担,redis 无主键概念
|
|
1156
|
+
const pkCols = (!isMongo && kind !== "redis" && Array.isArray(schema))
|
|
1157
|
+
? schema.filter((c) => c && c.key === "PRI").map((c) => c.name)
|
|
1158
|
+
: [];
|
|
1159
|
+
|
|
1160
|
+
// 打开浮层(查看态):编辑态初值一并就位——textarea 空 ↔ 勾选设为 NULL
|
|
1161
|
+
function openCell(rowIdx, colName, cell) {
|
|
1162
|
+
setPop({ rowIdx, colName, cell });
|
|
1163
|
+
setEditing(false);
|
|
1164
|
+
setCellErr("");
|
|
1165
|
+
setEditNull(cell === null);
|
|
1166
|
+
setText(cell === null ? "" : String(cell));
|
|
1167
|
+
}
|
|
1168
|
+
function copyCell() {
|
|
1169
|
+
if (!pop) return;
|
|
1170
|
+
navigator.clipboard.writeText(String(pop.cell)).then(() => {
|
|
1171
|
+
setCopied(true);
|
|
1172
|
+
window.setTimeout(() => setCopied(false), 1500);
|
|
1173
|
+
}, () => { /* 剪贴板不可用(无权限等):静默,按钮文案不变 */ });
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// 单元格编辑资格:返回 null=可编辑,否则为原因 i18n key(浮层 .dbt-readhint 展示)
|
|
1177
|
+
function cellReason(colName, shown) {
|
|
1178
|
+
if (!editable || !dialectOf(kind)) return "readOnlyRo";
|
|
1179
|
+
if (isMongo) return colName === "_id" ? "readOnlyRo" : null;
|
|
1180
|
+
if (kind === "redis") {
|
|
1181
|
+
if ((tableType === "string" && colName === "key") || (tableType === "hash" && colName === "field")) return "readOnlyRo";
|
|
1182
|
+
return (REDIS_EDITABLE_COLS[tableType] || []).includes(colName) ? null : "redisTypeRo";
|
|
1183
|
+
}
|
|
1184
|
+
// SQL 系:整表无主键 → 只读;截断/BLOB 单元格 → 防旧值覆盖
|
|
1185
|
+
if (pkCols.length === 0) return "noPkHint";
|
|
1186
|
+
if (isTruncatedCell(shown)) return "truncatedNoEdit";
|
|
1187
|
+
if (isBlobCell(shown)) return "blobNoEdit";
|
|
1188
|
+
return null;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// 保存:构造写回命令(按 kind 分派)→ 顺序执行(redis 两步)→ 全部成功关浮层并回调 onSaved
|
|
1192
|
+
async function saveCell() {
|
|
1193
|
+
if (!pop) return;
|
|
1194
|
+
setCellErr("");
|
|
1195
|
+
const { rowIdx, colName } = pop;
|
|
1196
|
+
const row = (preview.rows || [])[rowIdx] || [];
|
|
1197
|
+
let r;
|
|
1198
|
+
if (kind === "redis") {
|
|
1199
|
+
r = buildRedisOp(tableName, tableType, preview.columns, row, colName, text, editNull);
|
|
1200
|
+
} else if (isMongo) {
|
|
1201
|
+
const idIdx = preview.columns.indexOf("_id");
|
|
1202
|
+
const idCol = (schema || []).find((c) => c && c.name === "_id");
|
|
1203
|
+
const colInfo = (schema || []).find((c) => c && c.name === colName);
|
|
1204
|
+
r = buildMongoOp(tableName, idIdx >= 0 ? row[idIdx] : undefined,
|
|
1205
|
+
!!(idCol && typeof idCol.dataType === "string" && idCol.dataType.startsWith("ObjectId")),
|
|
1206
|
+
colName, text, editNull, colInfo ? colInfo.dataType : undefined);
|
|
1207
|
+
} else {
|
|
1208
|
+
r = buildUpdate(kind, tableName, dbRef, schemaName, colName, text, editNull, pkCols,
|
|
1209
|
+
pkCols.map((c) => row[preview.columns.indexOf(c)]));
|
|
1210
|
+
}
|
|
1211
|
+
if (!r.ok) { setCellErr(r.error ? t(r.error) : t("error")); return; }
|
|
1212
|
+
setBusy(true);
|
|
1213
|
+
try {
|
|
1214
|
+
for (const op of r.ops) {
|
|
1215
|
+
await runGuarded(
|
|
1216
|
+
(challengeId) => api("execute", { method: "POST", body: { projectPath, connId, statement: op.statement, params: op.params, database: op.database, challengeId } }),
|
|
1217
|
+
askConfirm,
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
setPop(null);
|
|
1221
|
+
if (onSaved) onSaved();
|
|
1222
|
+
} catch (e) {
|
|
1223
|
+
// 取消确认(e.cancelled)不算失败:浮层保持打开供重试
|
|
1224
|
+
if (!e.cancelled) setCellErr(t("error") + ": " + String(e && e.message ? e.message : e));
|
|
1225
|
+
} finally {
|
|
1226
|
+
setBusy(false);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const popReason = pop ? cellReason(pop.colName, pop.cell === null ? "NULL" : String(pop.cell)) : null;
|
|
947
1231
|
return React.createElement(
|
|
948
1232
|
"div",
|
|
949
|
-
{ style: { display: "flex", flexDirection: "column",
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
},
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1233
|
+
{ className: "dbt-browse-root", style: { display: "flex", flexDirection: "column", flex: 1, minHeight: 0 } },
|
|
1234
|
+
// 顶层只读提示:授权为 ro 时整格只读,单元格仍可点击查看完整值
|
|
1235
|
+
!editable ? React.createElement("div", { className: "dbt-readhint" }, t("readOnlyRo")) : null,
|
|
1236
|
+
// relative 容器承载单元格浮层(简化定位:水平居中贴底)
|
|
1237
|
+
React.createElement(
|
|
1238
|
+
"div",
|
|
1239
|
+
{ style: { position: "relative", flex: 1, minHeight: 0, display: "flex", flexDirection: "column" } },
|
|
1240
|
+
React.createElement(
|
|
1241
|
+
"div",
|
|
1242
|
+
{ className: "dbt-browse-tablewrap" },
|
|
1243
|
+
React.createElement(
|
|
1244
|
+
"table",
|
|
1245
|
+
{ className: "dbt-table" },
|
|
1246
|
+
React.createElement(
|
|
1247
|
+
"thead",
|
|
1248
|
+
null,
|
|
1249
|
+
React.createElement("tr", null, (preview.columns || []).map((c, i) => React.createElement("th", { key: i }, c))),
|
|
1250
|
+
),
|
|
1251
|
+
React.createElement(
|
|
1252
|
+
"tbody",
|
|
1253
|
+
null,
|
|
1254
|
+
(preview.rows || []).map((row, i) =>
|
|
1255
|
+
React.createElement("tr", { key: i },
|
|
1256
|
+
(preview.columns || []).map((colName, j) =>
|
|
1257
|
+
React.createElement("td", { key: j, title: row[j] === null ? "NULL" : String(row[j]) },
|
|
1258
|
+
React.createElement("button", {
|
|
1259
|
+
className: "dbt-cellbtn", type: "button",
|
|
1260
|
+
onClick: () => openCell(i, colName, row[j]),
|
|
1261
|
+
}, row[j] === null ? React.createElement("span", { className: "dbt-nullchip" }, "NULL") : String(row[j]))))),
|
|
1262
|
+
),
|
|
1263
|
+
),
|
|
1264
|
+
),
|
|
1265
|
+
pop ? React.createElement(
|
|
1266
|
+
"div",
|
|
1267
|
+
{ className: "dbt-cellpop", style: { position: "absolute", left: "50%", transform: "translateX(-50%)", bottom: 12 } },
|
|
1268
|
+
React.createElement("strong", null, t("cellValue") + " · " + pop.colName),
|
|
1269
|
+
React.createElement(
|
|
1270
|
+
"div",
|
|
1271
|
+
{ className: "dbt-cellpop-value" },
|
|
1272
|
+
pop.cell === null ? React.createElement("span", { className: "dbt-nullchip" }, "NULL") : String(pop.cell),
|
|
1273
|
+
),
|
|
1274
|
+
popReason ? React.createElement("div", { className: "dbt-readhint" }, t(popReason)) : null,
|
|
1275
|
+
cellErr ? React.createElement("div", { className: "dbt-err" }, cellErr) : null,
|
|
1276
|
+
editing
|
|
1277
|
+
? React.createElement(
|
|
1278
|
+
"div",
|
|
1279
|
+
{ className: "dbt-celledit" },
|
|
1280
|
+
React.createElement("textarea", { value: text, onChange: (e) => setText(e.target.value) }),
|
|
1281
|
+
React.createElement(
|
|
1282
|
+
"label",
|
|
1283
|
+
// 不用 .dbt-row:全局 input flex:1 会把 checkbox 拉伸占满、标签被推到远端(视觉审查命中)
|
|
1284
|
+
{ style: { display: "flex", alignItems: "center", gap: 6, flexWrap: "nowrap", width: "fit-content" } },
|
|
1285
|
+
React.createElement("input", { type: "checkbox", style: { flex: "none", minWidth: 0, width: "auto" }, checked: editNull, onChange: (e) => setEditNull(e.target.checked) }),
|
|
1286
|
+
t("setNull"),
|
|
1287
|
+
),
|
|
1288
|
+
React.createElement(
|
|
1289
|
+
"div",
|
|
1290
|
+
{ className: "dbt-row" },
|
|
1291
|
+
React.createElement("button", { className: "dbt-btn primary", disabled: busy, onClick: saveCell }, busy ? t("cellSaving") : t("save")),
|
|
1292
|
+
React.createElement("button", { className: "dbt-btn", disabled: busy, onClick: () => setEditing(false) }, t("cancel")),
|
|
1293
|
+
),
|
|
1294
|
+
)
|
|
1295
|
+
: React.createElement(
|
|
1296
|
+
"div",
|
|
1297
|
+
{ className: "dbt-cellpop-actions" },
|
|
1298
|
+
React.createElement("button", { className: "dbt-btn", onClick: copyCell }, copied ? t("copied") : t("copyBtn")),
|
|
1299
|
+
popReason === null ? React.createElement("button", { className: "dbt-btn primary", onClick: () => setEditing(true) }, t("editCell")) : null,
|
|
1300
|
+
React.createElement("button", { className: "dbt-btn", onClick: () => setPop(null) }, t("close")),
|
|
1301
|
+
),
|
|
1302
|
+
) : null,
|
|
1303
|
+
),
|
|
1304
|
+
),
|
|
966
1305
|
);
|
|
967
1306
|
}
|
|
968
1307
|
|
|
969
|
-
// Navicat 式浏览弹窗:左树选库-表,右看字段+数据(近全屏模态)
|
|
970
1308
|
function BrowseDialog(props) {
|
|
971
|
-
const {
|
|
972
|
-
|
|
1309
|
+
const { conns, projectPath, grants, askConfirm, onClose } = props;
|
|
1310
|
+
// grants / askConfirm:预留给后续单元格编辑的危险操作确认通道,本任务先接住不使用
|
|
1311
|
+
const [sel, setSel] = React.useState(null); // 弹窗内当前选中(null=未选表,右栏显示引导空态)
|
|
973
1312
|
const [view, setView] = React.useState("structure"); // structure | preview(纯视图切换,不影响数据加载)
|
|
974
1313
|
const [schema, setSchema] = React.useState([]);
|
|
975
1314
|
const [preview, setPreview] = React.useState(null); // QueryResult
|
|
@@ -977,7 +1316,6 @@ window.__ModuleLoader__.load({
|
|
|
977
1316
|
const [busy, setBusy] = React.useState("");
|
|
978
1317
|
const [loadErr, setLoadErr] = React.useState(""); // 右栏加载失败(就地显示 + 重试)
|
|
979
1318
|
const openSeq = React.useRef(0); // 请求序号守卫
|
|
980
|
-
const dialogRef = React.useRef(null);
|
|
981
1319
|
const closeRef = React.useRef(null);
|
|
982
1320
|
|
|
983
1321
|
// 请求序号守卫(沿用面板):快速切表/翻页时丢弃晚到旧响应,防止旧数据覆盖新选中项
|
|
@@ -1001,34 +1339,42 @@ window.__ModuleLoader__.load({
|
|
|
1001
1339
|
}, [projectPath]);
|
|
1002
1340
|
React.useEffect(() => { if (sel) openTable(sel, 1); }, [sel]); // eslint-disable-line
|
|
1003
1341
|
|
|
1004
|
-
//
|
|
1005
|
-
//
|
|
1342
|
+
// 非模态浮窗:无焦点陷阱(焦点可自由离开,主页交互不受阻);Esc 仅在焦点位于弹窗内时
|
|
1343
|
+
// 经容器 onKeyDown 触发(stopPropagation 防止宿主级快捷键同时响应)。初始焦点=关闭按钮
|
|
1006
1344
|
React.useEffect(() => {
|
|
1007
|
-
const onKey = (e) => {
|
|
1008
|
-
if (e.key === "Escape") { e.preventDefault(); onClose(); return; }
|
|
1009
|
-
if (e.key === "Tab") trapTab(e);
|
|
1010
|
-
};
|
|
1011
|
-
window.addEventListener("keydown", onKey);
|
|
1012
|
-
// 初始焦点=关闭按钮(APG:对话框内首个交互元素;禁止聚焦 role="dialog" 容器本身)
|
|
1013
1345
|
if (closeRef.current) closeRef.current.focus();
|
|
1014
|
-
|
|
1015
|
-
}, []); // 依赖 []:onClose 语义恒定(setSel(null)+焦点返回),无陈旧闭包危害
|
|
1346
|
+
}, []);
|
|
1016
1347
|
|
|
1017
|
-
//
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1348
|
+
// 拖动定位:初始停靠视口右上(宽度按 1100 常量估算,.dbt-browse-dialog 为 max-width:1400 的自适应宽)
|
|
1349
|
+
const [pos, setPos] = React.useState(() => ({ top: 72, left: Math.max(12, (window.innerWidth || 1200) - 1100 - 24) }));
|
|
1350
|
+
const draggingRef = React.useRef(null); // {dx, dy}
|
|
1351
|
+
const [dragging, setDragging] = React.useState(false);
|
|
1352
|
+
function stopDrag() { draggingRef.current = null; setDragging(false); }
|
|
1353
|
+
function onHeaderPointerDown(e) {
|
|
1354
|
+
// header 内可点元素(关闭按钮)不启动拖动,否则 setPointerCapture 会把 click 重定向到 header 使按钮失效
|
|
1355
|
+
if (e.target.closest && e.target.closest("button")) return;
|
|
1356
|
+
draggingRef.current = { dx: e.clientX - pos.left, dy: e.clientY - pos.top };
|
|
1357
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
1358
|
+
setDragging(true);
|
|
1359
|
+
}
|
|
1360
|
+
function onHeaderPointerMove(e) {
|
|
1361
|
+
if (!draggingRef.current) return;
|
|
1362
|
+
const h = window.innerHeight || 800, w = window.innerWidth || 1200;
|
|
1363
|
+
// 垂直 clamp 用浮窗实际高度:整体保持在视口内(视觉审查:底部分页栏不得溢出被裁);
|
|
1364
|
+
// 水平放宽到至少留 120px 可抓回(允许左右拖出一半)
|
|
1365
|
+
const dlgH = (e.currentTarget.parentElement && e.currentTarget.parentElement.offsetHeight) || 720;
|
|
1366
|
+
setPos({
|
|
1367
|
+
top: Math.min(Math.max(e.clientY - draggingRef.current.dy, 0), Math.max(0, h - dlgH)),
|
|
1368
|
+
left: Math.min(Math.max(e.clientX - draggingRef.current.dx, -(1100 - 120)), w - 120),
|
|
1369
|
+
});
|
|
1028
1370
|
}
|
|
1029
1371
|
|
|
1030
|
-
//
|
|
1372
|
+
// 右栏互斥状态:未选中引导 / 加载骨架 / 错误重试 / 空态 / 表格
|
|
1031
1373
|
function rightPane() {
|
|
1374
|
+
if (!sel && busy !== "open" && !loadErr) {
|
|
1375
|
+
// 未选表:显示引导空态(浏览窗口可直接打开,不强制先选表)
|
|
1376
|
+
return React.createElement("div", { className: "dbt-browse-empty" }, t("selectTableHint"));
|
|
1377
|
+
}
|
|
1032
1378
|
if (busy === "open") {
|
|
1033
1379
|
// 表形状骨架:6 行 6 列静态色块(不引入新动画,reduced-motion 天然安全)
|
|
1034
1380
|
return React.createElement("div", { className: "dbt-browse-skel", role: "status", "aria-busy": "true" },
|
|
@@ -1051,37 +1397,51 @@ window.__ModuleLoader__.load({
|
|
|
1051
1397
|
if (!preview || !preview.rows || preview.rows.length === 0) {
|
|
1052
1398
|
return React.createElement("div", { className: "dbt-browse-empty" }, t("emptyData"));
|
|
1053
1399
|
}
|
|
1054
|
-
|
|
1400
|
+
// Navicat 式单元格网格:kind/可编辑性由当前连接与授权决定(rw=可编辑),保存后重拉当前页
|
|
1401
|
+
const conn = conns.find((c) => c.id === sel.connId);
|
|
1402
|
+
const grant = (grants || []).find((g) => g.connId === sel.connId);
|
|
1403
|
+
return React.createElement(PreviewGrid, {
|
|
1404
|
+
preview, schema,
|
|
1405
|
+
kind: conn ? conn.kind : undefined,
|
|
1406
|
+
editable: !!grant && grant.mode === "rw",
|
|
1407
|
+
tableType: sel.table.type,
|
|
1408
|
+
tableName: sel.table.name,
|
|
1409
|
+
dbRef: sel.schemaName ? sel.db + "." + sel.schemaName : sel.db,
|
|
1410
|
+
schemaName: sel.schemaName || null,
|
|
1411
|
+
connId: sel.connId, projectPath,
|
|
1412
|
+
onSaved: () => openTable(sel, page),
|
|
1413
|
+
askConfirm,
|
|
1414
|
+
});
|
|
1055
1415
|
}
|
|
1056
1416
|
|
|
1057
|
-
//
|
|
1058
|
-
// 仅当按下与释放都在遮罩自身才视为「点击遮罩关闭」(双审查报告一致命中)
|
|
1059
|
-
const downOnOverlay = React.useRef(false);
|
|
1417
|
+
// 非模态浮窗:无遮罩、fixed 定位、可拖动;portal 到 body 不受侧栏视图切换影响
|
|
1060
1418
|
return mountDialog(React.createElement(
|
|
1061
1419
|
"div",
|
|
1062
1420
|
{
|
|
1063
|
-
className: "dbt-
|
|
1064
|
-
|
|
1065
|
-
|
|
1421
|
+
className: "dbt-browse-root dbt-browse-dialog" + (dragging ? " dragging" : ""),
|
|
1422
|
+
style: { position: "fixed", top: pos.top, left: pos.left },
|
|
1423
|
+
role: "dialog", "aria-labelledby": "dbt-browse-title",
|
|
1424
|
+
onKeyDown: (e) => {
|
|
1425
|
+
if (e.key === "Escape") { e.stopPropagation(); onClose(); }
|
|
1426
|
+
},
|
|
1066
1427
|
},
|
|
1428
|
+
// ---- header:面包屑 + 关闭(整条 header 可拖动)----
|
|
1067
1429
|
React.createElement(
|
|
1068
1430
|
"div",
|
|
1069
1431
|
{
|
|
1070
|
-
className: "dbt-browse-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1432
|
+
className: "dbt-browse-header",
|
|
1433
|
+
style: { cursor: dragging ? "grabbing" : "grab" },
|
|
1434
|
+
onPointerDown: onHeaderPointerDown,
|
|
1435
|
+
onPointerMove: onHeaderPointerMove,
|
|
1436
|
+
onPointerUp: stopDrag,
|
|
1437
|
+
onPointerCancel: stopDrag,
|
|
1074
1438
|
},
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
conn: (conns.find((c) => c.id === sel.connId) || {}).name || sel.connId,
|
|
1082
|
-
db: sel.schemaName ? sel.db + "." + sel.schemaName : sel.db,
|
|
1083
|
-
table: sel.table.name,
|
|
1084
|
-
})),
|
|
1439
|
+
React.createElement("div", { className: "dbt-browse-title", id: "dbt-browse-title" },
|
|
1440
|
+
!sel ? t("viewBrowse") : t("browseDialogTitle", {
|
|
1441
|
+
conn: (conns.find((c) => c.id === sel.connId) || {}).name || sel.connId,
|
|
1442
|
+
db: sel.schemaName ? sel.db + "." + sel.schemaName : sel.db,
|
|
1443
|
+
table: sel.table.name,
|
|
1444
|
+
})),
|
|
1085
1445
|
React.createElement(
|
|
1086
1446
|
"button",
|
|
1087
1447
|
{ className: "dbt-browse-close", ref: closeRef, "aria-label": t("close"), onClick: onClose },
|
|
@@ -1097,8 +1457,8 @@ window.__ModuleLoader__.load({
|
|
|
1097
1457
|
"div",
|
|
1098
1458
|
{ className: "dbt-browse-tree" },
|
|
1099
1459
|
React.createElement(BrowseTree, {
|
|
1100
|
-
conns, projectPath, sel,
|
|
1101
|
-
onSelect: (s) => setSel(s), //
|
|
1460
|
+
conns, projectPath, sel, autoExpand: true, // 本任务后弹窗树是唯一树实例,挂载即展开第一层级
|
|
1461
|
+
onSelect: (s) => setSel(s), // 弹窗内树切表
|
|
1102
1462
|
}),
|
|
1103
1463
|
),
|
|
1104
1464
|
React.createElement(
|
|
@@ -1119,8 +1479,8 @@ window.__ModuleLoader__.load({
|
|
|
1119
1479
|
React.createElement("div", { className: "dbt-browse-content" }, rightPane()),
|
|
1120
1480
|
),
|
|
1121
1481
|
),
|
|
1122
|
-
// ---- footer
|
|
1123
|
-
view === "preview" && !loadErr ? React.createElement(
|
|
1482
|
+
// ---- footer:分页条(仅已选表的数据预览视图;空选中态不显示) ----
|
|
1483
|
+
sel && view === "preview" && !loadErr ? React.createElement(
|
|
1124
1484
|
"div",
|
|
1125
1485
|
{ className: "dbt-browse-footer" },
|
|
1126
1486
|
React.createElement("button", { className: "dbt-btn", disabled: page <= 1 || busy === "open", onClick: () => openTable(sel, page - 1) }, "‹ " + t("prevPage")),
|
|
@@ -1130,7 +1490,7 @@ window.__ModuleLoader__.load({
|
|
|
1130
1490
|
React.createElement("button", { className: "dbt-btn", disabled: (preview && preview.truncated) === false || busy === "open", onClick: () => openTable(sel, page + 1) }, t("nextPage") + " ›"),
|
|
1131
1491
|
) : null,
|
|
1132
1492
|
),
|
|
1133
|
-
)
|
|
1493
|
+
);
|
|
1134
1494
|
}
|
|
1135
1495
|
|
|
1136
1496
|
/* ---------------- SQL 控制台 ---------------- */
|
|
@@ -1259,6 +1619,8 @@ window.__ModuleLoader__.load({
|
|
|
1259
1619
|
const [error, setError] = React.useState("");
|
|
1260
1620
|
const [message, setMessage] = React.useState("");
|
|
1261
1621
|
const [confirmReq, setConfirmReq] = React.useState(null); // {statement,danger,reason,resolve}
|
|
1622
|
+
// 数据浏览浮窗开关:由 Panel 层持有(不随 view 切换卸载),跨 tab 常驻
|
|
1623
|
+
const [browseOpen, setBrowseOpen] = React.useState(false);
|
|
1262
1624
|
|
|
1263
1625
|
const askConfirm = React.useCallback((info) => new Promise((resolve) => {
|
|
1264
1626
|
setConfirmReq(Object.assign({}, info, { resolve }));
|
|
@@ -1313,7 +1675,11 @@ window.__ModuleLoader__.load({
|
|
|
1313
1675
|
"div",
|
|
1314
1676
|
{ className: "dbt-tabs" },
|
|
1315
1677
|
[["manage", t("viewManage")], ["grants", t("viewGrants")], ["browse", t("viewBrowse")], ["console", t("viewConsole")]].map(([v, label]) =>
|
|
1316
|
-
|
|
1678
|
+
// browse tab:切视图的同时直接打开浮窗(单击即用,不必再进树点表)
|
|
1679
|
+
React.createElement("button", {
|
|
1680
|
+
key: v, className: view === v ? "active" : "",
|
|
1681
|
+
onClick: () => { setView(v); if (v === "browse") setBrowseOpen(true); },
|
|
1682
|
+
}, label)),
|
|
1317
1683
|
),
|
|
1318
1684
|
(projectEdited || !projectPath)
|
|
1319
1685
|
? React.createElement(
|
|
@@ -1348,11 +1714,24 @@ window.__ModuleLoader__.load({
|
|
|
1348
1714
|
React.createElement("div", { className: "dbt-view", key: view },
|
|
1349
1715
|
view === "manage" ? React.createElement(ManageView, shared) : null,
|
|
1350
1716
|
view === "grants" ? React.createElement(GrantsView, shared) : null,
|
|
1351
|
-
|
|
1717
|
+
// browse:提示卡(浮窗由 Panel 层在下方常驻渲染,不随视图切换卸载)
|
|
1718
|
+
view === "browse" ? React.createElement(
|
|
1719
|
+
"div",
|
|
1720
|
+
{ className: "dbt-card" },
|
|
1721
|
+
// 主提示用正常文字色(dbt-muted 11px 次要色压深底对比不足,视觉审查命中)
|
|
1722
|
+
React.createElement("div", { style: { fontSize: 13 } }, t("browsePanelHint")),
|
|
1723
|
+
React.createElement("div", { className: "dbt-row" },
|
|
1724
|
+
React.createElement("button", { className: "dbt-btn primary", onClick: () => setBrowseOpen(true) }, t("reopenBrowse"))),
|
|
1725
|
+
) : null,
|
|
1352
1726
|
view === "console" ? React.createElement(ConsoleView, Object.assign({}, shared, { askConfirm })) : null,
|
|
1353
1727
|
),
|
|
1354
1728
|
React.createElement("div", { className: "dbt-muted" }, t("footerHint")),
|
|
1355
1729
|
confirmReq ? React.createElement(DangerDialog, { challenge: confirmReq, onClose: () => setConfirmReq(null) }) : null,
|
|
1730
|
+
// 浮窗常驻:portal 到 body 且在 dbt-view 外,切 tab 时不消失
|
|
1731
|
+
browseOpen ? React.createElement(BrowseDialog, {
|
|
1732
|
+
conns, projectPath, grants, askConfirm,
|
|
1733
|
+
onClose: () => setBrowseOpen(false),
|
|
1734
|
+
}) : null,
|
|
1356
1735
|
);
|
|
1357
1736
|
}
|
|
1358
1737
|
|
|
@@ -1412,6 +1791,12 @@ window.__ModuleLoader__.load({
|
|
|
1412
1791
|
}
|
|
1413
1792
|
exports.apply = apply;
|
|
1414
1793
|
exports.inject = inject;
|
|
1794
|
+
// 测试面:单元格写回纯函数(单测直接断言命令构造,无需起 React/HTTP)
|
|
1795
|
+
exports.__testables = {
|
|
1796
|
+
dialectOf, quoteIdent, parseCellText, isTruncatedCell, isBlobCell,
|
|
1797
|
+
buildUpdate, buildRedisOp, buildMongoOp,
|
|
1798
|
+
};
|
|
1415
1799
|
return module.exports;
|
|
1416
1800
|
},
|
|
1417
1801
|
});
|
|
1802
|
+
// Stryker restore all
|
|
@@ -355,7 +355,7 @@ export async function createMongoAdapter(conn, opts) {
|
|
|
355
355
|
switch (op) {
|
|
356
356
|
case 'insertOne': {
|
|
357
357
|
// 不设 cmd.insert 别名:该键语义是集合名(insertMany 命令形态),混用会误导报错方向
|
|
358
|
-
const doc = requireObject(cmd.document, 'insertOne.document');
|
|
358
|
+
const doc = reviveEjson(requireObject(cmd.document, 'insertOne.document'));
|
|
359
359
|
const r = await c.insertOne(doc);
|
|
360
360
|
return { affectedRows: 1, message: `已插入 1 条文档到 ${name}(_id=${String(r.insertedId)})` };
|
|
361
361
|
}
|
|
@@ -363,7 +363,7 @@ export async function createMongoAdapter(conn, opts) {
|
|
|
363
363
|
if (!Array.isArray(cmd.documents ?? cmd.docs) || (cmd.documents ?? cmd.docs).length === 0) {
|
|
364
364
|
throw new Error('MongoDB insertMany 需要非空 documents 数组:{"insertMany":"coll","documents":[{...}]}');
|
|
365
365
|
}
|
|
366
|
-
const docs = (cmd.documents ?? cmd.docs).map((d) => requireObject(d, 'insertMany.documents[]'));
|
|
366
|
+
const docs = (cmd.documents ?? cmd.docs).map((d) => reviveEjson(requireObject(d, 'insertMany.documents[]')));
|
|
367
367
|
const r = await c.insertMany(docs);
|
|
368
368
|
return { affectedRows: r.insertedCount, message: `已插入 ${r.insertedCount} 条文档到 ${name}` };
|
|
369
369
|
}
|
|
@@ -371,8 +371,8 @@ export async function createMongoAdapter(conn, opts) {
|
|
|
371
371
|
case 'updateMany': {
|
|
372
372
|
if (op === 'updateMany')
|
|
373
373
|
assertExplicitFilter(op, cmd.filter);
|
|
374
|
-
const filter = requireObject(cmd.filter, `${op}.filter`);
|
|
375
|
-
const update = requireObject(cmd.update, `${op}.update`);
|
|
374
|
+
const filter = reviveEjson(requireObject(cmd.filter, `${op}.filter`));
|
|
375
|
+
const update = reviveEjson(requireObject(cmd.update, `${op}.update`));
|
|
376
376
|
const r = op === 'updateOne'
|
|
377
377
|
? await c.updateOne(filter, update)
|
|
378
378
|
: await c.updateMany(filter, update);
|
|
@@ -382,8 +382,8 @@ export async function createMongoAdapter(conn, opts) {
|
|
|
382
382
|
};
|
|
383
383
|
}
|
|
384
384
|
case 'replaceOne': {
|
|
385
|
-
const filter = requireObject(cmd.filter, 'replaceOne.filter');
|
|
386
|
-
const replacement = requireObject(cmd.replacement ?? cmd.update, 'replaceOne.replacement');
|
|
385
|
+
const filter = reviveEjson(requireObject(cmd.filter, 'replaceOne.filter'));
|
|
386
|
+
const replacement = reviveEjson(requireObject(cmd.replacement ?? cmd.update, 'replaceOne.replacement'));
|
|
387
387
|
const r = await c.replaceOne(filter, replacement);
|
|
388
388
|
return {
|
|
389
389
|
affectedRows: r.modifiedCount,
|
|
@@ -394,7 +394,7 @@ export async function createMongoAdapter(conn, opts) {
|
|
|
394
394
|
case 'deleteMany': {
|
|
395
395
|
if (op === 'deleteMany')
|
|
396
396
|
assertExplicitFilter(op, cmd.filter);
|
|
397
|
-
const filter = requireObject(cmd.filter, `${op}.filter`);
|
|
397
|
+
const filter = reviveEjson(requireObject(cmd.filter, `${op}.filter`));
|
|
398
398
|
const r = op === 'deleteOne'
|
|
399
399
|
? await c.deleteOne(filter)
|
|
400
400
|
: await c.deleteMany(filter);
|
|
@@ -526,6 +526,35 @@ function inferBsonType(v) {
|
|
|
526
526
|
return 'object';
|
|
527
527
|
return typeof v;
|
|
528
528
|
}
|
|
529
|
+
/** Mongo Extended JSON($oid/$date)→ BSON 原生类型:前端单元格写回以 JSON 表达 ObjectId/Date,
|
|
530
|
+
* driver 不识别 EJSON 形态,须经此复活。最小集:仅 $oid/$date;其余键原样递归保留。
|
|
531
|
+
* $oid 需 24 位十六进制,非法抛中文可读错误;vendor 缺失时 $oid 原样返回不抛(降级可用)。 */
|
|
532
|
+
const OID_HEX_RE = /^[0-9a-f]{24}$/i;
|
|
533
|
+
export function reviveEjson(v) {
|
|
534
|
+
if (Array.isArray(v))
|
|
535
|
+
return v.map(reviveEjson);
|
|
536
|
+
if (!isPlainObject(v))
|
|
537
|
+
return v;
|
|
538
|
+
const keys = Object.keys(v);
|
|
539
|
+
if (keys.length === 1 && keys[0] === '$oid') {
|
|
540
|
+
const s = v.$oid;
|
|
541
|
+
if (typeof s !== 'string' || !OID_HEX_RE.test(s)) {
|
|
542
|
+
throw new Error(`MongoDB $oid 值非法(${String(s)}):需 24 位十六进制字符串,如 {"$oid":"507f1f77bcf86cd799439011"}`);
|
|
543
|
+
}
|
|
544
|
+
const M = vendorOrNull();
|
|
545
|
+
return M ? new M.ObjectId(s) : v;
|
|
546
|
+
}
|
|
547
|
+
if (keys.length === 1 && keys[0] === '$date') {
|
|
548
|
+
const d = v.$date;
|
|
549
|
+
if (typeof d !== 'string' && typeof d !== 'number')
|
|
550
|
+
return v;
|
|
551
|
+
return new Date(d);
|
|
552
|
+
}
|
|
553
|
+
const out = {};
|
|
554
|
+
for (const [k, val] of Object.entries(v))
|
|
555
|
+
out[k] = reviveEjson(val);
|
|
556
|
+
return out;
|
|
557
|
+
}
|
|
529
558
|
export function buildUri(fields, ssl) {
|
|
530
559
|
const host = typeof fields?.host === 'string' && fields.host !== '' ? fields.host : '127.0.0.1';
|
|
531
560
|
const port = typeof fields?.port === 'number' ? fields.port : 27017;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-db-tool",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
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",
|