dsh-db-tool 1.5.91 → 1.7.0
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 +980 -31
- package/dist/adapters/dmdb/index.js +25 -4
- package/dist/adapters/gaussdb/index.js +2 -0
- package/dist/adapters/mongodb/index.js +47 -8
- package/dist/adapters/mysql/index.js +31 -5
- package/dist/adapters/oracle/index.js +39 -9
- package/dist/adapters/postgresql/index.js +4 -0
- package/dist/adapters/sql-shared/pg-like.js +54 -16
- package/dist/adapters/sqlite/index.js +50 -19
- package/dist/adapters/transfer/kinds/mongo.js +378 -0
- package/dist/adapters/transfer/kinds/mysql.js +331 -0
- package/dist/adapters/transfer/kinds/oralike.js +437 -0
- package/dist/adapters/transfer/kinds/pg.js +311 -0
- package/dist/adapters/transfer/kinds/sqlite.js +250 -0
- package/dist/adapters/transfer/model.js +77 -0
- package/dist/http/index.js +74 -0
- package/dist/manager.js +239 -2
- package/dist/store/connections.js +85 -2
- package/dist/store/export-crypto.js +78 -0
- package/dist/store/index.js +7 -1
- package/dist/store/transfer-history.js +114 -0
- package/dist/transfer.js +287 -0
- package/package.json +1 -1
package/client/client.js
CHANGED
|
@@ -26,6 +26,72 @@ window.__ModuleLoader__.load({
|
|
|
26
26
|
viewGrants: "项目授权",
|
|
27
27
|
viewBrowse: "数据浏览",
|
|
28
28
|
viewConsole: "SQL 控制台",
|
|
29
|
+
viewTransfer: "数据传输",
|
|
30
|
+
viewTransferLog: "传输日志",
|
|
31
|
+
tlogEmpty: "暂无传输日志",
|
|
32
|
+
tlogAuto: "自动刷新",
|
|
33
|
+
tlogClear: "清除日志",
|
|
34
|
+
tlogClearConfirm: "确定清除全部传输日志?",
|
|
35
|
+
tlogTypeStart: "开始",
|
|
36
|
+
tlogTypeTable: "表",
|
|
37
|
+
tlogTypeProgress: "进度",
|
|
38
|
+
tlogTypeFinish: "完成",
|
|
39
|
+
trSource: "源连接",
|
|
40
|
+
trTarget: "目标连接",
|
|
41
|
+
trTables: "数据库对象",
|
|
42
|
+
trGroupTable: "表",
|
|
43
|
+
trGroupCollection: "集合",
|
|
44
|
+
trGroupView: "视图",
|
|
45
|
+
trGroupOther: "其他",
|
|
46
|
+
trSelectAll: "全选",
|
|
47
|
+
trClear: "清空",
|
|
48
|
+
trWriteMode: "写入模式",
|
|
49
|
+
trModeInsert: "插入(冲突报错)",
|
|
50
|
+
trModeIgnore: "跳过重复",
|
|
51
|
+
trModeReplace: "覆盖(按主键)",
|
|
52
|
+
trModeTruncate: "清空后写入(危险)",
|
|
53
|
+
trBatchSize: "每批行数",
|
|
54
|
+
trTableConcurrency: "表级并行",
|
|
55
|
+
trShardConcurrency: "分片并行",
|
|
56
|
+
trStart: "开始传输",
|
|
57
|
+
trCancel: "取消任务",
|
|
58
|
+
trProgress: "传输进度",
|
|
59
|
+
trStatus: "状态",
|
|
60
|
+
trRows: "已写入行数",
|
|
61
|
+
trShards: "分片",
|
|
62
|
+
trFailures: "失败清单",
|
|
63
|
+
trStatusRunning: "进行中",
|
|
64
|
+
trStatusDone: "已完成",
|
|
65
|
+
trStatusFailed: "有失败",
|
|
66
|
+
trStatusCancelled: "已取消",
|
|
67
|
+
trStatusPending: "排队中",
|
|
68
|
+
trxOverwrite: "覆盖已有表结构(删除重建,危险)",
|
|
69
|
+
trxOverwriteHint: "已存在的目标表将被删除重建,其中数据全部丢失",
|
|
70
|
+
trHistory: "传输历史",
|
|
71
|
+
trHistoryClear: "清除历史",
|
|
72
|
+
trHistoryClearConfirm: "确定清除当前项目的全部传输历史?",
|
|
73
|
+
trHistoryEmpty: "暂无历史记录",
|
|
74
|
+
trSameConn: "源与目标上下文相同,请更改库或模式",
|
|
75
|
+
trLoadingTables: "加载表清单…",
|
|
76
|
+
// 数据传输(Navicat 式双栏)
|
|
77
|
+
trxSource: "源",
|
|
78
|
+
trxTarget: "目标",
|
|
79
|
+
trxConn: "连接:",
|
|
80
|
+
trxDatabase: "数据库:",
|
|
81
|
+
trxSchema: "模式:",
|
|
82
|
+
trxInfo: "信息",
|
|
83
|
+
trxConnType: "连接类型",
|
|
84
|
+
trxConnName: "连接名称",
|
|
85
|
+
trxHost: "主机",
|
|
86
|
+
trxPort: "端口",
|
|
87
|
+
trxServerVer: "服务器版本",
|
|
88
|
+
trxNoDbLevel: "默认上下文",
|
|
89
|
+
trxPickSource: "请选择源连接以加载表清单",
|
|
90
|
+
trxSelectedCount: "已选 {n} 项",
|
|
91
|
+
trxOptions: "传输选项",
|
|
92
|
+
trxArrowLabel: "传输方向:源到目标",
|
|
93
|
+
trxPickTable: "请至少勾选一张表",
|
|
94
|
+
trxLoadDbsFail: "库清单加载失败",
|
|
29
95
|
footerHint: "管理在此侧栏;业务操作要求当前项目已获得对应连接的授权。",
|
|
30
96
|
// 连接管理
|
|
31
97
|
newConn: "新建连接",
|
|
@@ -50,6 +116,7 @@ window.__ModuleLoader__.load({
|
|
|
50
116
|
testOk: "连接成功",
|
|
51
117
|
testFail: "连接失败",
|
|
52
118
|
edit: "编辑",
|
|
119
|
+
trRefresh: "刷新(重新拉取已展开的库/模式/表)",
|
|
53
120
|
delete: "删除",
|
|
54
121
|
deleteConnConfirm: "确定删除连接「{name}」?将级联删除其密钥与所有项目的授权。",
|
|
55
122
|
fieldKind: "数据库类型",
|
|
@@ -72,6 +139,16 @@ window.__ModuleLoader__.load({
|
|
|
72
139
|
auditDetail: "详情",
|
|
73
140
|
auditDenied: "拒绝",
|
|
74
141
|
auditConfirmed: "已确认",
|
|
142
|
+
// 连接导出/导入(口令加密备份)
|
|
143
|
+
exportConns: "导出",
|
|
144
|
+
importConns: "导入",
|
|
145
|
+
passphrase: "口令",
|
|
146
|
+
passphraseHint: "8 位以上,仅本次导出/导入使用",
|
|
147
|
+
exportDone: "已导出 {n} 条连接",
|
|
148
|
+
importResult: "已导入 {i} 条,跳过 {s} 条",
|
|
149
|
+
importPartialFail: "部分失败:成功 {i} 条,跳过 {s} 条,失败 {f} 条",
|
|
150
|
+
importDecryptFail: "口令错误或文件已损坏",
|
|
151
|
+
pickFile: "选择文件",
|
|
75
152
|
// 授权
|
|
76
153
|
grantsHint: "为当前项目授权连接。只读=仅查询/浏览,读写=允许执行语句与脚本。未授权的连接在业务操作中一律拒绝。",
|
|
77
154
|
grantsEmptyHint: "当前项目尚未授权任何连接,在下方每行选择「只读」或「读写」即可完成授权。",
|
|
@@ -168,6 +245,72 @@ window.__ModuleLoader__.load({
|
|
|
168
245
|
viewGrants: "Project Access",
|
|
169
246
|
viewBrowse: "Browse",
|
|
170
247
|
viewConsole: "SQL Console",
|
|
248
|
+
viewTransfer: "Data Transfer",
|
|
249
|
+
viewTransferLog: "Transfer Log",
|
|
250
|
+
tlogEmpty: "No transfer log yet",
|
|
251
|
+
tlogAuto: "Auto refresh",
|
|
252
|
+
tlogClear: "Clear log",
|
|
253
|
+
tlogClearConfirm: "Clear all transfer log?",
|
|
254
|
+
tlogTypeStart: "Start",
|
|
255
|
+
tlogTypeTable: "Table",
|
|
256
|
+
tlogTypeProgress: "Progress",
|
|
257
|
+
tlogTypeFinish: "Finish",
|
|
258
|
+
trSource: "Source connection",
|
|
259
|
+
trTarget: "Target connection",
|
|
260
|
+
trTables: "Database objects",
|
|
261
|
+
trGroupTable: "Tables",
|
|
262
|
+
trGroupCollection: "Collections",
|
|
263
|
+
trGroupView: "Views",
|
|
264
|
+
trGroupOther: "Other",
|
|
265
|
+
trSelectAll: "Select all",
|
|
266
|
+
trClear: "Clear",
|
|
267
|
+
trWriteMode: "Write mode",
|
|
268
|
+
trModeInsert: "Insert (error on conflict)",
|
|
269
|
+
trModeIgnore: "Skip duplicates",
|
|
270
|
+
trModeReplace: "Replace (by primary key)",
|
|
271
|
+
trModeTruncate: "Clear then write (dangerous)",
|
|
272
|
+
trBatchSize: "Batch size",
|
|
273
|
+
trTableConcurrency: "Table parallelism",
|
|
274
|
+
trShardConcurrency: "Shard parallelism",
|
|
275
|
+
trStart: "Start transfer",
|
|
276
|
+
trCancel: "Cancel task",
|
|
277
|
+
trProgress: "Transfer progress",
|
|
278
|
+
trStatus: "Status",
|
|
279
|
+
trRows: "Rows written",
|
|
280
|
+
trShards: "Shards",
|
|
281
|
+
trFailures: "Failures",
|
|
282
|
+
trStatusRunning: "Running",
|
|
283
|
+
trStatusDone: "Done",
|
|
284
|
+
trStatusFailed: "Failed",
|
|
285
|
+
trStatusCancelled: "Cancelled",
|
|
286
|
+
trStatusPending: "Pending",
|
|
287
|
+
trxOverwrite: "Overwrite existing table structure (drop & recreate, dangerous)",
|
|
288
|
+
trxOverwriteHint: "Existing target tables will be dropped and recreated; their data will be lost",
|
|
289
|
+
trHistory: "Transfer History",
|
|
290
|
+
trHistoryClear: "Clear history",
|
|
291
|
+
trHistoryClearConfirm: "Clear all transfer history for this project?",
|
|
292
|
+
trHistoryEmpty: "No history yet",
|
|
293
|
+
trSameConn: "Source and target share the same context; pick a different database or schema",
|
|
294
|
+
trLoadingTables: "Loading tables…",
|
|
295
|
+
// Data transfer (Navicat-style two columns)
|
|
296
|
+
trxSource: "Source",
|
|
297
|
+
trxTarget: "Target",
|
|
298
|
+
trxConn: "Connection:",
|
|
299
|
+
trxDatabase: "Database:",
|
|
300
|
+
trxSchema: "Schema:",
|
|
301
|
+
trxInfo: "Information",
|
|
302
|
+
trxConnType: "Type",
|
|
303
|
+
trxConnName: "Name",
|
|
304
|
+
trxHost: "Host",
|
|
305
|
+
trxPort: "Port",
|
|
306
|
+
trxServerVer: "Server version",
|
|
307
|
+
trxNoDbLevel: "Default context",
|
|
308
|
+
trxPickSource: "Pick a source connection to load tables",
|
|
309
|
+
trxSelectedCount: "{n} selected",
|
|
310
|
+
trxOptions: "Transfer options",
|
|
311
|
+
trxArrowLabel: "Transfer direction: source to target",
|
|
312
|
+
trxPickTable: "Select at least one table",
|
|
313
|
+
trxLoadDbsFail: "Failed to load databases",
|
|
171
314
|
footerHint: "Manage here in the sidebar; business operations require a project grant on the connection.",
|
|
172
315
|
newConn: "New Connection",
|
|
173
316
|
editConn: "Edit Connection",
|
|
@@ -191,6 +334,7 @@ window.__ModuleLoader__.load({
|
|
|
191
334
|
testOk: "Connection OK",
|
|
192
335
|
testFail: "Connection failed",
|
|
193
336
|
edit: "Edit",
|
|
337
|
+
trRefresh: "Refresh (reload expanded databases/schemas/tables)",
|
|
194
338
|
delete: "Delete",
|
|
195
339
|
deleteConnConfirm: "Delete connection \"{name}\"? Its secrets and all project grants will be removed.",
|
|
196
340
|
fieldKind: "Database type",
|
|
@@ -213,6 +357,15 @@ window.__ModuleLoader__.load({
|
|
|
213
357
|
auditDetail: "Detail",
|
|
214
358
|
auditDenied: "denied",
|
|
215
359
|
auditConfirmed: "confirmed",
|
|
360
|
+
exportConns: "Export",
|
|
361
|
+
importConns: "Import",
|
|
362
|
+
passphrase: "Passphrase",
|
|
363
|
+
passphraseHint: "8+ characters, used only for this export/import",
|
|
364
|
+
exportDone: "Exported {n} connection(s)",
|
|
365
|
+
importResult: "Imported {i}, skipped {s}",
|
|
366
|
+
importPartialFail: "Partial failure: {i} imported, {s} skipped, {f} failed",
|
|
367
|
+
importDecryptFail: "Wrong passphrase or corrupted file",
|
|
368
|
+
pickFile: "Pick file",
|
|
216
369
|
grantsHint: "Grant connections to the current project. ro = read-only (query/browse), rw = read-write (execute/script allowed). Unauthorized connections are always rejected.",
|
|
217
370
|
grantsEmptyHint: "No connections granted to this project yet. Pick read-only or read-write on a row below to grant.",
|
|
218
371
|
grantsSummary: "{granted}/{total} connections granted",
|
|
@@ -390,6 +543,7 @@ window.__ModuleLoader__.load({
|
|
|
390
543
|
".dbt-browse-root{color:var(--dbt-text);}",
|
|
391
544
|
"/* ===== 根容器 ===== */",
|
|
392
545
|
".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;}",
|
|
546
|
+
".dbt-panel input[type=\"checkbox\"]{width:16px;height:16px;flex:none;margin:0;accent-color:var(--dbt-accent);}",
|
|
393
547
|
"/* ===== iOS segmented control(顶部 tab) ===== */",
|
|
394
548
|
".dbt-tabs{display:flex;gap:2px;background:var(--dbt-surface);border-radius:var(--dbt-radius-ctrl);padding:2px;}",
|
|
395
549
|
".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:background .18s var(--dbt-ease),box-shadow .18s var(--dbt-ease);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}",
|
|
@@ -470,6 +624,31 @@ window.__ModuleLoader__.load({
|
|
|
470
624
|
".dbt-browse-skel{display:flex;flex-direction:column;gap:8px;}",
|
|
471
625
|
".dbt-browse-skelrow{display:flex;gap:8px;}",
|
|
472
626
|
".dbt-browse-skelcell{flex:1;height:24px;background:var(--dbt-surface-strong);border-radius:var(--dbt-radius-pill);}",
|
|
627
|
+
"/* ===== 数据传输(Navicat 式双栏:摘要条 + 源/目标栏 + 信息区) ===== */",
|
|
628
|
+
".dbt-transfer-summary{display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap;}",
|
|
629
|
+
".dbt-transfer-endpoint{display:flex;align-items:baseline;gap:4px;min-width:0;}",
|
|
630
|
+
".dbt-transfer-endpoint strong,.dbt-transfer-endpoint span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:150px;}",
|
|
631
|
+
".dbt-transfer-cols{display:flex;gap:8px;align-items:stretch;}",
|
|
632
|
+
".dbt-transfer-col{flex:1 1 0;min-width:0;display:flex;flex-direction:column;gap:6px;}",
|
|
633
|
+
".dbt-transfer-coltitle,.dbt-transfer-infotitle{font-size:var(--dbt-caption,11px);font-weight:600;color:var(--dbt-accent);}",
|
|
634
|
+
".dbt-transfer-field{display:flex;flex-direction:column;gap:2px;min-width:0;}",
|
|
635
|
+
".dbt-transfer-fieldlabel{font-size:var(--dbt-caption-xs,10px);color:var(--dbt-text-secondary);}",
|
|
636
|
+
".dbt-transfer-divider{flex:none;align-self:center;color:var(--dbt-text-secondary);}",
|
|
637
|
+
".dbt-transfer-info{border-top:1px solid var(--dbt-separator);padding-top:6px;display:flex;flex-direction:column;gap:3px;}",
|
|
638
|
+
".dbt-transfer-kv{display:grid;grid-template-columns:auto 1fr;gap:2px 8px;margin:0;font-size:var(--dbt-caption-xs,10px);}",
|
|
639
|
+
".dbt-transfer-kv dt{color:var(--dbt-text-secondary);}",
|
|
640
|
+
".dbt-transfer-kv dd{margin:0;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
641
|
+
".dbt-transfer-tablelist{max-height:220px;overflow:auto;border:1px solid var(--dbt-separator);border-radius:var(--dbt-radius-ctrl,6px);padding:3px 0;font-size:12px;background:var(--dbt-surface-strong);}",
|
|
642
|
+
".dbt-tr-group-head{display:flex;align-items:center;gap:6px;padding:3px 8px;min-height:26px;font-weight:600;cursor:pointer;user-select:none;}",
|
|
643
|
+
".dbt-tr-group-head:hover{background:var(--dbt-surface);}",
|
|
644
|
+
".dbt-tr-caret{display:inline-block;width:0;height:0;border-left:4px solid currentColor;border-top:3px solid transparent;border-bottom:3px solid transparent;opacity:.55;transform:rotate(0deg);transition:transform .12s var(--dbt-ease);flex:none;}",
|
|
645
|
+
".dbt-tr-caret.open{transform:rotate(90deg);}",
|
|
646
|
+
".dbt-tr-item{display:flex;align-items:center;padding:1px 8px 1px 26px;min-height:24px;}",
|
|
647
|
+
".dbt-tr-item:hover{background:var(--dbt-surface);}",
|
|
648
|
+
".dbt-tr-pair{display:flex;align-items:center;gap:6px;cursor:pointer;min-width:0;}",
|
|
649
|
+
".dbt-tr-pair span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
650
|
+
".dbt-tr-comment{color:var(--dbt-text-secondary);font-size:11px;font-weight:400;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1 1 auto;min-width:0;}",
|
|
651
|
+
".dbt-transfer-options>summary{cursor:pointer;font-size:var(--dbt-caption,11px);color:var(--dbt-text-secondary);padding:4px 0;}",
|
|
473
652
|
"/* ===== 浏览面板:单元格按钮与详情底栏(Apple 分区卡) ===== */",
|
|
474
653
|
".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;}",
|
|
475
654
|
".dbt-cellbtn:focus-visible{outline:2px solid var(--dbt-accent);outline-offset:-1px;border-radius:2px;}",
|
|
@@ -596,6 +775,74 @@ window.__ModuleLoader__.load({
|
|
|
596
775
|
/* ---------------- 连接管理 ---------------- */
|
|
597
776
|
const EMPTY_FORM = { id: "", kind: "mysql", name: "", mode: "url", url: "", urlUser: "", urlPassword: "", host: "", port: "", user: "", password: "", database: "", ssl: false };
|
|
598
777
|
|
|
778
|
+
// 口令弹窗(ManageView 局部复用,导出/导入共用):键盘契约对齐 DangerDialog
|
|
779
|
+
// (Escape=取消、Tab 在对话框内循环锁定);差异:初始焦点在口令输入框,确认按钮主色。
|
|
780
|
+
// ponytail: 仅两处使用,故为 ManageView 内部局部组件,不抽通用模块。
|
|
781
|
+
function PassphraseDialog(props) {
|
|
782
|
+
const [value, setValue] = React.useState("");
|
|
783
|
+
const [err, setErr] = React.useState(""); // 就地错误(如口令过短);读屏经 role=alert 播报
|
|
784
|
+
const inputRef = React.useRef(null);
|
|
785
|
+
const dialogRef = React.useRef(null);
|
|
786
|
+
const titleId = "dbt-pass-title";
|
|
787
|
+
function done(v) { props.onClose(v); }
|
|
788
|
+
function submit() {
|
|
789
|
+
const v = value.trim();
|
|
790
|
+
if (v.length < 8) { setErr(t("passphraseHint")); return; } // 与服务端 PASSPHRASE_MIN=8 对齐(服务端为权威边界)
|
|
791
|
+
done(v);
|
|
792
|
+
}
|
|
793
|
+
function onKeyDown(e) {
|
|
794
|
+
if (e.key === "Escape") { e.preventDefault(); done(null); return; }
|
|
795
|
+
if (e.key === "Enter" && e.target === inputRef.current) { e.preventDefault(); submit(); return; }
|
|
796
|
+
if (e.key !== "Tab") return;
|
|
797
|
+
const dialog = dialogRef.current;
|
|
798
|
+
if (!dialog) return;
|
|
799
|
+
const els = dialog.querySelectorAll("input,button");
|
|
800
|
+
if (els.length === 0) return;
|
|
801
|
+
const first = els[0];
|
|
802
|
+
const last = els[els.length - 1];
|
|
803
|
+
const doc = dialog.ownerDocument || document;
|
|
804
|
+
if (!dialog.contains(doc.activeElement)) { e.preventDefault(); (e.shiftKey ? last : first).focus(); return; }
|
|
805
|
+
if (!e.shiftKey && doc.activeElement === last) { e.preventDefault(); first.focus(); }
|
|
806
|
+
else if (e.shiftKey && doc.activeElement === first) { e.preventDefault(); last.focus(); }
|
|
807
|
+
}
|
|
808
|
+
// 挂载后焦点落口令输入框(与 DangerDialog 落「取消」不同:本弹窗无危险语义,输入即主路径)
|
|
809
|
+
React.useEffect(() => {
|
|
810
|
+
if (inputRef.current && typeof inputRef.current.focus === "function") inputRef.current.focus();
|
|
811
|
+
}, []);
|
|
812
|
+
return React.createElement(
|
|
813
|
+
"div",
|
|
814
|
+
{ className: "dbt-overlay", onClick: () => done(null), onKeyDown },
|
|
815
|
+
React.createElement(
|
|
816
|
+
"div",
|
|
817
|
+
{
|
|
818
|
+
className: "dbt-dialog",
|
|
819
|
+
onClick: (e) => e.stopPropagation(),
|
|
820
|
+
role: "dialog",
|
|
821
|
+
"aria-modal": "true",
|
|
822
|
+
"aria-labelledby": titleId,
|
|
823
|
+
ref: dialogRef,
|
|
824
|
+
},
|
|
825
|
+
React.createElement("strong", { id: titleId }, props.title),
|
|
826
|
+
React.createElement("input", {
|
|
827
|
+
type: "password",
|
|
828
|
+
"aria-label": t("passphrase"),
|
|
829
|
+
placeholder: t("passphrase"),
|
|
830
|
+
autoComplete: "off",
|
|
831
|
+
value,
|
|
832
|
+
ref: inputRef,
|
|
833
|
+
onChange: (e) => { setValue(e.target.value); if (err) setErr(""); },
|
|
834
|
+
}),
|
|
835
|
+
React.createElement("div", { className: "dbt-muted" }, t("passphraseHint")),
|
|
836
|
+
err ? React.createElement("div", { className: "dbt-err", role: "alert" }, err) : null,
|
|
837
|
+
React.createElement(
|
|
838
|
+
"div",
|
|
839
|
+
{ className: "dbt-row" },
|
|
840
|
+
React.createElement("button", { className: "dbt-btn primary", onClick: submit }, props.confirmLabel),
|
|
841
|
+
React.createElement("button", { className: "dbt-btn", onClick: () => done(null) }, t("cancel")),
|
|
842
|
+
),
|
|
843
|
+
),
|
|
844
|
+
);
|
|
845
|
+
}
|
|
599
846
|
// 通用表格渲染:columns + rows(审计列表与查询结果共用);wrapClass 可选(浏览弹窗用自适应高度容器)
|
|
600
847
|
function resultTable(columns, rows, cellTitles, wrapClass) {
|
|
601
848
|
return React.createElement(
|
|
@@ -1107,6 +1354,11 @@ window.__ModuleLoader__.load({
|
|
|
1107
1354
|
const [auditOpen, setAuditOpen] = React.useState(false);
|
|
1108
1355
|
const [audit, setAudit] = React.useState(null);
|
|
1109
1356
|
const [testInfo, setTestInfo] = React.useState({}); // connId -> "ok" | "fail: msg"
|
|
1357
|
+
// 导出/导入流程态:passReq 非空即弹口令窗("export" | "import");fileRef 承载隐藏文件选择器
|
|
1358
|
+
const [passReq, setPassReq] = React.useState(null);
|
|
1359
|
+
const [pendingJson, setPendingJson] = React.useState(null); // 待导入的密文文本(选文件后暂存,口令确认后发送)
|
|
1360
|
+
const [ioInfo, setIoInfo] = React.useState(null); // null | {kind: "ok"|"err", text}
|
|
1361
|
+
const fileRef = React.useRef(null);
|
|
1110
1362
|
// 键盘焦点态:统一走模块级 focusRingProps()(描边同 DESIGN.md focus 规范,accent 2px 内缩)
|
|
1111
1363
|
|
|
1112
1364
|
async function saveConn(body, reset) {
|
|
@@ -1156,6 +1408,64 @@ window.__ModuleLoader__.load({
|
|
|
1156
1408
|
}
|
|
1157
1409
|
}
|
|
1158
1410
|
|
|
1411
|
+
// 口令弹窗确认回调:按 passReq 分流导出/导入;value 为 null 即取消
|
|
1412
|
+
async function onPassphrase(value) {
|
|
1413
|
+
const mode = passReq;
|
|
1414
|
+
setPassReq(null);
|
|
1415
|
+
if (value === null || value === undefined) { setPendingJson(null); return; } // 取消即清密文
|
|
1416
|
+
if (mode === "export") await doExport(value);
|
|
1417
|
+
else if (mode === "import") await doImport(value);
|
|
1418
|
+
setPendingJson(null); // 完成即清密文,避免敏感数据滞留组件状态
|
|
1419
|
+
}
|
|
1420
|
+
// 导出:POST /connections/export 拿密文 json → Blob + a[download] 落盘(文件名带日期戳)
|
|
1421
|
+
async function doExport(passphrase) {
|
|
1422
|
+
await props.run("export", async () => {
|
|
1423
|
+
const data = await api("connections/export", { method: "POST", body: { passphrase } });
|
|
1424
|
+
const json = data && data.json;
|
|
1425
|
+
const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, ""); // YYYYMMDD
|
|
1426
|
+
const blob = new Blob([typeof json === "string" ? json : JSON.stringify(json, null, 2)], { type: "application/json" });
|
|
1427
|
+
const url = URL.createObjectURL(blob);
|
|
1428
|
+
const a = document.createElement("a");
|
|
1429
|
+
a.href = url;
|
|
1430
|
+
a.download = "dsh-db-connections-" + stamp + ".enc.json";
|
|
1431
|
+
document.body.appendChild(a);
|
|
1432
|
+
a.click();
|
|
1433
|
+
a.remove();
|
|
1434
|
+
// 延迟回收:部分浏览器在 click 后才异步 fetch blob,同步 revoke 可能中断下载(与结果导出 download 同模式)
|
|
1435
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
1436
|
+
// 服务端回传权威条数 count;缺失时回退本地列表(不声称精确数字)
|
|
1437
|
+
const n = (data && typeof data.count === "number") ? data.count : conns.length;
|
|
1438
|
+
setIoInfo({ kind: "ok", text: t("exportDone", { n }) });
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
// 导入:密文已在 pendingJson;口令错(INVALID_ARGUMENT + 解密失败文案)给专用提示,其余透传后端 message
|
|
1442
|
+
async function doImport(passphrase) {
|
|
1443
|
+
const json = pendingJson;
|
|
1444
|
+
await props.run("import", async () => {
|
|
1445
|
+
const data = await api("connections/import", { method: "POST", body: { json, passphrase } });
|
|
1446
|
+
await reload();
|
|
1447
|
+
const failed = (data && data.errors ? data.errors.length : 0);
|
|
1448
|
+
const i = (data && data.imported) || 0;
|
|
1449
|
+
const s = (data && data.skipped ? data.skipped.length : 0);
|
|
1450
|
+
setIoInfo(failed > 0
|
|
1451
|
+
? { kind: "err", text: t("importPartialFail", { i, s, f: failed }) }
|
|
1452
|
+
: { kind: "ok", text: t("importResult", { i, s }) });
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
// 隐藏文件选择器回调:FileReader 读文本后暂存,再弹口令窗;读失败走通用错误通道
|
|
1456
|
+
function onPickFile(e) {
|
|
1457
|
+
const file = e.target.files && e.target.files[0];
|
|
1458
|
+
e.target.value = ""; // 允许重复选同一文件
|
|
1459
|
+
if (!file) return;
|
|
1460
|
+
const reader = new FileReader();
|
|
1461
|
+
reader.onload = () => {
|
|
1462
|
+
setPendingJson(String(reader.result || ""));
|
|
1463
|
+
setPassReq("import");
|
|
1464
|
+
};
|
|
1465
|
+
reader.onerror = () => props.onError(reader.error || new Error(t("loadFailed")));
|
|
1466
|
+
reader.readAsText(file);
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1159
1469
|
if (editing) {
|
|
1160
1470
|
return React.createElement(ConnForm, {
|
|
1161
1471
|
initial: editing === "new" ? null : buildEditForm(editing),
|
|
@@ -1167,13 +1477,23 @@ window.__ModuleLoader__.load({
|
|
|
1167
1477
|
return React.createElement(
|
|
1168
1478
|
"div",
|
|
1169
1479
|
{ style: { display: "flex", flexDirection: "column", gap: 8 } },
|
|
1170
|
-
//
|
|
1480
|
+
// 顶部操作行:审计 + 导出/导入在左,「新建连接」主按钮置右侧
|
|
1171
1481
|
React.createElement(
|
|
1172
1482
|
"div",
|
|
1173
1483
|
{ className: "dbt-row", style: { justifyContent: "space-between" } },
|
|
1174
|
-
React.createElement(
|
|
1484
|
+
React.createElement(
|
|
1485
|
+
"div",
|
|
1486
|
+
{ className: "dbt-row" },
|
|
1487
|
+
React.createElement("button", { className: "dbt-btn", "aria-expanded": auditOpen, ...focusRingProps(), onClick: loadAudit }, t("auditShort")),
|
|
1488
|
+
React.createElement("button", { className: "dbt-btn", disabled: busy === "export", ...focusRingProps(), onClick: () => setPassReq("export") }, t("exportConns")),
|
|
1489
|
+
React.createElement("button", { className: "dbt-btn", disabled: busy === "import", ...focusRingProps(), onClick: () => { if (fileRef.current) fileRef.current.click(); } }, t("importConns")),
|
|
1490
|
+
),
|
|
1175
1491
|
React.createElement("button", { className: "dbt-btn primary", ...focusRingProps(), onClick: () => setEditing("new") }, t("newConn")),
|
|
1176
1492
|
),
|
|
1493
|
+
// 隐藏文件选择器:仅导入用,accept 限定 .json
|
|
1494
|
+
React.createElement("input", { type: "file", accept: ".json", "aria-label": t("pickFile"), style: { display: "none" }, ref: fileRef, onChange: onPickFile }),
|
|
1495
|
+
// 导出/导入就地反馈(与测试反馈同语言:dbt-msg 绿 / dbt-err 红)
|
|
1496
|
+
ioInfo ? React.createElement("div", { role: ioInfo.kind === "ok" ? "status" : "alert", className: ioInfo.kind === "ok" ? "dbt-msg" : "dbt-err" }, ioInfo.text) : null,
|
|
1177
1497
|
conns.length === 0
|
|
1178
1498
|
? // 空状态引导:一句引导 + 主按钮直达新增表单(state 切换在 ManageView 内)
|
|
1179
1499
|
React.createElement(
|
|
@@ -1231,6 +1551,12 @@ window.__ModuleLoader__.load({
|
|
|
1231
1551
|
auditTable(audit),
|
|
1232
1552
|
)
|
|
1233
1553
|
: null,
|
|
1554
|
+
// 口令弹窗:导出/导入共用;取消(null)不动作
|
|
1555
|
+
passReq ? React.createElement(PassphraseDialog, {
|
|
1556
|
+
title: passReq === "export" ? t("exportConns") : t("importConns"),
|
|
1557
|
+
confirmLabel: passReq === "export" ? t("exportConns") : t("importConns"),
|
|
1558
|
+
onClose: onPassphrase,
|
|
1559
|
+
}) : null,
|
|
1234
1560
|
);
|
|
1235
1561
|
}
|
|
1236
1562
|
|
|
@@ -1319,7 +1645,7 @@ window.__ModuleLoader__.load({
|
|
|
1319
1645
|
/* ---------------- 数据浏览 ---------------- */
|
|
1320
1646
|
// Navicat 式对象树:连接 ▸ 库/[schema] ▸ 表,懒加载展开(浏览面板唯一树实例,展开状态随面板生命周期)
|
|
1321
1647
|
function BrowseTree(props) {
|
|
1322
|
-
const { conns, projectPath, sel, onSelect, autoExpand } = props;
|
|
1648
|
+
const { conns, projectPath, sel, onSelect, autoExpand, refreshTick } = props;
|
|
1323
1649
|
// sel 由父组件持有(弹窗内 active 高亮与初始选中都依赖);onSelect(选中记录, 触发元素) 上报
|
|
1324
1650
|
const [open, setOpen] = React.useState({}); // "c:<id>" | "d:<id>/<db>" -> bool
|
|
1325
1651
|
const [loading, setLoading] = React.useState({});
|
|
@@ -1333,6 +1659,50 @@ window.__ModuleLoader__.load({
|
|
|
1333
1659
|
setOpen({}); setLoading({}); setError({}); setDbs({}); setSchemasMap({}); setTablesMap({});
|
|
1334
1660
|
}, [projectPath]);
|
|
1335
1661
|
|
|
1662
|
+
// 三层加载统一入口(toggle 展开与手动/自动刷新共用;key: c:<id> | d:<id>/<db> | s:<id>/<db>/<schema>)
|
|
1663
|
+
function loadByKey(key) {
|
|
1664
|
+
const cm = /^c:(.+)$/.exec(key);
|
|
1665
|
+
const dm = /^d:(.+?)\/(.+)$/.exec(key);
|
|
1666
|
+
const sm = /^s:(.+?)\/(.+?)\/(.+)$/.exec(key);
|
|
1667
|
+
const connOf = (id) => conns.find((x) => x.id === id);
|
|
1668
|
+
if (cm) {
|
|
1669
|
+
const c = connOf(cm[1]);
|
|
1670
|
+
if (!c) return Promise.resolve();
|
|
1671
|
+
return api("databases" + qs({ project: projectPath, connId: c.id })).then((list) => {
|
|
1672
|
+
setDbs((m) => Object.assign({}, m, { [c.id]: list || [] }));
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
if (dm) {
|
|
1676
|
+
const c = connOf(dm[1]);
|
|
1677
|
+
if (!c) return Promise.resolve();
|
|
1678
|
+
const useSchemas = !!HAS_SCHEMAS[c.kind];
|
|
1679
|
+
const what = useSchemas ? "schemas" : "tables";
|
|
1680
|
+
return api(what + qs({ project: projectPath, connId: c.id, database: dm[2] })).then((list) => {
|
|
1681
|
+
const setter = useSchemas ? setSchemasMap : setTablesMap;
|
|
1682
|
+
setter((m) => Object.assign({}, m, { [c.id + "/" + dm[2]]: list || [] }));
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
if (sm) {
|
|
1686
|
+
return api("tables" + qs({ project: projectPath, connId: sm[1], database: sm[2] + "." + sm[3] })).then((list) => {
|
|
1687
|
+
setTablesMap((m) => Object.assign({}, m, { [sm[1] + "/" + sm[2] + "/" + sm[3]]: list || [] }));
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
return Promise.resolve();
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// 手动刷新按钮 / 侧边栏切回可见:对所有已展开节点重新拉取(新库/新表即时可见),展开态保持
|
|
1694
|
+
React.useEffect(() => {
|
|
1695
|
+
if (!refreshTick) return;
|
|
1696
|
+
for (const [key, isOpen] of Object.entries(open)) {
|
|
1697
|
+
if (!isOpen || loading[key]) continue;
|
|
1698
|
+
setError((s) => Object.assign({}, s, { [key]: undefined }));
|
|
1699
|
+
setLoading((s) => Object.assign({}, s, { [key]: true }));
|
|
1700
|
+
loadByKey(key)
|
|
1701
|
+
.catch((e) => setError((s) => Object.assign({}, s, { [key]: e && e.message ? e.message : String(e) })))
|
|
1702
|
+
.finally(() => setLoading((s) => Object.assign({}, s, { [key]: false })));
|
|
1703
|
+
}
|
|
1704
|
+
}, [refreshTick]); // eslint-disable-line
|
|
1705
|
+
|
|
1336
1706
|
function toggle(key, load) {
|
|
1337
1707
|
const isOpen = !!open[key];
|
|
1338
1708
|
setOpen((o) => Object.assign({}, o, { [key]: !isOpen }));
|
|
@@ -1351,10 +1721,7 @@ window.__ModuleLoader__.load({
|
|
|
1351
1721
|
}
|
|
1352
1722
|
function toggleConn(c) {
|
|
1353
1723
|
if (!projectPath) return;
|
|
1354
|
-
toggle("c:" + c.id, () =>
|
|
1355
|
-
api("databases" + qs({ project: projectPath, connId: c.id })).then((list) => {
|
|
1356
|
-
setDbs((m) => Object.assign({}, m, { [c.id]: list || [] }));
|
|
1357
|
-
}));
|
|
1724
|
+
toggle("c:" + c.id, () => loadByKey("c:" + c.id));
|
|
1358
1725
|
}
|
|
1359
1726
|
// autoExpand:挂载后自动展开第一层级(连接节点),复用 toggleConn 的展开+懒加载;
|
|
1360
1727
|
// 加载失败仍走既有就地错误行机制。声明在清缓存 effect 之后:项目切换时先清 open 再重新展开。
|
|
@@ -1369,21 +1736,12 @@ window.__ModuleLoader__.load({
|
|
|
1369
1736
|
const HAS_SCHEMAS = { postgresql: true, gaussdb: true };
|
|
1370
1737
|
function toggleDb(c, d) {
|
|
1371
1738
|
if (!projectPath) return;
|
|
1372
|
-
|
|
1373
|
-
const what = useSchemas ? "schemas" : "tables";
|
|
1374
|
-
toggle("d:" + c.id + "/" + d, () =>
|
|
1375
|
-
api(what + qs({ project: projectPath, connId: c.id, database: d })).then((list) => {
|
|
1376
|
-
const setter = useSchemas ? setSchemasMap : setTablesMap;
|
|
1377
|
-
setter((m) => Object.assign({}, m, { [c.id + "/" + d]: list || [] }));
|
|
1378
|
-
}));
|
|
1739
|
+
toggle("d:" + c.id + "/" + d, () => loadByKey("d:" + c.id + "/" + d));
|
|
1379
1740
|
}
|
|
1380
1741
|
function toggleSchema(c, d, s) {
|
|
1381
1742
|
if (!projectPath) return;
|
|
1382
1743
|
// PG 系跨库浏览:tables 的 database 传 "库名.schema"(Navicat 官方行为,服务端按库开连接)
|
|
1383
|
-
toggle("s:" + c.id + "/" + d + "/" + s, () =>
|
|
1384
|
-
api("tables" + qs({ project: projectPath, connId: c.id, database: d + "." + s })).then((list) => {
|
|
1385
|
-
setTablesMap((m) => Object.assign({}, m, { [c.id + "/" + d + "/" + s]: list || [] }));
|
|
1386
|
-
}));
|
|
1744
|
+
toggle("s:" + c.id + "/" + d + "/" + s, () => loadByKey("s:" + c.id + "/" + d + "/" + s));
|
|
1387
1745
|
}
|
|
1388
1746
|
// 树键盘导航(WAI-ARIA treeview / roving tabindex):Tab 仅进停靠行,方向键在可见行间移动;
|
|
1389
1747
|
// 右箭头展开、左箭头收起或回到父级(父级 = DOM 序向上第一个更浅层级行),Home/End 跳首尾
|
|
@@ -1702,7 +2060,7 @@ window.__ModuleLoader__.load({
|
|
|
1702
2060
|
"label",
|
|
1703
2061
|
// 不用 .dbt-row:全局 input flex:1 会把 checkbox 拉伸占满、标签被推到远端(视觉审查命中)
|
|
1704
2062
|
{ style: { display: "flex", alignItems: "center", gap: 6, flexWrap: "nowrap", width: "fit-content" } },
|
|
1705
|
-
React.createElement("input", { type: "checkbox",
|
|
2063
|
+
React.createElement("input", { type: "checkbox", checked: editNull, onChange: (e) => setEditNull(e.target.checked) }),
|
|
1706
2064
|
t("setNull"),
|
|
1707
2065
|
) : null,
|
|
1708
2066
|
truncHint && truncHint !== selReason ? React.createElement("span", { className: "dbt-readhint" }, t(truncHint)) : null,
|
|
@@ -1828,6 +2186,7 @@ window.__ModuleLoader__.load({
|
|
|
1828
2186
|
React.createElement(BrowseTree, {
|
|
1829
2187
|
conns, projectPath, sel,
|
|
1830
2188
|
autoExpand: !!active, // 首次激活才自动展开连接层级(常驻挂载下隐藏不发请求)
|
|
2189
|
+
refreshTick: props.refreshTick, // 手动刷新按钮 / 侧边栏切回可见时重拉已展开层
|
|
1831
2190
|
onSelect: (s) => setSel(s), // 树内切表
|
|
1832
2191
|
}),
|
|
1833
2192
|
),
|
|
@@ -2450,6 +2809,579 @@ window.__ModuleLoader__.load({
|
|
|
2450
2809
|
);
|
|
2451
2810
|
}
|
|
2452
2811
|
|
|
2812
|
+
/* ---------------- 数据传输(Navicat 式双栏:摘要条 + 源/目标层级联动 + 信息区 + 表清单 + 进度轮询) ---------------- */
|
|
2813
|
+
// 双向箭头(摘要条与栏间分隔共用;stroke currentColor 随容器变色)
|
|
2814
|
+
function transferArrow() {
|
|
2815
|
+
return React.createElement("svg", { width: 18, height: 18, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.8", "aria-hidden": true },
|
|
2816
|
+
React.createElement("path", { d: "M4 8.5h15M15.5 4.5L19 8.5l-3.5 4" }),
|
|
2817
|
+
React.createElement("path", { d: "M20 15.5H5M8.5 11.5L5 15.5l3.5 4" }));
|
|
2818
|
+
}
|
|
2819
|
+
// 信息区服务器版本探测:300ms 防抖 + 按 connId 缓存(组件卸载丢弃)+ 失败静默为空串 + 切走后只写缓存不改显示
|
|
2820
|
+
function useServerVer(connId, cacheRef) {
|
|
2821
|
+
const [ver, setVer] = React.useState("");
|
|
2822
|
+
const liveRef = React.useRef(connId); // 乱序护栏:慢响应回来时只服务当前选中连接
|
|
2823
|
+
React.useEffect(() => {
|
|
2824
|
+
liveRef.current = connId;
|
|
2825
|
+
if (!connId) { setVer(""); return undefined; }
|
|
2826
|
+
const cached = cacheRef.current[connId];
|
|
2827
|
+
if (cached !== undefined) { setVer(cached); return undefined; }
|
|
2828
|
+
const asked = connId;
|
|
2829
|
+
const timer = window.setTimeout(() => {
|
|
2830
|
+
api("connections/" + encodeURIComponent(asked) + "/test", { method: "POST", body: {} })
|
|
2831
|
+
.then((r) => {
|
|
2832
|
+
const info = r && r.ok && r.serverInfo ? String(r.serverInfo) : "";
|
|
2833
|
+
cacheRef.current[asked] = info; // 失败也缓存空串,会话内不反复探测
|
|
2834
|
+
if (liveRef.current === asked) setVer(info);
|
|
2835
|
+
})
|
|
2836
|
+
.catch(() => { cacheRef.current[asked] = ""; });
|
|
2837
|
+
}, 300);
|
|
2838
|
+
return () => window.clearTimeout(timer);
|
|
2839
|
+
}, [connId]);
|
|
2840
|
+
return ver;
|
|
2841
|
+
}
|
|
2842
|
+
function TransferView(props) {
|
|
2843
|
+
const conns = props.conns || [];
|
|
2844
|
+
const projectPath = props.projectPath;
|
|
2845
|
+
const askConfirm = props.askConfirm;
|
|
2846
|
+
const visible = props.visible; // Panel 传入:视图隐藏时停轮询
|
|
2847
|
+
const [srcId, setSrcId] = React.useState("");
|
|
2848
|
+
const [dstId, setDstId] = React.useState("");
|
|
2849
|
+
// 层级状态(BrowsePane 同语义:database=对象树第一层,schema=第二层仅 pg 系;""=连接默认上下文)
|
|
2850
|
+
const [srcDb, setSrcDb] = React.useState("");
|
|
2851
|
+
const [srcSchema, setSrcSchema] = React.useState("");
|
|
2852
|
+
const [dstDb, setDstDb] = React.useState("");
|
|
2853
|
+
const [dstSchema, setDstSchema] = React.useState("");
|
|
2854
|
+
const [srcDbs, setSrcDbs] = React.useState(null); // null=未加载
|
|
2855
|
+
const [srcSchemas, setSrcSchemas] = React.useState(null);
|
|
2856
|
+
const [dstDbs, setDstDbs] = React.useState(null);
|
|
2857
|
+
const [dstSchemas, setDstSchemas] = React.useState(null);
|
|
2858
|
+
const [dbsErr, setDbsErr] = React.useState({ src: "", dst: "" }); // 库清单加载失败就地显示(不弹全局)
|
|
2859
|
+
const [srcTables, setSrcTables] = React.useState([]); // {name,type}[] 可传输对象(表/集合)
|
|
2860
|
+
const [loadingTables, setLoadingTables] = React.useState(false);
|
|
2861
|
+
const [tablesErr, setTablesErr] = React.useState("");
|
|
2862
|
+
const [tablesTick, setTablesTick] = React.useState(0); // 表清单就地重试
|
|
2863
|
+
const [selected, setSelected] = React.useState([]); // 已勾选表名
|
|
2864
|
+
const [writeMode, setWriteMode] = React.useState("insert");
|
|
2865
|
+
const [batchSize, setBatchSize] = React.useState(1000);
|
|
2866
|
+
const [tableConcurrency, setTableConcurrency] = React.useState(4);
|
|
2867
|
+
const [shardConcurrency, setShardConcurrency] = React.useState(4);
|
|
2868
|
+
const [overwriteStructure, setOverwriteStructure] = React.useState(false);
|
|
2869
|
+
const [subTab, setSubTab] = React.useState("history"); // history=传输历史 | tlog=传输日志(同级子标签)
|
|
2870
|
+
const [liveTasks, setLiveTasks] = React.useState(null); // 运行中任务快照(2s 轮询)
|
|
2871
|
+
// 实时进度轮询:标签可见即轮(不依赖本视图发起的任务,重启/外部发起的传输也可见)
|
|
2872
|
+
React.useEffect(() => {
|
|
2873
|
+
const loadLive = () => {
|
|
2874
|
+
api("transfer/live" + qs({ project: projectPath }))
|
|
2875
|
+
.then((list) => setLiveTasks(Array.isArray(list) ? list : []))
|
|
2876
|
+
.catch(() => setLiveTasks([]));
|
|
2877
|
+
};
|
|
2878
|
+
loadLive();
|
|
2879
|
+
if (!visible) return undefined;
|
|
2880
|
+
const timer = setInterval(loadLive, 2000);
|
|
2881
|
+
return () => clearInterval(timer);
|
|
2882
|
+
}, [visible, projectPath]);
|
|
2883
|
+
const pct = (rows, total) => (total != null && total > 0 ? Math.min(100, Math.round((rows / total) * 100)) + "%" : "—");
|
|
2884
|
+
const [task, setTask] = React.useState(null); // {id, snap}
|
|
2885
|
+
const [busy, setBusy] = React.useState(false);
|
|
2886
|
+
const [error, setError] = React.useState("");
|
|
2887
|
+
const pollRef = React.useRef(null);
|
|
2888
|
+
const mountedRef = React.useRef(true);
|
|
2889
|
+
const dbsCacheRef = React.useRef({}); // connId -> string[]
|
|
2890
|
+
const schemasCacheRef = React.useRef({}); // connId + "/" + db -> string[]
|
|
2891
|
+
const verCacheRef = React.useRef({}); // connId -> serverInfo(含失败空串)
|
|
2892
|
+
React.useEffect(() => () => { mountedRef.current = false; }, []);
|
|
2893
|
+
const srcVer = useServerVer(srcId, verCacheRef);
|
|
2894
|
+
const dstVer = useServerVer(dstId, verCacheRef);
|
|
2895
|
+
|
|
2896
|
+
const srcConn = conns.find((c) => c.id === srcId);
|
|
2897
|
+
const dstConn = conns.find((c) => c.id === dstId);
|
|
2898
|
+
const dstGrant = (props.grants || []).find((x) => x.connId === dstId);
|
|
2899
|
+
// PG/GaussDB 三层(库→模式→表),与 BrowsePane 同一张表;sqlite 无库层(连接默认上下文)
|
|
2900
|
+
const HAS_SCHEMAS = { postgresql: true, gaussdb: true };
|
|
2901
|
+
const NO_DB_LEVEL = { sqlite: true };
|
|
2902
|
+
|
|
2903
|
+
// 项目切换清层级缓存(对齐 BrowsePane:避免陈旧授权下的旧数据)
|
|
2904
|
+
React.useEffect(() => {
|
|
2905
|
+
dbsCacheRef.current = {}; schemasCacheRef.current = {};
|
|
2906
|
+
setSrcDb(""); setSrcSchema(""); setDstDb(""); setDstSchema(""); setSrcDbs(null); setSrcSchemas(null); setDstDbs(null); setDstSchemas(null);
|
|
2907
|
+
setDbsErr({ src: "", dst: "" });
|
|
2908
|
+
}, [projectPath]);
|
|
2909
|
+
|
|
2910
|
+
// 库清单加载(BrowsePane 同端点;缓存防重复拉取,失败就地记 dbsErr 可重试)
|
|
2911
|
+
function loadDbs(slot, connId) {
|
|
2912
|
+
const conn = conns.find((c) => c.id === connId);
|
|
2913
|
+
if (!connId || !projectPath || !conn || NO_DB_LEVEL[conn.kind]) return;
|
|
2914
|
+
const cached = dbsCacheRef.current[connId];
|
|
2915
|
+
if (cached) {
|
|
2916
|
+
if (slot === "src") setSrcDbs(cached); else setDstDbs(cached);
|
|
2917
|
+
return;
|
|
2918
|
+
}
|
|
2919
|
+
api("databases" + qs({ project: projectPath, connId }))
|
|
2920
|
+
.then((list) => {
|
|
2921
|
+
const v = Array.isArray(list) ? list : [];
|
|
2922
|
+
dbsCacheRef.current[connId] = v;
|
|
2923
|
+
if (slot === "src") setSrcDbs(v); else setDstDbs(v);
|
|
2924
|
+
})
|
|
2925
|
+
.catch((e) => setDbsErr((s) => Object.assign({}, s, { [slot]: String(e && e.message ? e.message : e) })));
|
|
2926
|
+
}
|
|
2927
|
+
// 源连接变化 → 清层级与表选中,拉库清单(目标侧连接变化只联动自身)
|
|
2928
|
+
React.useEffect(() => {
|
|
2929
|
+
setSrcDb(""); setSrcSchema(""); setSrcSchemas(null); setTablesErr("");
|
|
2930
|
+
setDbsErr((s) => Object.assign({}, s, { src: "" }));
|
|
2931
|
+
loadDbs("src", srcId);
|
|
2932
|
+
}, [srcId, projectPath]); // eslint-disable-line
|
|
2933
|
+
React.useEffect(() => {
|
|
2934
|
+
setDstDb(""); setDstSchema(""); setDstSchemas(null);
|
|
2935
|
+
setDbsErr((s) => Object.assign({}, s, { dst: "" }));
|
|
2936
|
+
loadDbs("dst", dstId);
|
|
2937
|
+
}, [dstId, projectPath]); // eslint-disable-line
|
|
2938
|
+
// 库变化(pg 系)→ 清模式并拉库内 schema 清单,完成后自动补选第一项(Navicat 式:进库即见表);源/目标共用
|
|
2939
|
+
function loadSchemas(slot, connId, db) {
|
|
2940
|
+
const conn = conns.find((c) => c.id === connId);
|
|
2941
|
+
if (!connId || !db || !HAS_SCHEMAS[conn && conn.kind]) return;
|
|
2942
|
+
const setList = slot === "src" ? setSrcSchemas : setDstSchemas;
|
|
2943
|
+
const setPick = slot === "src" ? setSrcSchema : setDstSchema;
|
|
2944
|
+
setPick(""); setList(null);
|
|
2945
|
+
const ck = connId + "/" + db;
|
|
2946
|
+
const cached = schemasCacheRef.current[ck];
|
|
2947
|
+
if (cached) { setList(cached); setPick(cached[0] || ""); return; }
|
|
2948
|
+
api("schemas" + qs({ project: projectPath, connId, database: db }))
|
|
2949
|
+
.then((list) => {
|
|
2950
|
+
const v = Array.isArray(list) ? list : [];
|
|
2951
|
+
schemasCacheRef.current[ck] = v;
|
|
2952
|
+
setList(v);
|
|
2953
|
+
setPick(v[0] || "");
|
|
2954
|
+
})
|
|
2955
|
+
.catch(() => { setList([]); }); // 失败置空清单:模式/表区按「无模式」呈现,不弹全局错误
|
|
2956
|
+
}
|
|
2957
|
+
React.useEffect(() => { loadSchemas("src", srcId, srcDb); }, [srcId, srcDb, projectPath]); // eslint-disable-line
|
|
2958
|
+
React.useEffect(() => { loadSchemas("dst", dstId, dstDb); }, [dstId, dstDb, projectPath]); // eslint-disable-line
|
|
2959
|
+
// 表清单按源层级加载:pg 系需库+模式齐备(服务端 tables.database="库名.模式" 复合形态,与 BrowsePane 一致);
|
|
2960
|
+
// 其余库层直传库名(""=默认库),sqlite/mongo 按连接默认上下文。只列 BASE TABLE/COLLECTION。乱序护栏 stopped。
|
|
2961
|
+
React.useEffect(() => {
|
|
2962
|
+
setSrcTables([]); setSelected([]); setTablesErr("");
|
|
2963
|
+
if (!srcId || !projectPath) return undefined;
|
|
2964
|
+
const kind = srcConn && srcConn.kind;
|
|
2965
|
+
let database;
|
|
2966
|
+
if (NO_DB_LEVEL[kind]) database = undefined;
|
|
2967
|
+
else if (HAS_SCHEMAS[kind]) {
|
|
2968
|
+
if (!srcDb || !srcSchema) return undefined; // 等模式下拉就位(schema effect 自动补选)
|
|
2969
|
+
database = srcDb + "." + srcSchema;
|
|
2970
|
+
} else database = srcDb || undefined;
|
|
2971
|
+
let stopped = false;
|
|
2972
|
+
setLoadingTables(true);
|
|
2973
|
+
api("tables" + qs({ project: projectPath, connId: srcId, database }))
|
|
2974
|
+
.then((list) => {
|
|
2975
|
+
if (stopped) return;
|
|
2976
|
+
const rows = (Array.isArray(list) ? list : [])
|
|
2977
|
+
.filter((x) => !x || !x.type || String(x.type).toUpperCase() === "TABLE" || String(x.type).toUpperCase() === "COLLECTION" || String(x.type).toUpperCase() === "BASE TABLE")
|
|
2978
|
+
.map((x) => ({ name: String(x.name), type: String((x && x.type) || "").toUpperCase(), comment: x && x.comment ? String(x.comment) : "" }))
|
|
2979
|
+
.filter((x) => x.name);
|
|
2980
|
+
setSrcTables(rows);
|
|
2981
|
+
})
|
|
2982
|
+
.catch((e) => { if (!stopped) { setSrcTables([]); setTablesErr(String(e && e.message ? e.message : e)); } })
|
|
2983
|
+
.finally(() => { if (!stopped) setLoadingTables(false); });
|
|
2984
|
+
return () => { stopped = true; };
|
|
2985
|
+
}, [srcId, srcDb, srcSchema, projectPath, tablesTick]); // eslint-disable-line
|
|
2986
|
+
|
|
2987
|
+
// 轮询:running → 每 800ms;终态/NOT_FOUND 停止(NOT_FOUND = 终态即清除)
|
|
2988
|
+
const stopPoll = React.useCallback(() => {
|
|
2989
|
+
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
|
|
2990
|
+
}, []);
|
|
2991
|
+
React.useEffect(() => stopPoll, [stopPoll]);
|
|
2992
|
+
function poll(id) {
|
|
2993
|
+
stopPoll();
|
|
2994
|
+
// 乱序护栏:慢响应(>800ms)晚到时本轮已被置失效,不得覆盖已到达的终态快照
|
|
2995
|
+
let stopped = false;
|
|
2996
|
+
pollRef.current = setInterval(async () => {
|
|
2997
|
+
if (stopped) return;
|
|
2998
|
+
try {
|
|
2999
|
+
const snap = await api("transfer/" + encodeURIComponent(id) + qs({ project: projectPath }));
|
|
3000
|
+
if (stopped) return; // 终态已先行(另一轮已 stopPoll):丢弃本轮旧快照
|
|
3001
|
+
setTask({ id, snap });
|
|
3002
|
+
if (!snap || snap.status !== "running") { stopped = true; stopPoll(); }
|
|
3003
|
+
} catch (e) {
|
|
3004
|
+
stopped = true;
|
|
3005
|
+
stopPoll();
|
|
3006
|
+
// 任务条目已被清理(终态保留窗口过后/被新任务 sweep):拉一次失败不报错,保持既有快照
|
|
3007
|
+
setTask((prev) => (prev && prev.snap && prev.snap.status !== "running" ? prev : null));
|
|
3008
|
+
}
|
|
3009
|
+
}, 800);
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
// 同连接且库/模式都相同才视为同上下文:允许同连接跨库(或同库跨模式)传输
|
|
3013
|
+
// (后端同目标互斥仍按 targetConnId,statement 携带定位入审计)
|
|
3014
|
+
const sameCtx = !!srcId && srcId === dstId && (srcDb || "") === (dstDb || "") && (srcSchema || "") === (dstSchema || "");
|
|
3015
|
+
const canStart = !!srcId && !!dstId && selected.length > 0 && !sameCtx;
|
|
3016
|
+
// 禁用提示按实际缺失条件给出(sameCtx 优先:结构性约束先于勾表提醒),不得与禁因不符
|
|
3017
|
+
const startHint = sameCtx ? t("trSameConn") : (!srcId || !dstId) ? t("selectConn") : t("trxPickTable");
|
|
3018
|
+
|
|
3019
|
+
async function start() {
|
|
3020
|
+
if (busy || !canStart) return;
|
|
3021
|
+
setBusy(true); setError("");
|
|
3022
|
+
try {
|
|
3023
|
+
const data = await runGuarded(
|
|
3024
|
+
(challengeId) => api("transfer/start", {
|
|
3025
|
+
method: "POST",
|
|
3026
|
+
body: {
|
|
3027
|
+
projectPath, sourceConnId: srcId, targetConnId: dstId, tables: selected, writeMode, batchSize, tableConcurrency, shardConcurrency, overwriteStructure, challengeId,
|
|
3028
|
+
// 定位字段(undefined 序列化即省略 = 连接默认上下文,后端 optStr 语义)
|
|
3029
|
+
sourceDatabase: srcDb || undefined, sourceSchema: srcSchema || undefined,
|
|
3030
|
+
targetDatabase: dstDb || undefined, targetSchema: dstSchema || undefined,
|
|
3031
|
+
},
|
|
3032
|
+
}),
|
|
3033
|
+
(info) => askConfirm(Object.assign({}, info, {
|
|
3034
|
+
target: { conn: dstConn ? (dstConn.name || dstId) : dstId, kind: dstConn ? dstConn.kind : "", db: dstDb ? (dstDb + (dstSchema ? "." + dstSchema : "")) : t("defaultDb"), mode: dstGrant ? dstGrant.mode : "" },
|
|
3035
|
+
})),
|
|
3036
|
+
);
|
|
3037
|
+
const id = data && data.taskId;
|
|
3038
|
+
// 确认往返期间组件可能已卸载(locale 切换重建 Panel):此时不再建立轮询
|
|
3039
|
+
if (id && mountedRef.current) {
|
|
3040
|
+
setTask({ id, snap: { status: "running", tables: [], failures: [] } });
|
|
3041
|
+
poll(id);
|
|
3042
|
+
}
|
|
3043
|
+
} catch (e) {
|
|
3044
|
+
if (!e.cancelled) setError(String(e && e.message ? e.message : e));
|
|
3045
|
+
} finally {
|
|
3046
|
+
setBusy(false);
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
function cancel() {
|
|
3051
|
+
if (!task) return;
|
|
3052
|
+
api("transfer/" + encodeURIComponent(task.id) + "/cancel", { method: "POST", body: { projectPath } })
|
|
3053
|
+
.catch((e) => setError(String(e && e.message ? e.message : e)));
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
const toggle = (name) => setSelected((prev) => (prev.includes(name) ? prev.filter((n) => n !== name) : prev.concat(name)));
|
|
3057
|
+
const [collapsed, setCollapsed] = React.useState({}); // 分组树折叠状态({ [type]: true })
|
|
3058
|
+
const [history, setHistory] = React.useState(null); // 终态历史条目数组(子标签激活时懒加载)
|
|
3059
|
+
const loadHistory = React.useCallback(() => {
|
|
3060
|
+
if (!projectPath) return;
|
|
3061
|
+
api("transfer-history" + qs({ limit: 30 }))
|
|
3062
|
+
.then((list) => setHistory(Array.isArray(list) ? list : []))
|
|
3063
|
+
.catch((e) => setError(String(e && e.message ? e.message : e)));
|
|
3064
|
+
}, [projectPath]);
|
|
3065
|
+
// 传输历史懒加载:子标签激活且顶级可见时拉取(默认激活子标签首次进入即加载,onClick 保持纯切换)
|
|
3066
|
+
React.useEffect(() => {
|
|
3067
|
+
if (visible && subTab === "history" && history === null) loadHistory();
|
|
3068
|
+
}, [visible, subTab, history, loadHistory]);
|
|
3069
|
+
// 清除确认走 askConfirm 通道(原生 confirm 已在库内移除:无 aria/焦点管理)
|
|
3070
|
+
async function clearHistory() {
|
|
3071
|
+
const yes = await props.askConfirm({
|
|
3072
|
+
title: t("trHistoryClear"),
|
|
3073
|
+
statement: t("trHistory"),
|
|
3074
|
+
hint: t("trHistoryClearConfirm"),
|
|
3075
|
+
confirmLabel: t("trHistoryClear"),
|
|
3076
|
+
target: { conn: "", kind: "", db: "", mode: "" },
|
|
3077
|
+
});
|
|
3078
|
+
if (!yes) return;
|
|
3079
|
+
api("transfer-history/clear", { method: "POST", body: {} })
|
|
3080
|
+
.then((r) => { setMessage(t("ok") + " (" + (r && r.removed != null ? r.removed : 0) + ")"); setHistory([]); })
|
|
3081
|
+
.catch((e) => setError(String(e && e.message ? e.message : e)));
|
|
3082
|
+
}
|
|
3083
|
+
const historyStatus = (s2) => s2 === "done" ? t("trStatusDone") : s2 === "failed" ? t("trStatusFailed") : s2 === "cancelled" ? t("trStatusCancelled") : s2;
|
|
3084
|
+
// 状态着色:failed 红 / done 绿 / 其余默认
|
|
3085
|
+
const statusColor = (s) => s === "failed" ? { color: "var(--dbt-danger,#ff453a)", fontWeight: 600 } : s === "done" ? { color: "var(--dbt-success,#30d158)" } : undefined;
|
|
3086
|
+
const statusLabel = (s) => s === "pending" ? t("trStatusPending") : s === "running" ? t("trStatusRunning") : s === "done" ? t("trStatusDone") : s === "failed" ? t("trStatusFailed") : s === "cancelled" ? t("trStatusCancelled") : s;
|
|
3087
|
+
const connOptions = (value, onPick, disabled, label) => React.createElement("select", { value, disabled: !!disabled || busy, "aria-label": label, onChange: (e) => onPick(e.target.value) },
|
|
3088
|
+
React.createElement("option", { value: "" }, t("selectConn")),
|
|
3089
|
+
conns.filter((c) => c.kind !== "redis").map((c) => {
|
|
3090
|
+
const g = (props.grants || []).find((x) => x.connId === c.id);
|
|
3091
|
+
const suffix = g && g.mode ? " · " + g.mode : "";
|
|
3092
|
+
return React.createElement("option", { key: c.id, value: c.id }, (c.name || c.id) + " (" + c.kind + suffix + ")");
|
|
3093
|
+
}));
|
|
3094
|
+
// 摘要条层级描述:pg 系「库名.模式名」,其余库层显库名,无层级/未选库显示默认库
|
|
3095
|
+
const scopeText = (conn, db, schema) => {
|
|
3096
|
+
if (!conn) return "—";
|
|
3097
|
+
const k = conn.kind;
|
|
3098
|
+
if (NO_DB_LEVEL[k] || !db) return t("defaultDb");
|
|
3099
|
+
return HAS_SCHEMAS[k] && schema ? db + "." + schema : db;
|
|
3100
|
+
};
|
|
3101
|
+
// 信息区键值:kind/name/host/port 直读连接元数据(URL 方式可能无 host/port,显示「—」不编造)
|
|
3102
|
+
function infoKv(conn, ver) {
|
|
3103
|
+
return React.createElement("dl", { className: "dbt-transfer-kv" },
|
|
3104
|
+
React.createElement("dt", null, t("trxConnType")), React.createElement("dd", null, conn.kind || "—"),
|
|
3105
|
+
React.createElement("dt", null, t("trxConnName")), React.createElement("dd", null, conn.name || conn.id),
|
|
3106
|
+
React.createElement("dt", null, t("trxHost")), React.createElement("dd", null, conn.host || "—"),
|
|
3107
|
+
React.createElement("dt", null, t("trxPort")), React.createElement("dd", null, conn.port != null ? String(conn.port) : "—"),
|
|
3108
|
+
React.createElement("dt", null, t("trxServerVer")), React.createElement("dd", null, ver || "—"));
|
|
3109
|
+
}
|
|
3110
|
+
// 单栏:栏标题(蓝)+ 连接/库(/模式)纵向下拉 + 底部信息区
|
|
3111
|
+
function transferCol(side) {
|
|
3112
|
+
const isSrc = side === "src";
|
|
3113
|
+
const connId = isSrc ? srcId : dstId;
|
|
3114
|
+
const conn = isSrc ? srcConn : dstConn;
|
|
3115
|
+
const kind = conn && conn.kind;
|
|
3116
|
+
const db = isSrc ? srcDb : dstDb;
|
|
3117
|
+
const onDb = isSrc ? setSrcDb : setDstDb;
|
|
3118
|
+
const dbs = isSrc ? srcDbs : dstDbs;
|
|
3119
|
+
const schemas = isSrc ? srcSchemas : dstSchemas;
|
|
3120
|
+
const schema = isSrc ? srcSchema : dstSchema;
|
|
3121
|
+
const onSchema = isSrc ? setSrcSchema : setDstSchema;
|
|
3122
|
+
const errText = isSrc ? dbsErr.src : dbsErr.dst;
|
|
3123
|
+
const titleId = "dbt-transfer-" + side + "-title";
|
|
3124
|
+
// mongo 源/目标栏均可选库:服务端 tables/preview 按 database 定位(mongodb/index.ts dbOf),
|
|
3125
|
+
// 表清单与 sourceDatabase/targetDatabase 同库对齐,无错位
|
|
3126
|
+
const noDbLevel = NO_DB_LEVEL[kind];
|
|
3127
|
+
return React.createElement("section", { className: "dbt-transfer-col", "aria-labelledby": titleId },
|
|
3128
|
+
React.createElement("header", { id: titleId, className: "dbt-transfer-coltitle" }, t(isSrc ? "trxSource" : "trxTarget")),
|
|
3129
|
+
React.createElement("label", { className: "dbt-transfer-field" },
|
|
3130
|
+
React.createElement("span", { className: "dbt-transfer-fieldlabel" }, t("trxConn")),
|
|
3131
|
+
connOptions(connId, isSrc ? setSrcId : setDstId, false, t("trxConn"))),
|
|
3132
|
+
noDbLevel
|
|
3133
|
+
? React.createElement("label", { className: "dbt-transfer-field" },
|
|
3134
|
+
React.createElement("span", { className: "dbt-transfer-fieldlabel" }, t("trxDatabase")),
|
|
3135
|
+
React.createElement("span", { className: "dbt-muted" }, t("trxNoDbLevel")))
|
|
3136
|
+
: React.createElement("label", { className: "dbt-transfer-field" },
|
|
3137
|
+
React.createElement("span", { className: "dbt-transfer-fieldlabel" }, t("trxDatabase")),
|
|
3138
|
+
React.createElement("select", { value: db, disabled: busy || !connId, "aria-label": t("trxDatabase"), onChange: (e) => onDb(e.target.value) },
|
|
3139
|
+
React.createElement("option", { value: "" }, t("defaultDb")),
|
|
3140
|
+
(dbs || []).map((d) => React.createElement("option", { key: d, value: d }, d)))),
|
|
3141
|
+
errText ? React.createElement("span", { className: "dbt-err" },
|
|
3142
|
+
t("trxLoadDbsFail") + ":" + errText + " ",
|
|
3143
|
+
React.createElement("button", { className: "dbt-btn", onClick: () => { setDbsErr((s) => Object.assign({}, s, { [side]: "" })); loadDbs(side, connId); } }, t("retry"))) : null,
|
|
3144
|
+
HAS_SCHEMAS[kind] ? React.createElement("label", { className: "dbt-transfer-field" },
|
|
3145
|
+
React.createElement("span", { className: "dbt-transfer-fieldlabel" }, t("trxSchema")),
|
|
3146
|
+
React.createElement("select", { value: schema, disabled: busy || !db || !schemas, "aria-label": t("trxSchema"), onChange: (e) => onSchema(e.target.value) },
|
|
3147
|
+
(schemas || []).map((s) => React.createElement("option", { key: s, value: s }, s)))) : null,
|
|
3148
|
+
conn ? React.createElement("footer", { className: "dbt-transfer-info" },
|
|
3149
|
+
React.createElement("div", { className: "dbt-transfer-infotitle" }, t("trxInfo")),
|
|
3150
|
+
infoKv(conn, isSrc ? srcVer : dstVer)) : null,
|
|
3151
|
+
!isSrc && sameCtx ? React.createElement("span", { className: "dbt-err", role: "alert" }, t("trSameConn")) : null,
|
|
3152
|
+
);
|
|
3153
|
+
}
|
|
3154
|
+
// 表清单三态:加载中 / 失败+就地重试 / 空态(未选连接或 pg 系未选库、库内无模式、无表)/ 分组树
|
|
3155
|
+
function tablesBody() {
|
|
3156
|
+
if (loadingTables) return React.createElement("div", { className: "dbt-muted" }, t("trLoadingTables"));
|
|
3157
|
+
if (tablesErr) return React.createElement("div", { className: "dbt-browse-errorbox" },
|
|
3158
|
+
React.createElement("span", { className: "dbt-err", role: "alert" }, tablesErr),
|
|
3159
|
+
React.createElement("button", { className: "dbt-btn", onClick: () => setTablesTick((x) => x + 1) }, t("retry")));
|
|
3160
|
+
const kind = srcConn && srcConn.kind;
|
|
3161
|
+
if (!srcId || (HAS_SCHEMAS[kind] && !srcDb)) return React.createElement("div", { className: "dbt-muted" }, t("trxPickSource"));
|
|
3162
|
+
if (HAS_SCHEMAS[kind] && srcDb && srcSchemas && srcSchemas.length === 0) return React.createElement("div", { className: "dbt-muted" }, t("noSchemas"));
|
|
3163
|
+
if (HAS_SCHEMAS[kind] && srcDb && !srcSchema) return React.createElement("div", { className: "dbt-muted" }, t("trLoadingTables"));
|
|
3164
|
+
if (srcTables.length === 0) return React.createElement("div", { className: "dbt-muted" }, t("noTables"));
|
|
3165
|
+
// 按类型分组(TABLE/BASE TABLE→表、COLLECTION→集合、其余→其他),对齐 Navicat 数据库对象树
|
|
3166
|
+
const byType = new Map();
|
|
3167
|
+
for (const row of srcTables) {
|
|
3168
|
+
const key = row.type === "COLLECTION" ? "COLLECTION" : row.type === "VIEW" ? "VIEW" : row.type === "" ? "OTHER" : "TABLE";
|
|
3169
|
+
if (!byType.has(key)) byType.set(key, []);
|
|
3170
|
+
byType.get(key).push(row);
|
|
3171
|
+
}
|
|
3172
|
+
const groupLabel = (key) => key === "COLLECTION" ? t("trGroupCollection") : key === "VIEW" ? t("trGroupView") : key === "OTHER" ? t("trGroupOther") : t("trGroupTable");
|
|
3173
|
+
const toggleGroup = (rows, allSelected) =>
|
|
3174
|
+
setSelected((prev) => allSelected
|
|
3175
|
+
? prev.filter((n) => !rows.some((r) => r.name === n))
|
|
3176
|
+
: prev.concat(rows.map((r) => r.name).filter((n) => !prev.includes(n))));
|
|
3177
|
+
const triRef = (all, some) => (el) => { if (el) el.indeterminate = some && !all; };
|
|
3178
|
+
return React.createElement("div", { className: "dbt-transfer-tablelist", role: "group", "aria-label": t("trTables") },
|
|
3179
|
+
[...byType.entries()].map(([key, rows]) => {
|
|
3180
|
+
const selCount = rows.filter((r) => selected.includes(r.name)).length;
|
|
3181
|
+
const all = selCount === rows.length;
|
|
3182
|
+
const some = selCount > 0;
|
|
3183
|
+
const open = !collapsed[key];
|
|
3184
|
+
const flip = () => setCollapsed((prev) => Object.assign({}, prev, { [key]: open }));
|
|
3185
|
+
return React.createElement("div", { key },
|
|
3186
|
+
React.createElement("div", { className: "dbt-tr-group-head", role: "button", tabIndex: 0, "aria-expanded": open,
|
|
3187
|
+
onClick: flip, onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); flip(); } } },
|
|
3188
|
+
React.createElement("span", { className: "dbt-tr-caret" + (open ? " open" : ""), "aria-hidden": "true" }),
|
|
3189
|
+
React.createElement("input", { type: "checkbox", ref: triRef(all, some), checked: all, onChange: () => toggleGroup(rows, all), onClick: (e) => e.stopPropagation(), "aria-label": groupLabel(key) }),
|
|
3190
|
+
React.createElement("span", null, groupLabel(key) + " (" + selCount + "/" + rows.length + ")")),
|
|
3191
|
+
open ? rows.map((row) => React.createElement("div", { key: row.name, className: "dbt-tr-item", title: row.comment || undefined },
|
|
3192
|
+
React.createElement("label", { className: "dbt-tr-pair" },
|
|
3193
|
+
React.createElement("input", { type: "checkbox", checked: selected.includes(row.name), onChange: () => toggle(row.name) }),
|
|
3194
|
+
React.createElement("span", null, row.name),
|
|
3195
|
+
row.comment ? React.createElement("span", { className: "dbt-tr-comment" }, row.comment) : null))) : null);
|
|
3196
|
+
}));
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
return React.createElement(
|
|
3200
|
+
"div",
|
|
3201
|
+
{ className: "dbt-card" },
|
|
3202
|
+
// 顶部摘要条:源连接/层级 ⇄ 目标连接/层级(源=目标同上下文时箭头转 danger 色)
|
|
3203
|
+
React.createElement("div", { className: "dbt-transfer-summary", "aria-label": t("trxArrowLabel") },
|
|
3204
|
+
React.createElement("span", { className: "dbt-transfer-endpoint" },
|
|
3205
|
+
React.createElement("strong", null, srcConn ? (srcConn.name || srcId) : t("selectConn")),
|
|
3206
|
+
React.createElement("span", { className: "dbt-muted" }, scopeText(srcConn, srcDb, srcSchema))),
|
|
3207
|
+
React.createElement("span", { className: "dbt-transfer-divider", style: sameCtx ? { color: "var(--dbt-danger)" } : undefined }, transferArrow()),
|
|
3208
|
+
React.createElement("span", { className: "dbt-transfer-endpoint" },
|
|
3209
|
+
React.createElement("strong", null, dstConn ? (dstConn.name || dstId) : t("selectConn")),
|
|
3210
|
+
React.createElement("span", { className: "dbt-muted" }, scopeText(dstConn, dstDb, dstSchema)))),
|
|
3211
|
+
// 双栏:源(连接/数据库/模式)⇄ 目标(连接/数据库)
|
|
3212
|
+
React.createElement("div", { className: "dbt-transfer-cols" },
|
|
3213
|
+
transferCol("src"),
|
|
3214
|
+
React.createElement("div", { className: "dbt-transfer-divider" }, transferArrow()),
|
|
3215
|
+
transferCol("dst")),
|
|
3216
|
+
// 传输对象:表清单按源层级联动加载
|
|
3217
|
+
React.createElement("div", { className: "dbt-group" },
|
|
3218
|
+
React.createElement("div", { className: "dbt-listrow" },
|
|
3219
|
+
React.createElement("span", { className: "dbt-muted" }, t("trTables")),
|
|
3220
|
+
React.createElement("span", { className: "dbt-muted" }, t("trxSelectedCount", { n: selected.length })),
|
|
3221
|
+
React.createElement("span", { style: { flex: 1 } }),
|
|
3222
|
+
srcTables.length > 0 ? React.createElement("button", { className: "dbt-btn", onClick: () => setSelected(srcTables.map((x) => x.name)) }, t("trSelectAll")) : null,
|
|
3223
|
+
selected.length > 0 ? React.createElement("button", { className: "dbt-btn", onClick: () => setSelected([]) }, t("trClear")) : null),
|
|
3224
|
+
tablesBody()),
|
|
3225
|
+
// 传输选项(原生 details 渐进披露;四控件沿用原逻辑)
|
|
3226
|
+
React.createElement("details", { className: "dbt-transfer-options" },
|
|
3227
|
+
React.createElement("summary", null, t("trxOptions")),
|
|
3228
|
+
React.createElement("div", { className: "dbt-group" },
|
|
3229
|
+
React.createElement("div", { className: "dbt-listrow" },
|
|
3230
|
+
React.createElement("span", { className: "dbt-muted" }, t("trWriteMode")),
|
|
3231
|
+
React.createElement("select", { value: writeMode, disabled: busy, "aria-label": t("trWriteMode"), onChange: (e) => setWriteMode(e.target.value) },
|
|
3232
|
+
React.createElement("option", { value: "insert" }, t("trModeInsert")),
|
|
3233
|
+
React.createElement("option", { value: "ignore" }, t("trModeIgnore")),
|
|
3234
|
+
React.createElement("option", { value: "replace" }, t("trModeReplace")),
|
|
3235
|
+
React.createElement("option", { value: "truncate" }, t("trModeTruncate")))),
|
|
3236
|
+
React.createElement("div", { className: "dbt-listrow" },
|
|
3237
|
+
React.createElement("span", { className: "dbt-muted" }, t("trBatchSize")),
|
|
3238
|
+
React.createElement("input", { type: "number", min: 1, max: 5000, "aria-label": t("trBatchSize"), value: batchSize, disabled: busy, onChange: (e) => setBatchSize(Math.min(5000, Math.max(1, Math.floor(Number(e.target.value) || 1)))) })),
|
|
3239
|
+
React.createElement("div", { className: "dbt-listrow" },
|
|
3240
|
+
React.createElement("span", { className: "dbt-muted" }, t("trTableConcurrency")),
|
|
3241
|
+
React.createElement("input", { type: "number", min: 1, max: 16, "aria-label": t("trTableConcurrency"), value: tableConcurrency, disabled: busy, onChange: (e) => setTableConcurrency(Math.min(16, Math.max(1, Math.floor(Number(e.target.value) || 1)))) })),
|
|
3242
|
+
React.createElement("div", { className: "dbt-listrow" },
|
|
3243
|
+
React.createElement("span", { className: "dbt-muted" }, t("trShardConcurrency")),
|
|
3244
|
+
React.createElement("input", { type: "number", min: 1, max: 32, "aria-label": t("trShardConcurrency"), value: shardConcurrency, disabled: busy, onChange: (e) => setShardConcurrency(Math.min(32, Math.max(1, Math.floor(Number(e.target.value) || 1)))) })),
|
|
3245
|
+
React.createElement("label", { className: "dbt-listrow", title: t("trxOverwriteHint") },
|
|
3246
|
+
React.createElement("input", { type: "checkbox", checked: overwriteStructure, disabled: busy, onChange: (e) => setOverwriteStructure(e.target.checked) }),
|
|
3247
|
+
React.createElement("span", { className: "dbt-muted" }, t("trxOverwrite"))))),
|
|
3248
|
+
|
|
3249
|
+
// 动作行(禁用时 title 说明原因)+ 进度(仅任务存在时渲染)
|
|
3250
|
+
React.createElement("div", { className: "dbt-listrow" },
|
|
3251
|
+
React.createElement("button", { className: "dbt-btn primary", disabled: busy || !canStart, title: canStart ? undefined : startHint, onClick: () => { start(); } }, t("trStart")),
|
|
3252
|
+
task && task.snap && task.snap.status === "running"
|
|
3253
|
+
? React.createElement("button", { className: "dbt-btn", onClick: cancel }, t("trCancel"))
|
|
3254
|
+
: null,
|
|
3255
|
+
error ? React.createElement("span", { className: "dbt-err", role: "alert" }, error) : null),
|
|
3256
|
+
task && task.snap
|
|
3257
|
+
? React.createElement(
|
|
3258
|
+
"div",
|
|
3259
|
+
{ className: "dbt-group" },
|
|
3260
|
+
React.createElement("div", { className: "dbt-listrow" },
|
|
3261
|
+
React.createElement("span", { className: "dbt-muted" }, t("trProgress")),
|
|
3262
|
+
React.createElement("strong", null, React.createElement("span", { "aria-live": "polite", style: statusColor(task.snap.status) }, statusLabel(task.snap.status)))),
|
|
3263
|
+
React.createElement("table", { className: "dbt-table" },
|
|
3264
|
+
React.createElement("thead", null, React.createElement("tr", null,
|
|
3265
|
+
React.createElement("th", null, t("trTables")),
|
|
3266
|
+
React.createElement("th", null, t("trStatus")),
|
|
3267
|
+
React.createElement("th", null, t("trRows")),
|
|
3268
|
+
React.createElement("th", null, t("trShards")))),
|
|
3269
|
+
React.createElement("tbody", null, (task.snap.tables || []).map((tb) =>
|
|
3270
|
+
React.createElement("tr", { key: tb.name },
|
|
3271
|
+
React.createElement("td", null, tb.name),
|
|
3272
|
+
React.createElement("td", { style: statusColor(tb.status) }, statusLabel(tb.status)),
|
|
3273
|
+
React.createElement("td", null, String(tb.rows) + (tb.totalRows != null ? " / " + tb.totalRows + "(" + pct(tb.rows, tb.totalRows) + ")" : "")),
|
|
3274
|
+
React.createElement("td", null, tb.shardsDone + "/" + tb.shardsTotal + (tb.shardsFailed > 0 ? " (" + tb.shardsFailed + "×)" : "")))))),
|
|
3275
|
+
(task.snap.failures || []).length > 0
|
|
3276
|
+
? React.createElement("div", { className: "dbt-group" },
|
|
3277
|
+
React.createElement("div", { className: "dbt-muted" }, t("trFailures")),
|
|
3278
|
+
task.snap.failures.map((f, i) => React.createElement("div", { key: i, className: "dbt-err" },
|
|
3279
|
+
f.table + (f.shard >= 0 ? " #" + f.shard : "") + ": " + f.error)))
|
|
3280
|
+
: null)
|
|
3281
|
+
: null,
|
|
3282
|
+
// 传输历史/传输日志:同级子标签(分段控件复用 .dbt-tabs),内容常驻挂载 + hidden 切换(切换不丢状态)
|
|
3283
|
+
React.createElement("div", { className: "dbt-tabs", role: "tablist" },
|
|
3284
|
+
[["history", t("trHistory")], ["tlog", t("viewTransferLog")]].map(([v, label]) =>
|
|
3285
|
+
React.createElement("button", {
|
|
3286
|
+
key: v, className: subTab === v ? "active" : "",
|
|
3287
|
+
role: "tab", "aria-selected": subTab === v,
|
|
3288
|
+
onClick: () => setSubTab(v),
|
|
3289
|
+
}, label)),
|
|
3290
|
+
),
|
|
3291
|
+
React.createElement("div", { hidden: subTab !== "history" },
|
|
3292
|
+
React.createElement("div", { className: "dbt-group" },
|
|
3293
|
+
React.createElement("div", { className: "dbt-listrow" },
|
|
3294
|
+
React.createElement("span", { style: { flex: 1 } }),
|
|
3295
|
+
history !== null && history.length > 0
|
|
3296
|
+
? React.createElement("button", { className: "dbt-btn", onClick: () => { clearHistory(); } }, t("trHistoryClear"))
|
|
3297
|
+
: null),
|
|
3298
|
+
history === null
|
|
3299
|
+
? React.createElement("div", { className: "dbt-muted" }, "…")
|
|
3300
|
+
: history.length === 0
|
|
3301
|
+
? React.createElement("div", { className: "dbt-muted" }, t("trHistoryEmpty"))
|
|
3302
|
+
: history.map((h) => React.createElement("div", { key: h.taskId + h.ts, className: "dbt-tr-item", title: h.statement },
|
|
3303
|
+
React.createElement("span", { className: "dbt-muted", style: { flex: "none" } }, new Date(h.ts).toLocaleString()),
|
|
3304
|
+
React.createElement("strong", { style: statusColor(h.status) }, historyStatus(h.status)),
|
|
3305
|
+
React.createElement("span", { className: "dbt-tr-comment" },
|
|
3306
|
+
((h.statement.match(/表\[[^\]]*\]/) || [""])[0].slice(0, 80))
|
|
3307
|
+
+ " · " + (h.tables || []).filter((t2) => t2.status === "done").length + "✓/"
|
|
3308
|
+
+ (h.tables || []).filter((t2) => t2.status === "failed").length + "✗"
|
|
3309
|
+
+ ((h.failures || []).length > 0 ? " · " + t("trFailures") + " " + h.failures.length : ""))))),
|
|
3310
|
+
),
|
|
3311
|
+
// 传输日志:props 直取原始入参别名(2843-2846 行绑定),不经过本地遮蔽的 busy/error(那是传输任务自己的状态);
|
|
3312
|
+
// visible=顶级标签激活且当前在日志子标签(切走即停 5s 轮询)
|
|
3313
|
+
React.createElement("div", { hidden: subTab !== "tlog" },
|
|
3314
|
+
React.createElement(TransferLogView, { projectPath, visible: visible && subTab === "tlog", askConfirm })),
|
|
3315
|
+
);
|
|
3316
|
+
}
|
|
3317
|
+
/* ---------------- 传输日志(「数据传输」内子标签:实时事件流,5s 自动刷新) ---------------- */
|
|
3318
|
+
function TransferLogView(props) {
|
|
3319
|
+
const projectPath = props.projectPath;
|
|
3320
|
+
const [rows, setRows] = React.useState(null); // 日志条目数组
|
|
3321
|
+
const [auto, setAuto] = React.useState(true);
|
|
3322
|
+
const [error, setError] = React.useState("");
|
|
3323
|
+
|
|
3324
|
+
const load = React.useCallback(() => {
|
|
3325
|
+
api("transfer-log" + qs({ project: projectPath, limit: 300 }))
|
|
3326
|
+
.then((list) => setRows(Array.isArray(list) ? list : []))
|
|
3327
|
+
.catch((e) => setError(String(e && e.message ? e.message : e)));
|
|
3328
|
+
}, [projectPath]);
|
|
3329
|
+
|
|
3330
|
+
// 子标签可见时 5s 轮询;隐藏即真正清除定时器(切走即停),重进/进入时立即拉取一次
|
|
3331
|
+
// (auto 关闭时这次拉取是唯一加载路径,等价替代已删除的手动刷新按钮)
|
|
3332
|
+
React.useEffect(() => {
|
|
3333
|
+
if (!props.visible) return undefined;
|
|
3334
|
+
load();
|
|
3335
|
+
if (!auto) return undefined;
|
|
3336
|
+
const timer = setInterval(load, 5000);
|
|
3337
|
+
return () => clearInterval(timer);
|
|
3338
|
+
}, [props.visible, auto, load]);
|
|
3339
|
+
|
|
3340
|
+
async function clearLog() {
|
|
3341
|
+
const yes = await props.askConfirm({
|
|
3342
|
+
title: t("tlogClear"),
|
|
3343
|
+
statement: t("viewTransferLog"),
|
|
3344
|
+
hint: t("tlogClearConfirm"),
|
|
3345
|
+
confirmLabel: t("tlogClear"),
|
|
3346
|
+
target: { conn: "", kind: "", db: "", mode: "" },
|
|
3347
|
+
});
|
|
3348
|
+
if (!yes) return;
|
|
3349
|
+
api("transfer-history/clear", { method: "POST", body: {} })
|
|
3350
|
+
.then(() => load())
|
|
3351
|
+
.catch((e) => setError(String(e && e.message ? e.message : e)));
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
const typeLabel = (ty) => ty === "start" ? t("tlogTypeStart") : ty === "table" ? t("tlogTypeTable") : ty === "progress" ? t("tlogTypeProgress") : ty === "finish" ? t("tlogTypeFinish") : ty;
|
|
3355
|
+
|
|
3356
|
+
return React.createElement(
|
|
3357
|
+
"div",
|
|
3358
|
+
{ className: "dbt-card" },
|
|
3359
|
+
React.createElement("div", { className: "dbt-listrow" },
|
|
3360
|
+
React.createElement("label", { className: "dbt-listrow", style: { flex: "none" } },
|
|
3361
|
+
React.createElement("input", { type: "checkbox", checked: auto, onChange: (e) => setAuto(e.target.checked) }),
|
|
3362
|
+
React.createElement("span", { className: "dbt-muted" }, t("tlogAuto"))),
|
|
3363
|
+
React.createElement("span", { style: { flex: 1 } }),
|
|
3364
|
+
React.createElement("button", { className: "dbt-btn", onClick: () => { clearLog(); } }, t("tlogClear"))),
|
|
3365
|
+
error ? React.createElement("div", { className: "dbt-err", role: "alert" }, error) : null,
|
|
3366
|
+
rows === null
|
|
3367
|
+
? React.createElement("div", { className: "dbt-muted" }, "…")
|
|
3368
|
+
: rows.length === 0
|
|
3369
|
+
? React.createElement("div", { className: "dbt-muted" }, t("tlogEmpty"))
|
|
3370
|
+
: React.createElement("div", { className: "dbt-transfer-tablelist", style: { maxHeight: 320 } },
|
|
3371
|
+
rows.map((r, i) => {
|
|
3372
|
+
const brief = r.type === "start"
|
|
3373
|
+
? (r.statement || "").replace(/表\[[^\]]*\]/, "表[" + ((r.statement.match(/表\[([^\]]*)\]/) || ["", ""])[1].split(",").length) + "张]")
|
|
3374
|
+
: r.type === "table" || r.type === "progress"
|
|
3375
|
+
? (r.table || "") + " " + (r.rows != null ? r.rows : 0) + (r.totalRows != null ? " / " + r.totalRows : "") + " 行" + (r.status === "failed" ? " " + (r.statusLabel || "") : "")
|
|
3376
|
+
: (r.statement || "").slice(0, 60) + " " + (r.status || "");
|
|
3377
|
+
return React.createElement("div", { key: r.ts + i, className: "dbt-tr-item" },
|
|
3378
|
+
React.createElement("span", { className: "dbt-muted", style: { flex: "none", minWidth: 84 } }, new Date(r.ts).toLocaleTimeString()),
|
|
3379
|
+
React.createElement("span", { className: "dbt-tr-chip", style: { flex: "none" } }, typeLabel(r.type)),
|
|
3380
|
+
React.createElement("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, brief));
|
|
3381
|
+
})),
|
|
3382
|
+
);
|
|
3383
|
+
}
|
|
3384
|
+
|
|
2453
3385
|
/* ---------------- 根面板 ---------------- */
|
|
2454
3386
|
function Panel(props) {
|
|
2455
3387
|
const visible = props.visible;
|
|
@@ -2463,6 +3395,11 @@ window.__ModuleLoader__.load({
|
|
|
2463
3395
|
const [error, setError] = React.useState("");
|
|
2464
3396
|
const [message, setMessage] = React.useState("");
|
|
2465
3397
|
const [confirmReq, setConfirmReq] = React.useState(null); // {statement,danger,reason,resolve}
|
|
3398
|
+
const [treeRefresh, setTreeRefresh] = React.useState(0); // 浏览树刷新信号(手动按钮 / 切回可见自动)
|
|
3399
|
+
// 侧边栏切回可见时自动重拉已展开层:agents 等外部建库/建表后切回来即可见,无需手点刷新
|
|
3400
|
+
React.useEffect(() => {
|
|
3401
|
+
if (visible) setTreeRefresh((x) => x + 1);
|
|
3402
|
+
}, [visible]);
|
|
2466
3403
|
|
|
2467
3404
|
const askConfirm = React.useCallback((info) => new Promise((resolve) => {
|
|
2468
3405
|
setConfirmReq(Object.assign({}, info, { resolve }));
|
|
@@ -2515,6 +3452,7 @@ window.__ModuleLoader__.load({
|
|
|
2515
3452
|
manage: React.createElement(ManageView, shared),
|
|
2516
3453
|
grants: React.createElement(GrantsView, shared),
|
|
2517
3454
|
console: React.createElement(ConsoleView, shared),
|
|
3455
|
+
transfer: React.createElement(TransferView, Object.assign({}, shared, { visible })),
|
|
2518
3456
|
};
|
|
2519
3457
|
return React.createElement(
|
|
2520
3458
|
"div",
|
|
@@ -2522,7 +3460,7 @@ window.__ModuleLoader__.load({
|
|
|
2522
3460
|
React.createElement(
|
|
2523
3461
|
"div",
|
|
2524
3462
|
{ className: "dbt-tabs", role: "tablist" },
|
|
2525
|
-
[["manage", t("viewManage")], ["grants", t("viewGrants")], ["browse", t("viewBrowse")], ["console", t("viewConsole")]].map(([v, label]) =>
|
|
3463
|
+
[["manage", t("viewManage")], ["grants", t("viewGrants")], ["browse", t("viewBrowse")], ["console", t("viewConsole")], ["transfer", t("viewTransfer")]].map(([v, label]) =>
|
|
2526
3464
|
React.createElement("button", {
|
|
2527
3465
|
key: v, className: view === v ? "active" : "",
|
|
2528
3466
|
role: "tab", "aria-selected": view === v,
|
|
@@ -2548,21 +3486,31 @@ window.__ModuleLoader__.load({
|
|
|
2548
3486
|
React.createElement("span", { className: "dbt-muted" }, t("projectLabel", { path: projectPath })),
|
|
2549
3487
|
// 编辑图标:极简铅笔 SVG(验收要求无 emoji;svg aria-hidden,按钮 aria-label 提供语义)
|
|
2550
3488
|
React.createElement("button", {
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
3489
|
+
className: "dbt-btn", onClick: () => setProjectEdited(true), "aria-label": t("edit"),
|
|
3490
|
+
},
|
|
3491
|
+
React.createElement("svg", { width: 12, height: 12, viewBox: "0 0 12 12", fill: "none", "aria-hidden": "true" },
|
|
3492
|
+
React.createElement("path", {
|
|
3493
|
+
d: "M8.6 1.4 L10.6 3.4 L4.2 9.8 L1.6 10.4 L2.2 7.8 Z",
|
|
3494
|
+
stroke: "currentColor", strokeWidth: 1.2, strokeLinejoin: "round",
|
|
3495
|
+
})),
|
|
3496
|
+
),
|
|
3497
|
+
// 刷新:重拉所有已展开的库/模式/表(agents 等外部建库后点一下即可见)
|
|
3498
|
+
React.createElement("button", {
|
|
3499
|
+
className: "dbt-btn", onClick: () => setTreeRefresh((x) => x + 1), "aria-label": t("trRefresh"), title: t("trRefresh"),
|
|
3500
|
+
},
|
|
3501
|
+
React.createElement("svg", { width: 12, height: 12, viewBox: "0 0 12 12", fill: "none", "aria-hidden": "true" },
|
|
3502
|
+
React.createElement("path", {
|
|
3503
|
+
d: "M10.5 6a4.5 4.5 0 1 1-1.32-3.18M10.5 1v2.5H8",
|
|
3504
|
+
stroke: "currentColor", strokeWidth: 1.2, strokeLinecap: "round", strokeLinejoin: "round",
|
|
3505
|
+
})),
|
|
3506
|
+
)),
|
|
2559
3507
|
error ? React.createElement("div", { className: "dbt-err", "aria-live": "polite" }, t("error") + ": " + error) : null,
|
|
2560
3508
|
message ? React.createElement("div", { className: "dbt-msg", "aria-live": "polite" }, message) : null,
|
|
2561
3509
|
// 视图常驻挂载 + hidden 切换:切 tab 保留各视图内部状态(控制台 SQL、浏览选中不丢);
|
|
2562
3510
|
// dbt-in 入场动画只在面板首次打开播一次(.dbt-view 类自带,常驻后不随 tab 重播);
|
|
2563
3511
|
// browse 撑满面板剩余高度(BrowsePane 内详情栏才能钉在面板最底部),active 控制首次激活才拉树。
|
|
2564
3512
|
// .dbt-view 类的 display:flex 会盖过 hidden 的 UA 样式,故内联 display 同步切换
|
|
2565
|
-
["manage", "grants", "browse", "console"].map((v) =>
|
|
3513
|
+
["manage", "grants", "browse", "console", "transfer"].map((v) =>
|
|
2566
3514
|
React.createElement("div", {
|
|
2567
3515
|
key: v, className: "dbt-view", hidden: view !== v,
|
|
2568
3516
|
style: v === "browse"
|
|
@@ -2570,7 +3518,7 @@ window.__ModuleLoader__.load({
|
|
|
2570
3518
|
: (view === v ? undefined : { display: "none" }),
|
|
2571
3519
|
},
|
|
2572
3520
|
v === "browse"
|
|
2573
|
-
? React.createElement(BrowsePane, { ctx: props.ctx, conns, projectPath, grants, askConfirm, active: view === "browse" })
|
|
3521
|
+
? React.createElement(BrowsePane, { ctx: props.ctx, conns, projectPath, grants, askConfirm, active: view === "browse", refreshTick: treeRefresh })
|
|
2574
3522
|
: viewEls[v])),
|
|
2575
3523
|
React.createElement("div", { className: "dbt-muted" }, t("footerHint")),
|
|
2576
3524
|
confirmReq ? React.createElement(DangerDialog, { challenge: confirmReq, onClose: () => setConfirmReq(null) }) : null,
|
|
@@ -2634,13 +3582,14 @@ window.__ModuleLoader__.load({
|
|
|
2634
3582
|
}
|
|
2635
3583
|
exports.apply = apply;
|
|
2636
3584
|
exports.inject = inject;
|
|
2637
|
-
// 测试面:单元格写回 + 控制台工具纯函数 + ConsoleView
|
|
3585
|
+
// 测试面:单元格写回 + 控制台工具纯函数 + ConsoleView(集成测试浅渲染元素树)+ Panel/TransferView/TransferLogView(传输日志降级结构断言)
|
|
2638
3586
|
exports.__testables = {
|
|
2639
3587
|
dialectOf, quoteIdent, parseCellText, isTruncatedCell, isBlobCell,
|
|
2640
3588
|
buildUpdate, buildRedisOp, buildMongoOp, renderStatementWithParams,
|
|
2641
3589
|
splitSqlStatements, fmtDialectOf, toCsv, toJson, fmtMs,
|
|
2642
3590
|
sortRows, slicePage, ResultSetGrid,
|
|
2643
3591
|
classifyHead, ConsoleView,
|
|
3592
|
+
Panel, TransferView, TransferLogView,
|
|
2644
3593
|
};
|
|
2645
3594
|
return module.exports;
|
|
2646
3595
|
},
|