dsh-db-tool 1.5.2 → 1.5.4
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 +136 -10
- package/package.json +3 -2
package/client/client.js
CHANGED
|
@@ -388,7 +388,7 @@ window.__ModuleLoader__.load({
|
|
|
388
388
|
".dbt-chev.leaf{visibility:hidden;}",
|
|
389
389
|
".dbt-treename{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
390
390
|
/* ===== Navicat 式浏览弹窗 ===== */
|
|
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);}",
|
|
391
|
+
".dbt-browse-dialog{position:fixed;z-index:2147483000;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
392
|
".dbt-browse-dialog.dragging{user-select:none;}",
|
|
393
393
|
".dbt-browse-header{flex:none;display:flex;align-items:center;gap:10px;padding:12px 16px;border-bottom:1px solid var(--dbt-separator);}",
|
|
394
394
|
".dbt-browse-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600;font-size:17px;}",
|
|
@@ -463,11 +463,49 @@ window.__ModuleLoader__.load({
|
|
|
463
463
|
}
|
|
464
464
|
|
|
465
465
|
// --- 弹窗挂载:优先 createPortal 挂 document.body(避免侧栏容器裁剪);宿主未提供 react-dom 时回退 Panel 内 fixed overlay(DangerDialog 同模式) ---
|
|
466
|
-
|
|
467
|
-
|
|
466
|
+
// 三重降级解析(模块级缓存,全模块一次):①模块加载时同步 require(dsh.client.inject 声明 react-dom 后应成功)
|
|
467
|
+
// ②宿主模块系统异步 import(ctx.modules,宿主 rc.8+ 提供)③均不可用则保持 Panel 内回退渲染。
|
|
468
|
+
let portalImpl = null; // 解析成功的 createPortal(缓存,全模块一次)
|
|
469
|
+
let portalTried = false; // 是否已尝试解析(异步 import 只发起一次)
|
|
470
|
+
let portalPending = null; // 订阅解析成功的回调列表
|
|
471
|
+
try {
|
|
472
|
+
var rd = require("react-dom");
|
|
473
|
+
if (rd && typeof rd.createPortal === "function") { portalImpl = rd.createPortal.bind(rd); portalTried = true; }
|
|
474
|
+
} catch (e) { /* 留 null,等异步解析 */ }
|
|
475
|
+
function resolvePortalAsync(ctx) {
|
|
476
|
+
if (portalTried) return;
|
|
477
|
+
portalTried = true;
|
|
478
|
+
if (portalImpl) return;
|
|
479
|
+
var mods = ctx && ctx.modules;
|
|
480
|
+
var p = mods && typeof mods.import === "function" ? mods.import("react-dom") : null;
|
|
481
|
+
if (p && typeof p.then === "function") {
|
|
482
|
+
p.then(function (m) {
|
|
483
|
+
if (m && typeof m.createPortal === "function") {
|
|
484
|
+
portalImpl = m.createPortal.bind(m);
|
|
485
|
+
var waiters = portalPending || []; portalPending = null;
|
|
486
|
+
for (var i = 0; i < waiters.length; i++) waiters[i]();
|
|
487
|
+
}
|
|
488
|
+
}).catch(function () { /* 解析失败保持回退渲染 */ });
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function onPortalReady(cb) {
|
|
492
|
+
if (portalImpl) { cb(); return; }
|
|
493
|
+
(portalPending = portalPending || []).push(cb);
|
|
494
|
+
}
|
|
468
495
|
function mountDialog(children) {
|
|
469
|
-
return
|
|
496
|
+
return (portalImpl && !portalDisabled) ? portalImpl(children, document.body) : children;
|
|
470
497
|
}
|
|
498
|
+
// PiP 独立窗口渲染时禁 portal:PiP root 本身就挂在 pipDoc.body 上,再 portal 回主 document 会把 DOM 拉出独立窗口
|
|
499
|
+
let portalDisabled = false;
|
|
500
|
+
// createRoot 多级探测(PiP 独立窗口需要独立 React root):react-dom → react-dom/client
|
|
501
|
+
let reactCreateRoot = null;
|
|
502
|
+
try {
|
|
503
|
+
var rd2 = require("react-dom");
|
|
504
|
+
reactCreateRoot = rd2 && typeof rd2.createRoot === "function" ? rd2.createRoot.bind(rd2) : null;
|
|
505
|
+
if (!reactCreateRoot) {
|
|
506
|
+
try { var rdc = require("react-dom/client"); reactCreateRoot = rdc && typeof rdc.createRoot === "function" ? rdc.createRoot.bind(rdc) : null; } catch (e2) { /* 保持 null */ }
|
|
507
|
+
}
|
|
508
|
+
} catch (e) { /* 保持 null,PiP 不可用走 DOM 浮窗降级 */ }
|
|
471
509
|
|
|
472
510
|
/* ---------------- 连接管理 ---------------- */
|
|
473
511
|
const EMPTY_FORM = { id: "", kind: "mysql", name: "", mode: "url", url: "", urlUser: "", urlPassword: "", host: "", port: "", user: "", password: "", database: "", ssl: false };
|
|
@@ -1306,8 +1344,20 @@ window.__ModuleLoader__.load({
|
|
|
1306
1344
|
}
|
|
1307
1345
|
|
|
1308
1346
|
function BrowseDialog(props) {
|
|
1309
|
-
const { conns, projectPath, grants, askConfirm, onClose } = props;
|
|
1347
|
+
const { conns, projectPath, grants, askConfirm, onClose, ctx } = props;
|
|
1310
1348
|
// grants / askConfirm:预留给后续单元格编辑的危险操作确认通道,本任务先接住不使用
|
|
1349
|
+
// portal 异步解析接入:portalReady 仅用于触发重渲染(mountDialog 内部读 portalImpl)。
|
|
1350
|
+
// createPortal 不换组件实例——portalReady 切换前后是同一 DOM 节点,pos state 保持,居中定位不受 portal 重挂影响。
|
|
1351
|
+
const [portalReady, setPortalReady] = React.useState(!!portalImpl);
|
|
1352
|
+
React.useEffect(() => {
|
|
1353
|
+
resolvePortalAsync(ctx);
|
|
1354
|
+
onPortalReady(function () { setPortalReady(true); });
|
|
1355
|
+
// 诊断:一次性输出(帮助真机排查 iframe 隔离与 portal 解析结果)
|
|
1356
|
+
if (!window.__dbtPortalDiag) {
|
|
1357
|
+
window.__dbtPortalDiag = true;
|
|
1358
|
+
console.info("[dbt] portal:", portalImpl ? "sync/async ok" : "unavailable", "sameDoc:", window.top === window.self);
|
|
1359
|
+
}
|
|
1360
|
+
}, []);
|
|
1311
1361
|
const [sel, setSel] = React.useState(null); // 弹窗内当前选中(null=未选表,右栏显示引导空态)
|
|
1312
1362
|
const [view, setView] = React.useState("structure"); // structure | preview(纯视图切换,不影响数据加载)
|
|
1313
1363
|
const [schema, setSchema] = React.useState([]);
|
|
@@ -1345,13 +1395,14 @@ window.__ModuleLoader__.load({
|
|
|
1345
1395
|
if (closeRef.current) closeRef.current.focus();
|
|
1346
1396
|
}, []);
|
|
1347
1397
|
|
|
1348
|
-
// 拖动定位:首帧给个保守值,mount
|
|
1398
|
+
// 拖动定位:首帧给个保守值,mount 后按浮窗实测尺寸居中(坐标基准取浮窗所在 document 的视口——DOM 浮窗为主视口,PiP 独立窗口为 PiP 视口)
|
|
1349
1399
|
const dialogRef2 = React.useRef(null);
|
|
1350
1400
|
const [pos, setPos] = React.useState(() => ({ top: 72, left: 12 }));
|
|
1351
1401
|
React.useEffect(() => {
|
|
1352
1402
|
const el = dialogRef2.current;
|
|
1353
1403
|
if (!el) return;
|
|
1354
|
-
const
|
|
1404
|
+
const view = (el.ownerDocument && el.ownerDocument.defaultView) || window;
|
|
1405
|
+
const w = view.innerWidth || 1200, h = view.innerHeight || 800;
|
|
1355
1406
|
setPos({
|
|
1356
1407
|
top: Math.max(0, Math.round((h - el.offsetHeight) / 2)),
|
|
1357
1408
|
left: Math.max(0, Math.round((w - el.offsetWidth) / 2)),
|
|
@@ -1369,7 +1420,8 @@ window.__ModuleLoader__.load({
|
|
|
1369
1420
|
}
|
|
1370
1421
|
function onHeaderPointerMove(e) {
|
|
1371
1422
|
if (!draggingRef.current) return;
|
|
1372
|
-
const
|
|
1423
|
+
const view = (e.currentTarget.ownerDocument && e.currentTarget.ownerDocument.defaultView) || window;
|
|
1424
|
+
const h = view.innerHeight || 800, w = view.innerWidth || 1200;
|
|
1373
1425
|
// clamp 全部用浮窗实测尺寸:整体保持在视口内(窄视口下常量估宽会把窗拖丢)
|
|
1374
1426
|
const dlg = e.currentTarget.parentElement;
|
|
1375
1427
|
const dlgH = (dlg && dlg.offsetHeight) || 720;
|
|
@@ -1505,6 +1557,63 @@ window.__ModuleLoader__.load({
|
|
|
1505
1557
|
);
|
|
1506
1558
|
}
|
|
1507
1559
|
|
|
1560
|
+
/* ---------------- PiP 独立窗口(Document Picture-in-Picture) ---------------- */
|
|
1561
|
+
// OS 级独立窗口:可拖出桌面端主窗口、系统级置顶。内容为完整数据浏览 UI(同 JS 环境复用
|
|
1562
|
+
// BrowseDialog/DangerDialog 与 mock 之外的宿主 fetch 同源);DOM 浮窗作为不可用时的降级。
|
|
1563
|
+
// 打开成功返回 pipWindow,失败返回 null(调用方降级为 DOM 浮窗)。
|
|
1564
|
+
function PipBrowseApp(props) { // {ctx, conns, projectPath, grants, onClose}
|
|
1565
|
+
// 危险操作确认渲染在 PiP 窗口内(Panel 的确认对话框在主窗口,PiP 用户看不见)
|
|
1566
|
+
const [confirmReq, setConfirmReq] = React.useState(null);
|
|
1567
|
+
const askConfirm = React.useCallback((info) => new Promise((resolve) => {
|
|
1568
|
+
setConfirmReq(Object.assign({}, info, { resolve }));
|
|
1569
|
+
}), []);
|
|
1570
|
+
return React.createElement(
|
|
1571
|
+
React.Fragment,
|
|
1572
|
+
null,
|
|
1573
|
+
React.createElement(BrowseDialog, {
|
|
1574
|
+
ctx: props.ctx, conns: props.conns, projectPath: props.projectPath,
|
|
1575
|
+
grants: props.grants, askConfirm: askConfirm, onClose: props.onClose,
|
|
1576
|
+
}),
|
|
1577
|
+
confirmReq ? React.createElement(DangerDialog, { challenge: confirmReq, onClose: () => setConfirmReq(null) }) : null,
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
function openBrowsePip(ctx, deps) { // deps: {conns, projectPath, grants, onClosed} → 返回 pipWindow | null
|
|
1581
|
+
try {
|
|
1582
|
+
const dpip = window.documentPictureInPicture;
|
|
1583
|
+
if (!dpip || typeof dpip.requestWindow !== "function" || !reactCreateRoot || !portalImpl) return null;
|
|
1584
|
+
// PiP 只能在用户手势内同步发起 requestWindow,Promise then 里再做 DOM 装配
|
|
1585
|
+
return dpip.requestWindow({ width: 1100, height: 720 }).then((w) => {
|
|
1586
|
+
const doc = w.document;
|
|
1587
|
+
// 相对地址基准:PiP 文档是 about:blank,fetch 的相对 API 路径须按主 origin 解析
|
|
1588
|
+
const base = doc.createElement("base");
|
|
1589
|
+
base.href = window.location.origin + "/";
|
|
1590
|
+
doc.head.appendChild(base);
|
|
1591
|
+
// 样式:整套 --dbt-* token 与规则注入 PiP 文档
|
|
1592
|
+
if (styleEl) doc.head.appendChild(doc.importNode(styleEl, true));
|
|
1593
|
+
doc.body.style.margin = "0";
|
|
1594
|
+
portalDisabled = true; // PiP root 已在 pipDoc.body 上,禁 portal 防 DOM 被拉回主窗口
|
|
1595
|
+
const root = reactCreateRoot(doc.body);
|
|
1596
|
+
const closePip = () => {
|
|
1597
|
+
try { root.unmount(); } catch (e) { /* 已卸载 */ }
|
|
1598
|
+
portalDisabled = false;
|
|
1599
|
+
try { w.close(); } catch (e2) { /* 已关闭 */ }
|
|
1600
|
+
};
|
|
1601
|
+
root.render(React.createElement(PipBrowseApp, {
|
|
1602
|
+
ctx, conns: deps.conns, projectPath: deps.projectPath, grants: deps.grants,
|
|
1603
|
+
onClose: () => { closePip(); deps.onClosed(); },
|
|
1604
|
+
}));
|
|
1605
|
+
w.addEventListener("pagehide", () => { // 用户点 PiP 窗口系统关闭钮
|
|
1606
|
+
portalDisabled = false;
|
|
1607
|
+
try { root.unmount(); } catch (e) { /* 已卸载 */ }
|
|
1608
|
+
deps.onClosed();
|
|
1609
|
+
});
|
|
1610
|
+
return w;
|
|
1611
|
+
}).catch(() => null);
|
|
1612
|
+
} catch (e) {
|
|
1613
|
+
return Promise.resolve(null);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1508
1617
|
/* ---------------- SQL 控制台 ---------------- */
|
|
1509
1618
|
// Apple 等宽字体栈(规范 §1):优先样式层 --dbt-mono token,fallback 内联栈
|
|
1510
1619
|
const MONO_FONT = "var(--dbt-mono, ui-monospace, \"SF Mono\", Menlo, Consolas, monospace)";
|
|
@@ -1633,6 +1742,21 @@ window.__ModuleLoader__.load({
|
|
|
1633
1742
|
const [confirmReq, setConfirmReq] = React.useState(null); // {statement,danger,reason,resolve}
|
|
1634
1743
|
// 数据浏览浮窗开关:由 Panel 层持有(不随 view 切换卸载),跨 tab 常驻
|
|
1635
1744
|
const [browseOpen, setBrowseOpen] = React.useState(false);
|
|
1745
|
+
// PiP 独立窗口句柄:优先于 DOM 浮窗(OS 级窗口,可拖出桌面端主窗口);存在时 DOM 浮窗不渲染
|
|
1746
|
+
const [pipWin, setPipWin] = React.useState(null);
|
|
1747
|
+
function openBrowse() {
|
|
1748
|
+
if (pipWin) { try { pipWin.focus(); } catch (e) { /* 已关闭 */ } return; }
|
|
1749
|
+
// requestWindow 必须在用户手势内同步发起(本函数仅由 onClick 直调)
|
|
1750
|
+
const p = openBrowsePip(props.ctx, {
|
|
1751
|
+
conns, projectPath, grants,
|
|
1752
|
+
onClosed: () => setPipWin(null),
|
|
1753
|
+
});
|
|
1754
|
+
if (p && typeof p.then === "function") {
|
|
1755
|
+
p.then((w) => { if (w) setPipWin(w); else setBrowseOpen(true); });
|
|
1756
|
+
} else {
|
|
1757
|
+
setBrowseOpen(true);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1636
1760
|
|
|
1637
1761
|
const askConfirm = React.useCallback((info) => new Promise((resolve) => {
|
|
1638
1762
|
setConfirmReq(Object.assign({}, info, { resolve }));
|
|
@@ -1690,7 +1814,7 @@ window.__ModuleLoader__.load({
|
|
|
1690
1814
|
// browse tab:切视图的同时直接打开浮窗(单击即用,不必再进树点表)
|
|
1691
1815
|
React.createElement("button", {
|
|
1692
1816
|
key: v, className: view === v ? "active" : "",
|
|
1693
|
-
onClick: () => { setView(v); if (v === "browse")
|
|
1817
|
+
onClick: () => { setView(v); if (v === "browse") openBrowse(); },
|
|
1694
1818
|
}, label)),
|
|
1695
1819
|
),
|
|
1696
1820
|
(projectEdited || !projectPath)
|
|
@@ -1733,7 +1857,7 @@ window.__ModuleLoader__.load({
|
|
|
1733
1857
|
// 主提示用正常文字色(dbt-muted 11px 次要色压深底对比不足,视觉审查命中)
|
|
1734
1858
|
React.createElement("div", { style: { fontSize: 13 } }, t("browsePanelHint")),
|
|
1735
1859
|
React.createElement("div", { className: "dbt-row" },
|
|
1736
|
-
React.createElement("button", { className: "dbt-btn primary", onClick:
|
|
1860
|
+
React.createElement("button", { className: "dbt-btn primary", onClick: openBrowse }, t("reopenBrowse"))),
|
|
1737
1861
|
) : null,
|
|
1738
1862
|
view === "console" ? React.createElement(ConsoleView, Object.assign({}, shared, { askConfirm })) : null,
|
|
1739
1863
|
),
|
|
@@ -1742,6 +1866,7 @@ window.__ModuleLoader__.load({
|
|
|
1742
1866
|
// 浮窗常驻:portal 到 body 且在 dbt-view 外,切 tab 时不消失
|
|
1743
1867
|
browseOpen ? React.createElement(BrowseDialog, {
|
|
1744
1868
|
conns, projectPath, grants, askConfirm,
|
|
1869
|
+
ctx: props.ctx, // 宿主 ctx 透传给浮窗,供 portal 异步解析(ctx.modules.import)
|
|
1745
1870
|
onClose: () => setBrowseOpen(false),
|
|
1746
1871
|
}) : null,
|
|
1747
1872
|
);
|
|
@@ -1766,6 +1891,7 @@ window.__ModuleLoader__.load({
|
|
|
1766
1891
|
key: "db-tool-" + String(localeKey || "zh"),
|
|
1767
1892
|
visible: props.visible,
|
|
1768
1893
|
scope: props.scope,
|
|
1894
|
+
ctx: ctx, // 宿主 ctx 下传,Panel 透传给 BrowseDialog 供 portal 解析
|
|
1769
1895
|
});
|
|
1770
1896
|
}
|
|
1771
1897
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-db-tool",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.4",
|
|
4
4
|
"description": "DSH community plugin: chat-operated multi-database admin tool with sidebar management, project-scoped grants, ro/rw modes and dangerous-operation confirmation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
},
|
|
39
39
|
"client": {
|
|
40
40
|
"inject": [
|
|
41
|
-
"@deepseek-ai/dsh-client-locale"
|
|
41
|
+
"@deepseek-ai/dsh-client-locale",
|
|
42
|
+
"react-dom"
|
|
42
43
|
],
|
|
43
44
|
"platform": "web"
|
|
44
45
|
}
|