dsh-dbhub-live 5.0.1 → 5.2.0-dev.3
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/AGENTS.md +95 -209
- package/README.en.md +251 -169
- package/README.md +251 -169
- package/doc/ARCHITECTURE.md +108 -0
- package/doc/CLIENT.md +124 -0
- package/doc/DESENSITIZE.md +56 -0
- package/doc/DEVELOPMENT.md +139 -0
- package/doc/IMPACT-MATRIX.md +36 -0
- package/doc/RELEASE.md +93 -0
- package/doc/REQUIREMENTS.md +177 -105
- package/doc/images/chat-query.png +0 -0
- package/doc/images/row-editor.png +0 -0
- package/doc/images/settings-page.png +0 -0
- package/lib/adhoc.mjs +412 -34
- package/lib/capability.mjs +157 -0
- package/lib/client.js +1243 -80
- package/lib/config.mjs +370 -21
- package/lib/i18n.mjs +119 -4
- package/lib/index.mjs +401 -43
- package/lib/mcp.mjs +496 -391
- package/lib/meta.mjs +267 -0
- package/lib/options.mjs +25 -5
- package/lib/policy.mjs +92 -0
- package/lib/toml.mjs +188 -0
- package/lib/tools.mjs +1093 -827
- package/lib/update.mjs +244 -0
- package/package.json +34 -10
- package/scripts/desensitize-check.mjs +10 -3
- package/scripts/desensitize.config.mjs +255 -237
- package/scripts/docs-check.mjs +214 -0
- package/scripts/release-preflight.mjs +180 -0
- package/scripts/render-check.mjs +1366 -843
- package/test/adhoc-toml.test.mjs +365 -0
- package/test/bridge.test.mjs +227 -185
- package/test/capability.test.mjs +231 -0
- package/test/client-format.test.mjs +515 -352
- package/test/configure-options.test.mjs +244 -0
- package/test/configure-ssh.test.mjs +232 -0
- package/test/copy-from.test.mjs +155 -0
- package/test/env-options.test.mjs +456 -0
- package/test/meta.test.mjs +234 -0
- package/test/options.test.mjs +160 -123
- package/test/policy.test.mjs +132 -0
- package/test/row-cache.test.mjs +237 -138
- package/test/ssh-probe.test.mjs +834 -0
- package/test/test-op-fastpath.test.mjs +208 -0
- package/test/toml.test.mjs +352 -0
- package/test/update.test.mjs +270 -0
- package/test/util.test.mjs +17 -0
- package/test/zero-knowledge.test.mjs +120 -78
package/lib/client.js
CHANGED
|
@@ -41,10 +41,49 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
41
41
|
}
|
|
42
42
|
var ChevronIcon = pickIcon("IconChevronDownOutline14", "▼");
|
|
43
43
|
var CopyIcon = pickIcon("IconCopyOutline16", "⧉");
|
|
44
|
-
var EditIcon = pickIcon("IconEditOutline16", "✏");
|
|
45
44
|
var CloseIcon = pickIcon("IconCloseOutline16", "✕");
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
// Row-action glyphs are drawn here instead of taken from the icon set: at 1x the
|
|
46
|
+
// bundled edit glyph reads as an unidentifiable dash, while these three shapes
|
|
47
|
+
// (filled play, pencil, lidded trash) stay recognisable at 16px on any DPI.
|
|
48
|
+
var svgIcon = function (children) {
|
|
49
|
+
return function (props) {
|
|
50
|
+
var size = (props && props.size) || 16;
|
|
51
|
+
return h("svg", {
|
|
52
|
+
className: "dbh-svgicon",
|
|
53
|
+
width: size, height: size, viewBox: "0 0 16 16",
|
|
54
|
+
fill: "none", stroke: "currentColor", strokeWidth: "1.5",
|
|
55
|
+
strokeLinecap: "round", strokeLinejoin: "round",
|
|
56
|
+
"aria-hidden": true, focusable: false,
|
|
57
|
+
}, children);
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
var RunIcon = svgIcon(h("path", {
|
|
61
|
+
key: "play", d: "M5.2 3.3v9.4L12.6 8 5.2 3.3z",
|
|
62
|
+
fill: "currentColor", stroke: "none",
|
|
63
|
+
}));
|
|
64
|
+
var EditIcon = svgIcon([
|
|
65
|
+
h("path", { key: "body", d: "M10.9 2.6a1.6 1.6 0 0 1 2.3 2.3L5.6 12.5l-3 .7.7-3 7.6-7.6z" }),
|
|
66
|
+
h("path", { key: "tip", d: "M9.7 3.8l2.3 2.3" }),
|
|
67
|
+
]);
|
|
68
|
+
var TrashIcon = svgIcon([
|
|
69
|
+
h("path", { key: "lid", d: "M3 4.5h10" }),
|
|
70
|
+
h("path", { key: "handle", d: "M6.3 4.5V3.1h3.4v1.4" }),
|
|
71
|
+
h("path", { key: "body", d: "M4.5 4.5l.6 8.6h5.8l.6-8.6" }),
|
|
72
|
+
h("path", { key: "line1", d: "M6.6 7v4.2" }),
|
|
73
|
+
h("path", { key: "line2", d: "M9.4 7v4.2" }),
|
|
74
|
+
]);
|
|
75
|
+
// Read-only state: the user asked for the universally recognised emoji lock
|
|
76
|
+
// pair — 🔓 when writable, 🔒 when writes are blocked. The hand-drawn 16px
|
|
77
|
+
// shackle was too small to tell open from closed at a glance.
|
|
78
|
+
// The refresh glyph is the update check's affordance: a named primitive when
|
|
79
|
+
// the running dsh ships it, a text glyph otherwise (a rename must not blank
|
|
80
|
+
// the button).
|
|
81
|
+
var RefreshIcon = pickIcon("IconRefreshOutlineRegular", "↻");
|
|
82
|
+
// The platform's own toggle: same geometry, tokens and 120ms thumb animation
|
|
83
|
+
// as the harness settings pages, so it follows every theme/skin. A local
|
|
84
|
+
// fallback with the same geometry keeps the control alive if a baseline build
|
|
85
|
+
// predates the primitive.
|
|
86
|
+
var SwitchPrimitive = primitives && typeof primitives.Switch === "function" ? primitives.Switch : null;
|
|
48
87
|
|
|
49
88
|
var NS = "dsh-dbhub-live";
|
|
50
89
|
|
|
@@ -67,7 +106,83 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
67
106
|
"cfg.off": "隐藏",
|
|
68
107
|
"cfg.update": "自动更新间隔(天)",
|
|
69
108
|
"cfg.sidebar": "在侧边栏显示入口",
|
|
70
|
-
"cfg.
|
|
109
|
+
"cfg.policy": "向模型注入数据库访问策略",
|
|
110
|
+
"cfg.policyHint": "开启后,系统提示词会声明「本插件已配置的连接一律走 dbhub_* 工具,不要用命令行客户端/脚本/驱动或其它插件绕过」,并说明本插件不支持的库可以用你安装的其它数据库插件。关闭只是不再注入这段文字——工具、只读与其它设置全部照常工作。",
|
|
111
|
+
"cfg.sidebarHint": "关闭后左侧栏不再出现「DBHub 数据库工具」快捷入口——但插件照常工作(工具仍可用),本设置页与插件页也始终可进入。注意:点「禁用」会让入口无条件消失,与这个开关无关。",
|
|
112
|
+
"cfg.enabledHint": "侧边栏入口 = 本开关 且 插件处于启用状态。",
|
|
113
|
+
"cfg.disabledHint": "插件已禁用:侧边栏入口已隐藏(与上面的开关无关)。本设置页与插件页仍然可用——点上方的「启用」即可恢复。",
|
|
114
|
+
"cfg.enableHint": "停用后模型无法调用本插件的工具;连接与配置全部保留,随时可以再启用。",
|
|
115
|
+
"cfg.updateHint": "dbhub 可执行文件的自动更新间隔(天);0 = 不自动更新。",
|
|
116
|
+
// Modern surfaces (5.2 redesign): one compact status line, DSH-style settings
|
|
117
|
+
// rows, an on-demand update link, and a collapsed technical-details block.
|
|
118
|
+
"status.conns": "{n} 个连接",
|
|
119
|
+
"settings.title": "插件设置",
|
|
120
|
+
"settings.toggle": "设置",
|
|
121
|
+
"settings.more": "说明与技术详情",
|
|
122
|
+
"settings.notes": "说明",
|
|
123
|
+
"settings.less": "收起说明",
|
|
124
|
+
"row.enable": "启用插件",
|
|
125
|
+
"row.sidebar": "侧边栏入口",
|
|
126
|
+
"row.policy": "向模型注入访问策略",
|
|
127
|
+
"row.update": "dbhub 自动更新",
|
|
128
|
+
"state.enable": "启用",
|
|
129
|
+
"state.disable": "停用",
|
|
130
|
+
"state.show": "显示",
|
|
131
|
+
"state.hide": "隐藏",
|
|
132
|
+
"state.on": "开启",
|
|
133
|
+
"state.off": "关闭",
|
|
134
|
+
"unit.days": "天",
|
|
135
|
+
"note.tools": "模型侧固定 {n} 个工具;每次调用使用一次性 dbhub 进程,无常驻服务。",
|
|
136
|
+
"note.update": "“dbhub 自动更新”只控制 dbhub 可执行文件;插件自身的版本更新走页头的升级提示。",
|
|
137
|
+
"note.sidebar": "关闭侧边栏入口只是隐藏快捷入口,插件仍然工作;停用插件才会让模型无法调用工具。",
|
|
138
|
+
"note.policy": "开启后,系统提示词会声明「本插件已配置的连接一律走 dbhub_* 工具,不要用命令行客户端/脚本/驱动或其它插件绕过」,并说明本插件不支持的库可以用你安装的其它数据库插件。关闭只是不再注入这段文字,工具、只读与其它设置全部照常工作。",
|
|
139
|
+
"note.version": "插件 v{plugin} · DSH {dsh} · {runtime}",
|
|
140
|
+
"ver.link": "可升级到 {v}",
|
|
141
|
+
"ro.chipOnHint": "只读已开启,模型无法写入;点此关闭",
|
|
142
|
+
"ro.chipOffHint": "开启只读,阻止模型写入(模型只能开、不能关)",
|
|
143
|
+
"ro.chip": "只读",
|
|
144
|
+
"ro.writable": "可写",
|
|
145
|
+
"ver.copySpec": "点击复制 dsh-dbhub-live@{v}",
|
|
146
|
+
"ver.check": "检查更新",
|
|
147
|
+
"ver.checking": "检查中…",
|
|
148
|
+
"ver.checkFailed": "检查更新失败",
|
|
149
|
+
"ver.checkedAgo": "检查于 {n} 分钟前",
|
|
150
|
+
"ver.webHint": "Web 版:在终端执行 dsh plugin --profile web up dsh-dbhub-live@{v},然后重启 dsh web。",
|
|
151
|
+
"ver.desktopHint": "桌面版:打开左侧菜单 → 插件,卸载 dsh-dbhub-live 后重新安装最新版;连接与凭据保存在 ~/.dsh/storages/dsh-dbhub-live,不会被删除。",
|
|
152
|
+
"ver.genericHint": "Web 版执行 dsh plugin --profile web up dsh-dbhub-live@{v} 后重启 dsh web;桌面版在插件页卸载后重新安装。",
|
|
153
|
+
"ver.releaseNotes": "更新说明",
|
|
154
|
+
"ver.copy": "复制",
|
|
155
|
+
"ver.copyCmd": "复制命令",
|
|
156
|
+
"ver.runtimeDesktop": "桌面版",
|
|
157
|
+
"ver.runtimeWeb": "Web 版",
|
|
158
|
+
"ro.turnOn": "开启只读",
|
|
159
|
+
"ro.turnOff": "关闭只读",
|
|
160
|
+
"cap.tooOld": "dbhub {v} 不支持只读 / SSH 隧道(需要 1.4.0 及以上)",
|
|
161
|
+
"cap.untested": "dbhub {v} 未经验证(只读 / SSH 在 1.4.0+ 实测)",
|
|
162
|
+
"add.readOnly": "只读模式",
|
|
163
|
+
"ro.chip": "只读",
|
|
164
|
+
"ro.title": "只读模式:dbhub 只放行只读语句,写操作会被拒绝(READONLY_VIOLATION)",
|
|
165
|
+
"ro.sshTitle": "SSH 隧道 {host}:{port}(用户 {user})",
|
|
166
|
+
"add.adv": "高级:SSH 隧道",
|
|
167
|
+
"adv.enable": "经 SSH 隧道连接",
|
|
168
|
+
"adv.host": "SSH 主机",
|
|
169
|
+
"adv.port": "端口",
|
|
170
|
+
"adv.user": "SSH 用户",
|
|
171
|
+
"adv.auth": "认证方式",
|
|
172
|
+
"adv.auth.key": "密钥",
|
|
173
|
+
"adv.auth.password": "密码",
|
|
174
|
+
"adv.keyPath": "密钥路径",
|
|
175
|
+
"adv.passphrase": "密钥口令",
|
|
176
|
+
"adv.password": "SSH 密码",
|
|
177
|
+
"adv.proxyJump": "ProxyJump(可选,单跳)",
|
|
178
|
+
"adv.keepSecret": "留空 = 保持原值",
|
|
179
|
+
"adv.hostHint": "主机名请填域名或 IP(不要填 ~/.ssh/config 里的别名);库地址仍来自 DSN,要写“从跳板机看过去”的地址。",
|
|
180
|
+
"adv.slow": "SSH 隧道首次连接较慢,请稍候。",
|
|
181
|
+
"btn.testConn": "测试该配置",
|
|
182
|
+
"btn.testSsh": "测试 SSH 隧道",
|
|
183
|
+
"adv.testSshHint": "只测 SSH 层:先看跳板机端口通不通、密钥文件在不在,再让 dbhub 真的建一次隧道(故意连一个必然关闭的端口——数据库那步失败才说明隧道通了)。",
|
|
184
|
+
"adv.testSshNeedFields": "请先填 SSH 主机与用户(并选择认证方式),再测试隧道。",
|
|
185
|
+
"btn.saveOpts": "保存选项",
|
|
71
186
|
"btn.save": "保存配置",
|
|
72
187
|
"btn.add": "添加连接",
|
|
73
188
|
"btn.addConn": "+ 添加连接",
|
|
@@ -85,6 +200,7 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
85
200
|
"btn.enable": "启用",
|
|
86
201
|
"btn.test": "测试",
|
|
87
202
|
"test.testing": "测试中…",
|
|
203
|
+
"test.elapsed": "测试中… {s}s",
|
|
88
204
|
"test.timeout": "测试超时未返回",
|
|
89
205
|
"saved.ok": "已保存 ✓",
|
|
90
206
|
"badge.saved": "已保存",
|
|
@@ -105,6 +221,7 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
105
221
|
"src.user": "已保存·用户",
|
|
106
222
|
"src.collected": "已保存·扫描",
|
|
107
223
|
"src.copied": "已保存·复制",
|
|
224
|
+
"src.promoted": "已保存·选项提升",
|
|
108
225
|
"src.mise": "自动·mise env",
|
|
109
226
|
"src.env": "自动·.env",
|
|
110
227
|
"pretest.title": "校验并保存连接",
|
|
@@ -139,7 +256,81 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
139
256
|
"cfg.off": "hidden",
|
|
140
257
|
"cfg.update": "Update interval (days)",
|
|
141
258
|
"cfg.sidebar": "Show the sidebar entry",
|
|
142
|
-
"cfg.
|
|
259
|
+
"cfg.policy": "Announce the database-access policy to the model",
|
|
260
|
+
"cfg.policyHint": "When on, the system prompt states that this plugin's configured connections go through the dbhub_* tools (no command-line client, script, driver or other plugin detour) and that databases it does not cover may use your other database plugin. Turning it off only removes that text — tools, read-only and every other setting keep working.",
|
|
261
|
+
"cfg.sidebarHint": "When off, the “DBHub Database Tools” shortcut no longer appears in the sidebar — the plugin keeps working (its tools stay available) and this settings page plus the Plugins row stay reachable. Note: pressing Disable hides the entry unconditionally, independently of this switch.",
|
|
262
|
+
"cfg.enabledHint": "The sidebar entry = this switch AND the plugin being enabled.",
|
|
263
|
+
"cfg.disabledHint": "The plugin is disabled, so the sidebar entry is hidden (independently of the switch above). This settings page and the Plugins row still work — press Enable above to bring it back.",
|
|
264
|
+
"cfg.enableHint": "While disabled the model cannot call this plugin's tools; every connection and setting is kept and can be re-enabled at any time.",
|
|
265
|
+
"cfg.updateHint": "Auto-update interval (days) for the dbhub executable; 0 = never update automatically.",
|
|
266
|
+
"status.conns": "{n} connections",
|
|
267
|
+
"settings.title": "Plugin settings",
|
|
268
|
+
"settings.toggle": "Settings",
|
|
269
|
+
"settings.more": "Details & diagnostics",
|
|
270
|
+
"settings.notes": "Details",
|
|
271
|
+
"settings.less": "Hide details",
|
|
272
|
+
"row.enable": "Enable plugin",
|
|
273
|
+
"row.sidebar": "Sidebar entry",
|
|
274
|
+
"row.policy": "Inject database access policy into the model",
|
|
275
|
+
"row.update": "dbhub auto-update",
|
|
276
|
+
"state.enable": "Enabled",
|
|
277
|
+
"state.disable": "Disabled",
|
|
278
|
+
"state.show": "Shown",
|
|
279
|
+
"state.hide": "Hidden",
|
|
280
|
+
"state.on": "On",
|
|
281
|
+
"state.off": "Off",
|
|
282
|
+
"unit.days": "days",
|
|
283
|
+
"note.tools": "The model always sees {n} tools; every call runs a one-shot dbhub process (no resident service).",
|
|
284
|
+
"note.update": "The dbhub auto-update interval only controls the dbhub executable; the plugin's own update is offered by the header link.",
|
|
285
|
+
"note.sidebar": "Hiding the sidebar entry only removes the shortcut — the plugin keeps working. Only disabling the plugin stops the model from calling its tools.",
|
|
286
|
+
"note.policy": "When on, the system prompt states that every connection configured here goes through the dbhub_* tools (no CLI clients, scripts, drivers or other plugins), and that databases this plugin does not support may use other database plugins the user installed. Turning it off removes only that text; tools, read-only and every other setting keep working.",
|
|
287
|
+
"note.version": "Plugin v{plugin} · DSH {dsh} · {runtime}",
|
|
288
|
+
"ver.link": "Upgrade to {v}",
|
|
289
|
+
"ro.chipOnHint": "Read-only is on and the model cannot write; click to turn it off",
|
|
290
|
+
"ro.chipOffHint": "Turn read-only on to block model writes (the model can only turn it on, never off)",
|
|
291
|
+
"ro.chip": "RO",
|
|
292
|
+
"ro.writable": "Writable",
|
|
293
|
+
"ver.copySpec": "Click to copy dsh-dbhub-live@{v}",
|
|
294
|
+
"ver.check": "Check for updates",
|
|
295
|
+
"ver.checking": "Checking…",
|
|
296
|
+
"ver.checkFailed": "Update check failed",
|
|
297
|
+
"ver.checkedAgo": "Checked {n} min ago",
|
|
298
|
+
"ver.webHint": "Web: run dsh plugin --profile web up dsh-dbhub-live@{v} in a terminal, then restart dsh web.",
|
|
299
|
+
"ver.desktopHint": "Desktop: open Plugins in the left menu, uninstall dsh-dbhub-live, then install the latest version again. Connections and credentials live in ~/.dsh/storages/dsh-dbhub-live and are preserved.",
|
|
300
|
+
"ver.genericHint": "Web: run dsh plugin --profile web up dsh-dbhub-live@{v} and restart dsh web. Desktop: uninstall the plugin on the Plugins page, then reinstall it.",
|
|
301
|
+
"ver.releaseNotes": "Release notes",
|
|
302
|
+
"ver.copy": "Copy",
|
|
303
|
+
"ver.copyCmd": "Copy command",
|
|
304
|
+
"ver.runtimeDesktop": "Desktop",
|
|
305
|
+
"ver.runtimeWeb": "Web",
|
|
306
|
+
"ro.turnOn": "Turn read-only on",
|
|
307
|
+
"ro.turnOff": "Turn read-only off",
|
|
308
|
+
"cap.tooOld": "dbhub {v} does not support read-only / SSH tunnels (1.4.0 or newer required)",
|
|
309
|
+
"cap.untested": "dbhub {v} is untested (read-only / SSH were measured on 1.4.0+)",
|
|
310
|
+
"add.readOnly": "Read-only mode",
|
|
311
|
+
"ro.chip": "RO",
|
|
312
|
+
"ro.title": "Read-only mode: dbhub only allows read statements and rejects writes (READONLY_VIOLATION)",
|
|
313
|
+
"ro.sshTitle": "SSH tunnel {host}:{port} (user {user})",
|
|
314
|
+
"add.adv": "Advanced: SSH tunnel",
|
|
315
|
+
"adv.enable": "Connect through an SSH tunnel",
|
|
316
|
+
"adv.host": "SSH host",
|
|
317
|
+
"adv.port": "Port",
|
|
318
|
+
"adv.user": "SSH user",
|
|
319
|
+
"adv.auth": "Auth",
|
|
320
|
+
"adv.auth.key": "Key",
|
|
321
|
+
"adv.auth.password": "Password",
|
|
322
|
+
"adv.keyPath": "Key path",
|
|
323
|
+
"adv.passphrase": "Key passphrase",
|
|
324
|
+
"adv.password": "SSH password",
|
|
325
|
+
"adv.proxyJump": "ProxyJump (optional, single hop)",
|
|
326
|
+
"adv.keepSecret": "blank = keep the stored value",
|
|
327
|
+
"adv.hostHint": "Use a domain or IP (never an ~/.ssh/config alias); the database address still comes from the DSN and must be the one the bastion sees.",
|
|
328
|
+
"adv.slow": "The first connection through an SSH tunnel is slow — please wait.",
|
|
329
|
+
"btn.testConn": "Test this configuration",
|
|
330
|
+
"btn.testSsh": "Test SSH tunnel",
|
|
331
|
+
"adv.testSshHint": "Tests the SSH layer ONLY: first whether the bastion's port is reachable and the key file exists, then whether dbhub can bring the tunnel up (it deliberately aims at a port that must be closed — the database step failing is what proves the tunnel worked).",
|
|
332
|
+
"adv.testSshNeedFields": "Fill in the SSH host and user first (and pick an auth method), then test the tunnel.",
|
|
333
|
+
"btn.saveOpts": "Save options",
|
|
143
334
|
"btn.save": "Save",
|
|
144
335
|
"btn.add": "Add connection",
|
|
145
336
|
"btn.addConn": "+ Add connection",
|
|
@@ -157,6 +348,7 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
157
348
|
"btn.enable": "Enable",
|
|
158
349
|
"btn.test": "Test",
|
|
159
350
|
"test.testing": "testing…",
|
|
351
|
+
"test.elapsed": "testing… {s}s",
|
|
160
352
|
"test.timeout": "no test result (timed out)",
|
|
161
353
|
"saved.ok": "Saved ✓",
|
|
162
354
|
"badge.saved": "saved",
|
|
@@ -177,6 +369,7 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
177
369
|
"src.user": "saved · user",
|
|
178
370
|
"src.collected": "saved · scan",
|
|
179
371
|
"src.copied": "saved · copied",
|
|
372
|
+
"src.promoted": "saved · promoted",
|
|
180
373
|
"src.mise": "auto · mise env",
|
|
181
374
|
"src.env": "auto · .env",
|
|
182
375
|
"pretest.title": "Validate & Save Connection",
|
|
@@ -234,6 +427,9 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
234
427
|
"persisted(user)": "src.user",
|
|
235
428
|
"persisted(collected)": "src.collected",
|
|
236
429
|
"persisted(copied)": "src.copied",
|
|
430
|
+
// An auto-discovered row that the user (or the model) explicitly gave an
|
|
431
|
+
// option to is persisted with this provenance (D10).
|
|
432
|
+
"persisted(promoted)": "src.promoted",
|
|
237
433
|
"工作区 mise env": "src.mise",
|
|
238
434
|
"工作目录 .env": "src.env",
|
|
239
435
|
};
|
|
@@ -268,6 +464,16 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
268
464
|
|
|
269
465
|
var STYLE_ID = "dsh-dbhub-live-style";
|
|
270
466
|
|
|
467
|
+
// Client-side watchdogs for a connection probe. The Host answers every probe
|
|
468
|
+
// (success, failure, or its own timeout), so these only cover "the report
|
|
469
|
+
// never arrived". The tunnelled tier is longer because SSH setup is slow; the
|
|
470
|
+
// HOST budget (adhoc.mjs PROBE_TIMEOUT_MS / PROBE_TIMEOUT_SSH_MS) stays
|
|
471
|
+
// strictly below its client counterpart — asserted in test/adhoc-toml.test.mjs.
|
|
472
|
+
var PRETEST_WATCHDOG_MS = 30000;
|
|
473
|
+
var PRETEST_WATCHDOG_SSH_MS = 60000;
|
|
474
|
+
var TEST_WATCHDOG_MS = 30000;
|
|
475
|
+
var TEST_WATCHDOG_SSH_MS = 60000;
|
|
476
|
+
|
|
271
477
|
var CSS_TEXT = [
|
|
272
478
|
".dbh-wrap{box-sizing:border-box;height:100%;width:100%;overflow:auto;padding:22px clamp(14px,3vw,32px) 44px;color:var(--dsw-alias-label-primary);font-family:inherit;}",
|
|
273
479
|
// Content column: capped at 960px, which also caps the tile grid at 3 columns.
|
|
@@ -294,8 +500,8 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
294
500
|
".dbh-iconbtn:disabled{cursor:default;}",
|
|
295
501
|
".dbh-iconbtn-busy{opacity:.5;}",
|
|
296
502
|
".dbh-iconbtn-danger:hover:not(:disabled){color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover-danger);}",
|
|
297
|
-
".dbh-sect{display:flex;align-items:center;gap:8px;padding:
|
|
298
|
-
".dbh-secttitle{font-size:
|
|
503
|
+
".dbh-sect{display:flex;align-items:center;gap:8px;padding:14px 0 2px;flex-wrap:wrap;}",
|
|
504
|
+
".dbh-secttitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);letter-spacing:.01em;}",
|
|
299
505
|
".dbh-sectcount{font-size:11px;color:var(--dsw-alias-label-tertiary);}",
|
|
300
506
|
".dbh-grow{flex:1;min-width:0;}",
|
|
301
507
|
// Level 1: one bordered card per workspace. Transparent surface + hairline
|
|
@@ -325,6 +531,7 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
325
531
|
".dbh-conn-pre{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);}",
|
|
326
532
|
".dbh-conn-db{flex:none;color:var(--dsw-alias-label-primary);font-weight:600;}",
|
|
327
533
|
".dbh-acts{display:flex;align-items:center;gap:2px;flex:none;margin-left:auto;}",
|
|
534
|
+
".dbh-svgicon{display:block;flex:none;pointer-events:none;}",
|
|
328
535
|
// The source handle: dashed outline + copy glyph says "id, click to copy".
|
|
329
536
|
".dbh-src{display:inline-flex;align-items:center;gap:4px;flex:none;height:22px;padding:0 6px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;color:var(--dsw-alias-label-secondary);background:transparent;border:1px dashed var(--dsw-alias-border-l2,rgba(127,127,127,.45));border-radius:6px;cursor:pointer;transition:background .14s,border-color .14s,color .14s;}",
|
|
330
537
|
".dbh-src:hover{background:var(--dsw-alias-interactive-bg-hover);border-style:solid;color:var(--dsw-alias-label-primary);}",
|
|
@@ -332,6 +539,24 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
332
539
|
".dbh-src-ok{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-tertiary);border-style:solid;}",
|
|
333
540
|
".dbh-srcid{max-width:96px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
334
541
|
".dbh-editrow{display:flex;align-items:center;gap:6px;padding:4px 0 6px;flex-wrap:wrap;}",
|
|
542
|
+
// Editor block: the input row plus its advanced (SSH) section, so the row
|
|
543
|
+
// still contributes exactly ONE sibling to the environment wrapper.
|
|
544
|
+
".dbh-editbox{display:flex;flex-direction:column;gap:6px;padding:4px 0 6px;}",
|
|
545
|
+
// Read-only marker inside the environment chip: an attribute that differs
|
|
546
|
+
// per row and CHANGES the semantics of every query, so it must be visible
|
|
547
|
+
// at a glance (the origin/source is the opposite case and stays a tooltip).
|
|
548
|
+
".dbh-ro{flex:none;margin-left:4px;font-size:10px;font-weight:500;color:var(--dsw-alias-state-warn-primary);}",
|
|
549
|
+
// Checkboxes are styled by the platform's own input rendering; only the
|
|
550
|
+
// accent colour is themed, and the neutral fallback is a translucent grey.
|
|
551
|
+
".dbh-checkbox{flex:none;accent-color:var(--dsw-alias-brand-primary,rgba(127,127,127,.8));}",
|
|
552
|
+
".dbh-adv{display:flex;flex-direction:column;gap:6px;}",
|
|
553
|
+
".dbh-advtoggle{align-self:flex-start;display:inline-flex;align-items:center;gap:4px;height:24px;padding:0 8px;font-size:12px;font-family:inherit;line-height:1;border:1px dashed var(--dsw-alias-border-l2,rgba(127,127,127,.45));border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;}",
|
|
554
|
+
".dbh-advtoggle:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
|
|
555
|
+
".dbh-advtoggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px;}",
|
|
556
|
+
".dbh-advbody{display:flex;flex-direction:column;gap:6px;margin-left:6px;padding-left:10px;border-left:2px solid var(--dsw-alias-border-l1,rgba(127,127,127,.3));}",
|
|
557
|
+
".dbh-advrow{display:flex;align-items:center;gap:8px;}",
|
|
558
|
+
".dbh-advlabel{flex:0 0 92px;font-size:12px;color:var(--dsw-alias-label-secondary);}",
|
|
559
|
+
".dbh-advrow .dbh-input{flex:1;}",
|
|
335
560
|
".dbh-input{box-sizing:border-box;height:28px;min-width:0;padding:0 8px;font-size:12px;font-family:inherit;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3,transparent);border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.45));border-radius:6px;}",
|
|
336
561
|
".dbh-input:focus{outline:none;border-color:var(--dsw-alias-brand-primary);}",
|
|
337
562
|
".dbh-envinput{flex:0 0 130px;}",
|
|
@@ -357,8 +582,8 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
357
582
|
".dbh-hint{font-size:11px;color:var(--dsw-alias-label-tertiary);line-height:1.5;}",
|
|
358
583
|
".dbh-empty{font-size:12px;color:var(--dsw-alias-label-tertiary);padding:8px 0;}",
|
|
359
584
|
".dbh-saved{font-size:12px;color:var(--dsw-alias-state-success-primary);}",
|
|
360
|
-
".dbh-test-ok{font-size:12px;color:var(--dsw-alias-state-success-primary);word-break:break-
|
|
361
|
-
".dbh-test-fail{font-size:12px;color:var(--dsw-alias-state-error-primary);word-break:break-
|
|
585
|
+
".dbh-test-ok{font-size:12px;color:var(--dsw-alias-state-success-primary);word-break:break-word;white-space:pre-wrap;}",
|
|
586
|
+
".dbh-test-fail{font-size:12px;color:var(--dsw-alias-state-error-primary);word-break:break-word;white-space:pre-wrap;}",
|
|
362
587
|
".dbh-pretest{display:flex;flex-direction:column;gap:6px;padding:10px;border:1px solid var(--dsw-alias-state-error-primary);border-radius:8px;background:var(--dsw-alias-bg-layer-2,transparent);}",
|
|
363
588
|
".dbh-pretest-title{font-weight:600;font-size:13px;color:var(--dsw-alias-state-error-primary);}",
|
|
364
589
|
// Overwrite confirmation: a warning, not an error — the write is still the
|
|
@@ -366,6 +591,115 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
366
591
|
".dbh-warncard{display:flex;flex-direction:column;gap:6px;padding:10px;border:1px solid var(--dsw-alias-state-warn-primary,rgba(127,127,127,.6));border-radius:8px;background:var(--dsw-alias-bg-layer-2,transparent);}",
|
|
367
592
|
".dbh-warncard-title{font-weight:600;font-size:13px;color:var(--dsw-alias-state-warn-primary);}",
|
|
368
593
|
".dbh-summary{font-size:12px;color:var(--dsw-alias-label-secondary);}",
|
|
594
|
+
// ── modern surfaces (dsh >= 0.2.1) ──────────────────────────────────────
|
|
595
|
+
// One component, three surfaces: the settings dialog (narrow, unchanged from
|
|
596
|
+
// the 0.1.x layout), the wide page reached from the left menu / the Plugins
|
|
597
|
+
// row, and a phone browser. The page classes come from the MEASURED
|
|
598
|
+
// container width, so a docked desktop window degrades like a phone does.
|
|
599
|
+
".dbh-titlebar{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;padding:2px 0 2px;}",
|
|
600
|
+
".dbh-title{margin:0;font-size:18px;font-weight:600;line-height:1.25;color:var(--dsw-alias-label-primary);}",
|
|
601
|
+
".dbh-ver{flex:none;font-size:12px;color:var(--dsw-alias-label-tertiary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;}",
|
|
602
|
+
// Update affordance: a quiet text link (brand colour only) that expands into a
|
|
603
|
+
// neutral panel on demand — never a red/error-styled banner.
|
|
604
|
+
".dbh-updatelink{display:inline-flex;align-items:center;gap:3px;height:22px;padding:0 4px;font-size:12px;font-family:inherit;line-height:1;border:1px solid transparent;border-radius:6px;background:transparent;color:var(--dsw-alias-brand-text);cursor:pointer;}",
|
|
605
|
+
".dbh-updatelink:hover{text-decoration:underline;}",
|
|
606
|
+
".dbh-updatelink:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px;}",
|
|
607
|
+
".dbh-vercheck{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;padding:0;font-size:11px;font-family:inherit;line-height:1;border:1px solid transparent;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;}",
|
|
608
|
+
".dbh-vercheck:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
|
|
609
|
+
".dbh-vercheck:disabled{cursor:default;opacity:.7;}",
|
|
610
|
+
".dbh-vercheck:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px;}",
|
|
611
|
+
// One status line: running/disabled + how many connections. Tool count and the
|
|
612
|
+
// one-shot execution mode are implementation facts and live in the notes.
|
|
613
|
+
".dbh-statusline{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);padding:0 0 6px;}",
|
|
614
|
+
".dbh-dot{flex:none;width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-state-success-primary);}",
|
|
615
|
+
".dbh-dot-off{background:var(--dsw-alias-state-idle-primary);}",
|
|
616
|
+
".dbh-statussep{color:var(--dsw-alias-label-tertiary);}",
|
|
617
|
+
".dbh-linkbtn{display:inline-flex;align-items:center;gap:3px;padding:0 4px;height:20px;border:1px solid transparent;border-radius:6px;background:transparent;font-family:inherit;font-size:12px;line-height:1;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;}",
|
|
618
|
+
".dbh-linkbtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
|
|
619
|
+
".dbh-linkbtn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px;}",
|
|
620
|
+
".dbh-updatepanel{display:flex;flex-direction:column;gap:6px;margin:0 0 8px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.3));border-radius:8px;background:var(--dsw-alias-bg-layer-2,transparent);}",
|
|
621
|
+
".dbh-updaterow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0;}",
|
|
622
|
+
".dbh-spec{flex:1 1 auto;min-width:0;padding:3px 8px;border-radius:6px;background:var(--dsw-alias-bg-layer-3,transparent);border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.3));font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
623
|
+
".dbh-link{font-size:11px;color:var(--dsw-alias-brand-text);text-decoration:none;align-self:flex-start;}",
|
|
624
|
+
".dbh-link:hover{text-decoration:underline;}",
|
|
625
|
+
// Settings: the harness's own row pattern — title left, control right, thin
|
|
626
|
+
// separators, no card/grid, no coloured chrome of our own.
|
|
627
|
+
".dbh-inner.dbh-wide{max-width:1320px;gap:12px;}",
|
|
628
|
+
".dbh-wide .dbh-tiles{grid-template-columns:repeat(auto-fill,minmax(320px,1fr));}",
|
|
629
|
+
".dbh-settings{display:flex;flex-direction:column;}",
|
|
630
|
+
".dbh-settings-title{font-size:11px;font-weight:600;color:var(--dsw-alias-label-tertiary);letter-spacing:.02em;padding:2px 0 2px;}",
|
|
631
|
+
// Wide page: all four controls on one quiet line — the labels stay attached
|
|
632
|
+
// to their switches instead of a label at the far left and a control at the
|
|
633
|
+
// far right with half a screen of empty middle.
|
|
634
|
+
".dbh-settingsbar{padding:0 0 2px;}",
|
|
635
|
+
".dbh-barline{display:flex;align-items:center;gap:8px 22px;flex-wrap:wrap;padding:2px 0 4px;}",
|
|
636
|
+
".dbh-bartitle{font-size:11px;font-weight:600;color:var(--dsw-alias-label-tertiary);letter-spacing:.02em;margin-right:2px;}",
|
|
637
|
+
".dbh-bargroup{display:inline-flex;align-items:center;gap:6px;flex:none;}",
|
|
638
|
+
".dbh-barlabel{font-size:12px;color:var(--dsw-alias-label-secondary);}",
|
|
639
|
+
".dbh-barstate{font-size:12px;color:var(--dsw-alias-label-tertiary);}",
|
|
640
|
+
".dbh-settingsbar [role=\"switch\"]{transform:scale(.85);}",
|
|
641
|
+
// Narrow desktop column: drop the section title and the state words, tighten
|
|
642
|
+
// the gaps, so the four controls still flow on one/two lines instead of
|
|
643
|
+
// collapsing into a vertical list.
|
|
644
|
+
".dbh-barnarrow .dbh-barline{gap:6px 12px;}",
|
|
645
|
+
".dbh-barnarrow .dbh-bartitle{display:none;}",
|
|
646
|
+
".dbh-barnarrow .dbh-barstate{display:none;}",
|
|
647
|
+
".dbh-barnarrow .dbh-barlabel{font-size:11px;}",
|
|
648
|
+
".dbh-barnarrow .dbh-srow-input{width:50px;}",
|
|
649
|
+
// Narrow surfaces: the same controls as compact rows (weak by design — the
|
|
650
|
+
// connection list below is the main content).
|
|
651
|
+
".dbh-srow{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:8px 0;border-bottom:0.5px solid var(--dsw-alias-border-l2,rgba(127,127,127,.3));}",
|
|
652
|
+
".dbh-srow:last-child{border-bottom:none;}",
|
|
653
|
+
".dbh-srow-main{min-width:0;}",
|
|
654
|
+
".dbh-srow-title{font-size:13px;line-height:18px;color:var(--dsw-alias-label-secondary);}",
|
|
655
|
+
".dbh-srow-ctl{display:flex;align-items:center;gap:8px;flex:none;flex-wrap:nowrap;}",
|
|
656
|
+
".dbh-srow-state{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);min-width:2.4em;text-align:right;white-space:nowrap;}",
|
|
657
|
+
".dbh-srow-input{width:58px;height:26px;flex:none;text-align:center;}",
|
|
658
|
+
".dbh-srow-unit{font-size:12px;color:var(--dsw-alias-label-secondary);}",
|
|
659
|
+
".dbh-btn-sm{height:26px;padding:0 10px;font-size:12px;}",
|
|
660
|
+
".dbh-settings-foot{display:flex;align-items:center;gap:8px;padding:6px 0 2px;}",
|
|
661
|
+
".dbh-notes{display:flex;flex-direction:column;gap:4px;margin:2px 0 4px;padding:10px 12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2,transparent);font-size:12px;line-height:1.55;color:var(--dsw-alias-label-secondary);}",
|
|
662
|
+
// Read-only toggle: a quiet chip on the connection row, in EVERY layout, so it
|
|
663
|
+
// can be flipped while reading the list instead of opening the edit form.
|
|
664
|
+
".dbh-rochip{display:inline-flex;align-items:center;gap:4px;height:20px;padding:0 7px;flex:none;border-radius:999px;border:1px dashed var(--dsw-alias-border-l2,rgba(127,127,127,.45));background:transparent;color:var(--dsw-alias-label-tertiary);font-size:11px;font-family:inherit;line-height:1;cursor:pointer;transition:background .12s,border-color .12s,color .12s;}",
|
|
665
|
+
".dbh-rochip:hover:not(:disabled){border-color:var(--dsw-alias-border-l3,rgba(127,127,127,.6));color:var(--dsw-alias-label-secondary);}",
|
|
666
|
+
".dbh-rochip-on{border-style:solid;border-color:var(--dsw-alias-border-l3,rgba(127,127,127,.6));background:var(--dsw-alias-bg-layer-3,rgba(127,127,127,.16));color:var(--dsw-alias-label-primary);}",
|
|
667
|
+
".dbh-rochip:disabled{opacity:.55;cursor:default;}",
|
|
668
|
+
".dbh-rochip:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px;}",
|
|
669
|
+
".dbh-roemoji{font-size:13px;line-height:1;}",
|
|
670
|
+
// Fallback switch only when the baseline predates the Switch primitive: same
|
|
671
|
+
// 36x20 geometry and token colours as the platform control.
|
|
672
|
+
".dbh-fbswitch{position:relative;flex:none;width:36px;height:20px;padding:2px;border:0;border-radius:999px;background:var(--dsw-alias-border-l3,rgba(127,127,127,.45));cursor:pointer;transition:background .12s;}",
|
|
673
|
+
".dbh-fbswitch-on{background:var(--dsw-alias-brand-primary);}",
|
|
674
|
+
".dbh-fbswitch:disabled{opacity:.5;cursor:default;}",
|
|
675
|
+
".dbh-fbswitch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;}",
|
|
676
|
+
".dbh-fbswitch-thumb{display:block;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-label-primary-foreground);transition:transform .12s ease;}",
|
|
677
|
+
".dbh-fbswitch-on .dbh-fbswitch-thumb{transform:translateX(16px);}",
|
|
678
|
+
// Phone browser: one column, touch-sized controls, actions on their own row.
|
|
679
|
+
".dbh-inner.dbh-mobile{font-size:13px;gap:8px;}",
|
|
680
|
+
".dbh-mobile .dbh-titlebar{gap:6px;}",
|
|
681
|
+
".dbh-mobile .dbh-title{font-size:16px;}",
|
|
682
|
+
".dbh-mobile .dbh-statusline{font-size:12px;}",
|
|
683
|
+
".dbh-mobile .dbh-updatepanel{padding:8px 10px;}",
|
|
684
|
+
".dbh-mobile .dbh-envrow{gap:6px;}",
|
|
685
|
+
".dbh-mobile .dbh-envname{max-width:120px;}",
|
|
686
|
+
".dbh-mobile .dbh-conn{flex:1 1 100%;order:1;min-width:0;}",
|
|
687
|
+
".dbh-mobile .dbh-rochip{height:24px;padding:0 8px;order:2;}",
|
|
688
|
+
".dbh-mobile .dbh-roemoji{font-size:14px;}",
|
|
689
|
+
".dbh-mobile .dbh-acts{order:3;width:100%;margin-left:0;justify-content:flex-end;gap:6px;padding:2px 0 0;}",
|
|
690
|
+
".dbh-mobile .dbh-iconbtn{width:32px;height:32px;}",
|
|
691
|
+
".dbh-mobile .dbh-btn{height:30px;padding:0 12px;}",
|
|
692
|
+
".dbh-mobile .dbh-tiles{grid-template-columns:1fr;}",
|
|
693
|
+
".dbh-mobile .dbh-srow{gap:8px;padding:6px 0;}",
|
|
694
|
+
".dbh-mobile .dbh-srow-title{font-size:12px;}",
|
|
695
|
+
".dbh-mobile .dbh-srow-state{font-size:11px;min-width:2.2em;}",
|
|
696
|
+
".dbh-mobile .dbh-srow [role=\"switch\"]{transform:scale(.8);}",
|
|
697
|
+
".dbh-mobile .dbh-btn-sm{height:24px;padding:0 8px;font-size:11px;}",
|
|
698
|
+
".dbh-mobile .dbh-groupbody{margin:0 4px 6px 8px;padding-left:8px;}",
|
|
699
|
+
".dbh-mobile .dbh-editrow .dbh-envinput{flex:1 1 100%;}",
|
|
700
|
+
".dbh-mobile .dbh-editrow .dbh-input{flex:1 1 100%;}",
|
|
701
|
+
".dbh-mobile .dbh-srcid{max-width:72px;}",
|
|
702
|
+
".dbh-mobile .dbh-updatehead{align-items:flex-start;}",
|
|
369
703
|
].join("");
|
|
370
704
|
|
|
371
705
|
function ensureStyle() {
|
|
@@ -398,10 +732,24 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
398
732
|
// The settings section's page (also the sidebar panel's body): the same
|
|
399
733
|
// management page the Plugins page renders in its 'page' view, wrapped in its
|
|
400
734
|
// own scroll container. The Plugins-row mount keeps the shell's container.
|
|
735
|
+
// `surface` names the MOUNT, not the width: the settings dialog is a 760px
|
|
736
|
+
// modal, the left-menu entry and the Plugins row are full pages, and the
|
|
737
|
+
// card itself still measures its column before choosing a layout.
|
|
401
738
|
function ConfigPanel(props) {
|
|
402
739
|
ensureStyle();
|
|
740
|
+
var surface = props && typeof props.surface === "string" ? props.surface : "modal";
|
|
403
741
|
return h("div", { className: "dbh-wrap" },
|
|
404
|
-
h(StatusCard, Object.assign({}, props, { view: "page" })));
|
|
742
|
+
h(StatusCard, Object.assign({}, props, { view: "page", surface: surface })));
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** Settings -> DBHub dialog (narrow, unchanged density). */
|
|
746
|
+
function SettingsSectionPanel(props) {
|
|
747
|
+
return h(ConfigPanel, Object.assign({}, props, { surface: "modal" }));
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/** Left-menu DBHub panel: a full page with most of the viewport available. */
|
|
751
|
+
function MainPanel(props) {
|
|
752
|
+
return h(ConfigPanel, Object.assign({}, props, { surface: "main" }));
|
|
405
753
|
}
|
|
406
754
|
|
|
407
755
|
// ── pure layout helpers ───────────────────────────────────────────────────
|
|
@@ -568,7 +916,201 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
568
916
|
// same page session (switching settings sections, opening the sidebar panel)
|
|
569
917
|
// and reset on reload. Deliberately not persisted anywhere: it is view state,
|
|
570
918
|
// not configuration.
|
|
571
|
-
var uiMemory = { expanded: {}, addOpen: false, addTarget: "", settingsOpen: false };
|
|
919
|
+
var uiMemory = { expanded: {}, addOpen: false, addTarget: "", settingsOpen: false, advOpen: false };
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Upstream dbhub capabilities, published by the Host as a JSON string.
|
|
923
|
+
* `''` (the probe has not finished) means "assume supported": the controls stay
|
|
924
|
+
* usable and a genuine incompatibility surfaces as a dbhub error, whereas
|
|
925
|
+
* disabling them on a guess would block a working setup.
|
|
926
|
+
*/
|
|
927
|
+
function capsOf(value) {
|
|
928
|
+
var raw = value && typeof value.capabilities === "string" ? value.capabilities : "";
|
|
929
|
+
if (!raw) return { version: "", readonlyTools: true, ssh: true, warning: "" };
|
|
930
|
+
try {
|
|
931
|
+
var parsed = JSON.parse(raw);
|
|
932
|
+
if (!parsed || typeof parsed !== "object") return { version: "", readonlyTools: true, ssh: true, warning: "" };
|
|
933
|
+
return {
|
|
934
|
+
version: typeof parsed.version === "string" ? parsed.version : "",
|
|
935
|
+
readonlyTools: parsed.readonlyTools !== false,
|
|
936
|
+
ssh: parsed.ssh !== false,
|
|
937
|
+
warning: typeof parsed.warning === "string" ? parsed.warning : "",
|
|
938
|
+
};
|
|
939
|
+
} catch (e) {
|
|
940
|
+
return { version: "", readonlyTools: true, ssh: true, warning: "" };
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/** Localized one-line explanation of a capability warning ('' when fine). */
|
|
945
|
+
function capabilityHint(caps, t) {
|
|
946
|
+
if (!caps || !caps.warning) return "";
|
|
947
|
+
if (caps.warning === "tooOld") return t("cap.tooOld", { v: caps.version || "?" });
|
|
948
|
+
if (caps.warning === "untested") return t("cap.untested", { v: caps.version || "?" });
|
|
949
|
+
return "";
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Build the option half of a write payload from one form draft.
|
|
954
|
+
*
|
|
955
|
+
* Secrets follow the "blank = keep the stored value" rule: an empty password /
|
|
956
|
+
* passphrase / key path is simply OMITTED, and the Host merges it with what is
|
|
957
|
+
* already stored (a user must never have to re-type a password to flip the
|
|
958
|
+
* read-only checkbox). `sshOn === false` is the explicit "no tunnel" signal and
|
|
959
|
+
* must survive as a defined `null` — otherwise it would read as "unchanged".
|
|
960
|
+
*
|
|
961
|
+
* @returns `{ readOnly, ssh? , sshOff? }` (spread into a configOp payload).
|
|
962
|
+
*/
|
|
963
|
+
function connDraftOf(form) {
|
|
964
|
+
var f = form || {};
|
|
965
|
+
var out = { readOnly: f.ro === true };
|
|
966
|
+
var host = String(f.sshHost || "").trim();
|
|
967
|
+
var user = String(f.sshUser || "").trim();
|
|
968
|
+
var keyPath = String(f.sshKeyPath || "").trim();
|
|
969
|
+
var password = String(f.sshPassword || "");
|
|
970
|
+
var passphrase = String(f.sshPassphrase || "");
|
|
971
|
+
var requested = !!host || !!user || !!keyPath || !!password || f.sshOn === true;
|
|
972
|
+
if (f.sshOn !== true) {
|
|
973
|
+
if (requested === false) return out;
|
|
974
|
+
out.ssh = null;
|
|
975
|
+
return out;
|
|
976
|
+
}
|
|
977
|
+
var auth = f.sshAuthKind === "password" ? "password" : "key";
|
|
978
|
+
var ssh = {
|
|
979
|
+
host: host,
|
|
980
|
+
port: String(f.sshPort || "").trim(),
|
|
981
|
+
user: user,
|
|
982
|
+
auth: auth,
|
|
983
|
+
proxyJump: String(f.sshProxyJump || "").trim(),
|
|
984
|
+
};
|
|
985
|
+
if (auth === "password") {
|
|
986
|
+
if (password) ssh.password = password;
|
|
987
|
+
} else {
|
|
988
|
+
if (keyPath) ssh.keyPath = keyPath;
|
|
989
|
+
if (passphrase) ssh.passphrase = passphrase;
|
|
990
|
+
}
|
|
991
|
+
out.ssh = ssh;
|
|
992
|
+
return out;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** The option draft behind a write form: forms built for a write carry the
|
|
996
|
+
* originating form under `_draft`, editing forms ARE the draft. */
|
|
997
|
+
function draftOf(form) {
|
|
998
|
+
return form && form._draft ? form._draft : form;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/** Spread `connDraftOf` into one configOp payload. */
|
|
1002
|
+
function withConnDraft(op, form) {
|
|
1003
|
+
var d = connDraftOf(draftOf(form));
|
|
1004
|
+
op.readOnly = d.readOnly === true;
|
|
1005
|
+
if (d.ssh === null) op.ssh = null;
|
|
1006
|
+
else if (d.ssh) op.ssh = d.ssh;
|
|
1007
|
+
return op;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/** Empty draft fields for the advanced (SSH) section. */
|
|
1011
|
+
function sshDraftOf(row) {
|
|
1012
|
+
var ssh = row && row.ssh && typeof row.ssh === "object" ? row.ssh : null;
|
|
1013
|
+
return {
|
|
1014
|
+
sshOn: !!ssh,
|
|
1015
|
+
sshHost: ssh ? String(ssh.host || "") : "",
|
|
1016
|
+
sshPort: ssh && ssh.port ? String(ssh.port) : "",
|
|
1017
|
+
sshUser: ssh ? String(ssh.user || "") : "",
|
|
1018
|
+
sshAuthKind: ssh && ssh.authKind === "password" ? "password" : "key",
|
|
1019
|
+
sshKeyPath: "",
|
|
1020
|
+
sshPassword: "",
|
|
1021
|
+
sshPassphrase: "",
|
|
1022
|
+
sshProxyJump: ssh ? String(ssh.proxyJump || "") : "",
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Parse the Host's transient update snapshot (a JSON string). Always returns
|
|
1028
|
+
* the full shape so render code never has to guard field by field.
|
|
1029
|
+
* @returns `{ latest, latestSpec, hasUpdate, checkedAt, source, error }`
|
|
1030
|
+
*/
|
|
1031
|
+
function parseUpdate(value) {
|
|
1032
|
+
var out = { latest: "", latestSpec: "", hasUpdate: false, checkedAt: 0, source: "", error: "" };
|
|
1033
|
+
var raw = value && typeof value.update === "string" ? value.update : "";
|
|
1034
|
+
if (!raw) return out;
|
|
1035
|
+
try {
|
|
1036
|
+
var parsed = JSON.parse(raw);
|
|
1037
|
+
if (!parsed || typeof parsed !== "object") return out;
|
|
1038
|
+
out.latest = typeof parsed.latest === "string" ? parsed.latest.trim() : "";
|
|
1039
|
+
out.latestSpec = typeof parsed.latestSpec === "string" && parsed.latestSpec
|
|
1040
|
+
? parsed.latestSpec
|
|
1041
|
+
: (out.latest ? "dsh-dbhub-live@" + out.latest : "");
|
|
1042
|
+
out.hasUpdate = parsed.hasUpdate === true && !!out.latest;
|
|
1043
|
+
out.checkedAt = typeof parsed.checkedAt === "number" && parsed.checkedAt > 0 ? parsed.checkedAt : 0;
|
|
1044
|
+
out.source = typeof parsed.source === "string" ? parsed.source : "";
|
|
1045
|
+
out.error = typeof parsed.error === "string" ? parsed.error : "";
|
|
1046
|
+
} catch (e) {
|
|
1047
|
+
/* a malformed snapshot is simply "no update information" */
|
|
1048
|
+
}
|
|
1049
|
+
return out;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Measured width of the page's own content column. The same component serves
|
|
1054
|
+
* a 500px settings dialog, a full page and a phone browser, so the layout is
|
|
1055
|
+
* decided by how much room it actually has. ResizeObserver is absent in older
|
|
1056
|
+
* browsers and in the offscreen render gate; then the caller falls back to
|
|
1057
|
+
* `window.innerWidth` (or to the mount surface's default).
|
|
1058
|
+
*/
|
|
1059
|
+
function useMeasuredWidth(ref) {
|
|
1060
|
+
var state = react.useState(0);
|
|
1061
|
+
var setWidth = state[1];
|
|
1062
|
+
react.useEffect(function () {
|
|
1063
|
+
var el = ref && ref.current;
|
|
1064
|
+
if (typeof ResizeObserver !== "function" || !el) return undefined;
|
|
1065
|
+
var observer = new ResizeObserver(function (entries) {
|
|
1066
|
+
var entry = entries && entries[0];
|
|
1067
|
+
var w = entry && entry.contentRect ? entry.contentRect.width : 0;
|
|
1068
|
+
if (w > 0) setWidth(Math.round(w));
|
|
1069
|
+
});
|
|
1070
|
+
try {
|
|
1071
|
+
observer.observe(el);
|
|
1072
|
+
} catch (e) {
|
|
1073
|
+
return undefined;
|
|
1074
|
+
}
|
|
1075
|
+
return function () {
|
|
1076
|
+
try { observer.disconnect(); } catch (e) { /* disposal must never throw */ }
|
|
1077
|
+
};
|
|
1078
|
+
}, []);
|
|
1079
|
+
return state[0];
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* The 显示/隐藏 style switch used by the modern surfaces. A native checkbox
|
|
1084
|
+
* stays in the DOM (keyboard + screen-reader semantics, and the offscreen gate
|
|
1085
|
+
* can drive it); the track/knob are decoration.
|
|
1086
|
+
*/
|
|
1087
|
+
function switchControl(opts) {
|
|
1088
|
+
var o = opts || {};
|
|
1089
|
+
var label = o.ariaLabel || o.label || "";
|
|
1090
|
+
if (SwitchPrimitive) {
|
|
1091
|
+
return h(SwitchPrimitive, {
|
|
1092
|
+
checked: o.checked === true,
|
|
1093
|
+
onChange: function (next) { if (typeof o.onChange === "function") o.onChange(next); },
|
|
1094
|
+
label: label,
|
|
1095
|
+
disabled: o.disabled === true,
|
|
1096
|
+
title: o.title || "",
|
|
1097
|
+
className: o.className,
|
|
1098
|
+
key: o.key,
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
// Fallback with the same 36x20 geometry and token colours as the primitive.
|
|
1102
|
+
return h("button", {
|
|
1103
|
+
type: "button",
|
|
1104
|
+
role: "switch",
|
|
1105
|
+
"aria-checked": o.checked === true,
|
|
1106
|
+
"aria-label": label,
|
|
1107
|
+
title: o.title || "",
|
|
1108
|
+
disabled: o.disabled === true,
|
|
1109
|
+
className: "dbh-fbswitch" + (o.checked === true ? " dbh-fbswitch-on" : "") + (o.className ? " " + o.className : ""),
|
|
1110
|
+
key: o.key,
|
|
1111
|
+
onClick: function () { if (typeof o.onChange === "function") o.onChange(o.checked !== true); },
|
|
1112
|
+
}, h("span", { className: "dbh-fbswitch-thumb" }));
|
|
1113
|
+
}
|
|
572
1114
|
|
|
573
1115
|
// ── the page ──────────────────────────────────────────────────────────────
|
|
574
1116
|
|
|
@@ -597,6 +1139,10 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
597
1139
|
var lastError = typeof value.lastError === "string" && value.lastError ? value.lastError : "";
|
|
598
1140
|
var modeLabel = t("mode." + (value.mode || "oneshot"));
|
|
599
1141
|
var workspaces = workspacesOf(value);
|
|
1142
|
+
var caps = capsOf(value);
|
|
1143
|
+
var capHint = capabilityHint(caps, t);
|
|
1144
|
+
var roSupported = caps.readonlyTools !== false;
|
|
1145
|
+
var sshSupported = caps.ssh !== false;
|
|
600
1146
|
var savedCount = workspaces.filter(function (w) { return w.persisted; }).length;
|
|
601
1147
|
var groups = groupRows(workspaces);
|
|
602
1148
|
var layout = layoutModeOf(groups);
|
|
@@ -609,6 +1155,79 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
609
1155
|
meta.emoji + " " + t(meta.key) + " · " + t("tools.fixed", { n: String(toolCount) }) + " · " + summary);
|
|
610
1156
|
}
|
|
611
1157
|
|
|
1158
|
+
// ── modern surfaces (dsh >= 0.2.1) ───────────────────────────────────────
|
|
1159
|
+
//
|
|
1160
|
+
// The Host decides `dshModern` from the RUNNING dsh version (lib/meta.mjs):
|
|
1161
|
+
// on 0.1.x every flag below stays false and the page renders exactly the old
|
|
1162
|
+
// DOM, which the offscreen gate's legacy assertions pin.
|
|
1163
|
+
var modern = value.dshModern === true;
|
|
1164
|
+
var surface = props.surface === "main" || props.surface === "modal" || props.surface === "page"
|
|
1165
|
+
? props.surface
|
|
1166
|
+
: "page";
|
|
1167
|
+
var pluginVersion = typeof value.pluginVersion === "string" ? String(value.pluginVersion).trim() : "";
|
|
1168
|
+
var dshVersion = typeof value.dshVersion === "string" ? String(value.dshVersion).trim() : "";
|
|
1169
|
+
var runtime = value.runtime === "desktop" ? "desktop" : value.runtime === "web" ? "web" : "";
|
|
1170
|
+
var update = parseUpdate(value);
|
|
1171
|
+
var innerRef = react.useRef(null);
|
|
1172
|
+
var measuredWidth = useMeasuredWidth(innerRef);
|
|
1173
|
+
var viewportWidth = 0;
|
|
1174
|
+
try {
|
|
1175
|
+
if (typeof window !== "undefined" && window && typeof window.innerWidth === "number") viewportWidth = window.innerWidth;
|
|
1176
|
+
} catch (e) { /* an embedded webview may restrict access */ }
|
|
1177
|
+
var contentWidth = measuredWidth > 0 ? measuredWidth : viewportWidth;
|
|
1178
|
+
// Phone browser = the VIEWPORT is small. A narrow settings dialog on a
|
|
1179
|
+
// desktop is not a phone: its column is small but the window is not.
|
|
1180
|
+
var isMobile = modern && (viewportWidth > 0 ? viewportWidth < 700 : (contentWidth > 0 && contentWidth < 600));
|
|
1181
|
+
var isWide = modern && !isMobile && surface !== "modal" && (contentWidth === 0 || contentWidth >= 720);
|
|
1182
|
+
var checkingState = react.useState(false);
|
|
1183
|
+
var checking = checkingState[0];
|
|
1184
|
+
var setChecking = checkingState[1];
|
|
1185
|
+
// The update explanation is progressive disclosure: the header only shows the
|
|
1186
|
+
// quiet "可升级到 x" link, and this state opens the neutral detail panel.
|
|
1187
|
+
var updateOpenState = react.useState(false);
|
|
1188
|
+
var updateOpen = updateOpenState[0];
|
|
1189
|
+
var setUpdateOpen = updateOpenState[1];
|
|
1190
|
+
// Implementation details (tool count, one-shot mode, policy text, versions)
|
|
1191
|
+
// start collapsed: the settings page is for acting, not reading.
|
|
1192
|
+
var notesState = react.useState(false);
|
|
1193
|
+
var notesOpen = notesState[0];
|
|
1194
|
+
var setNotesOpen = notesState[1];
|
|
1195
|
+
var checkBaseline = react.useRef(0);
|
|
1196
|
+
var checkTimer = react.useRef(null);
|
|
1197
|
+
var runUpdateCheck = function (force) {
|
|
1198
|
+
setChecking(true);
|
|
1199
|
+
checkBaseline.current = update.checkedAt;
|
|
1200
|
+
if (checkTimer.current) clearTimeout(checkTimer.current);
|
|
1201
|
+
checkTimer.current = setTimeout(function () {
|
|
1202
|
+
checkTimer.current = null;
|
|
1203
|
+
setChecking(false);
|
|
1204
|
+
}, 20000);
|
|
1205
|
+
try {
|
|
1206
|
+
if (typeof props.configOp === "function") props.configOp({ op: "updateCheck", force: force === true });
|
|
1207
|
+
} catch (e) {
|
|
1208
|
+
setChecking(false);
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
// Opening the page asks the Host for a (throttled) update check; the Host
|
|
1212
|
+
// owns the network call and the cache, so a refresh here costs nothing.
|
|
1213
|
+
react.useEffect(function () {
|
|
1214
|
+
if (!modern) return undefined;
|
|
1215
|
+
try {
|
|
1216
|
+
if (typeof props.configOp === "function") props.configOp({ op: "updateCheck", force: false });
|
|
1217
|
+
} catch (e) { /* an offline host is not a page failure */ }
|
|
1218
|
+
return undefined;
|
|
1219
|
+
}, [modern]);
|
|
1220
|
+
react.useEffect(function () {
|
|
1221
|
+
if (!checking) return;
|
|
1222
|
+
if (update.checkedAt > 0 && update.checkedAt !== checkBaseline.current) {
|
|
1223
|
+
if (checkTimer.current) {
|
|
1224
|
+
clearTimeout(checkTimer.current);
|
|
1225
|
+
checkTimer.current = null;
|
|
1226
|
+
}
|
|
1227
|
+
setChecking(false);
|
|
1228
|
+
}
|
|
1229
|
+
}, [value.update, checking]);
|
|
1230
|
+
|
|
612
1231
|
// ── view state (page only) ───────────────────────────────────────────────
|
|
613
1232
|
//
|
|
614
1233
|
// `useWorkspaces` is a standard prop of root-scoped slots: it tells us which
|
|
@@ -669,11 +1288,16 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
669
1288
|
setAddTarget(nextTarget);
|
|
670
1289
|
};
|
|
671
1290
|
|
|
672
|
-
// ── config draft (interval
|
|
1291
|
+
// ── config draft (interval + the sidebar switch + the policy switch) ──────
|
|
673
1292
|
var draftState = react.useState(function () {
|
|
674
1293
|
return {
|
|
675
1294
|
updateIntervalDays: typeof value.updateIntervalDays === "number" ? String(value.updateIntervalDays) : "7",
|
|
1295
|
+
// The user's own "hide the shortcut" preference. Independent of the
|
|
1296
|
+
// plugin's enabled state: hiding the entry does NOT disable anything.
|
|
676
1297
|
showSidebarEntry: value.showSidebarEntry !== false,
|
|
1298
|
+
// The user's own "let the plugin announce its access policy" preference.
|
|
1299
|
+
// Independent of everything else: off removes one prompt section only.
|
|
1300
|
+
announceAccessPolicy: value.announceAccessPolicy !== false,
|
|
677
1301
|
};
|
|
678
1302
|
});
|
|
679
1303
|
var draft = draftState[0];
|
|
@@ -686,15 +1310,24 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
686
1310
|
props.saveConfig({
|
|
687
1311
|
updateIntervalDays: Number(draft.updateIntervalDays),
|
|
688
1312
|
showSidebarEntry: draft.showSidebarEntry !== false,
|
|
1313
|
+
announceAccessPolicy: draft.announceAccessPolicy !== false,
|
|
689
1314
|
}).then(function () { setSaved(true); });
|
|
690
1315
|
};
|
|
691
1316
|
|
|
1317
|
+
// The switch applies immediately (no Save round-trip), like it always has.
|
|
692
1318
|
var toggleSidebar = function (next) {
|
|
693
1319
|
setDraft(Object.assign({}, draft, { showSidebarEntry: next }));
|
|
694
1320
|
setSaved(false);
|
|
695
1321
|
props.saveConfig({ showSidebarEntry: next });
|
|
696
1322
|
};
|
|
697
1323
|
|
|
1324
|
+
// Same immediate-apply contract for the policy announcement.
|
|
1325
|
+
var togglePolicy = function (next) {
|
|
1326
|
+
setDraft(Object.assign({}, draft, { announceAccessPolicy: next }));
|
|
1327
|
+
setSaved(false);
|
|
1328
|
+
props.saveConfig({ announceAccessPolicy: next });
|
|
1329
|
+
};
|
|
1330
|
+
|
|
698
1331
|
// ── pre-test + confirm dialog for add/edit ────────────────────────────────
|
|
699
1332
|
//
|
|
700
1333
|
// Both the add form and the edit form gate saves behind a live connection
|
|
@@ -715,25 +1348,50 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
715
1348
|
var pretestRef = react.useRef(null);
|
|
716
1349
|
pretestRef.current = pretest;
|
|
717
1350
|
|
|
718
|
-
var addState = react.useState({ ws: "", env: "default", dsn: "" });
|
|
1351
|
+
var addState = react.useState(Object.assign({ ws: "", env: "default", dsn: "", ro: false, adv: false }, sshDraftOf(null)));
|
|
719
1352
|
var addForm = addState[0];
|
|
720
1353
|
var setAddForm = addState[1];
|
|
721
1354
|
var addDsn = function () {
|
|
722
1355
|
if (!addForm.dsn.trim()) return;
|
|
723
|
-
|
|
1356
|
+
// The read-only switch and the tunnel ride the SAME write as the DSN, so
|
|
1357
|
+
// one click produces one atomic store write (and one op for the legacy
|
|
1358
|
+
// single-field command channel). `_draft` keeps the originating form
|
|
1359
|
+
// reachable for the pre-test payload and the final save.
|
|
1360
|
+
requestWrite(withConnDraft({
|
|
724
1361
|
ws: addTarget || addForm.ws.trim(),
|
|
725
1362
|
env: addForm.env.trim() || "default",
|
|
726
1363
|
dsn: addForm.dsn.trim(),
|
|
727
|
-
|
|
1364
|
+
_draft: addForm,
|
|
1365
|
+
}, addForm), true);
|
|
728
1366
|
};
|
|
729
1367
|
|
|
730
1368
|
var editState = react.useState(null);
|
|
731
1369
|
var editing = editState[0];
|
|
732
1370
|
var setEditing = editState[1];
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
//
|
|
1371
|
+
var updateEditing = function (patch) {
|
|
1372
|
+
setEditing(function (prev) { return prev ? Object.assign({}, prev, patch) : prev; });
|
|
1373
|
+
};
|
|
1374
|
+
// Does the draft differ from the row's mirrored OPTIONS? Secrets are not
|
|
1375
|
+
// comparable (a blank input means "keep"), so only the non-secret fields and
|
|
1376
|
+
// the read-only flag take part.
|
|
1377
|
+
var optionsDiffer = function (row, form) {
|
|
1378
|
+
var d = connDraftOf(form);
|
|
1379
|
+
if ((row.ro === true) !== (d.readOnly === true)) return true;
|
|
1380
|
+
var had = row.ssh || null;
|
|
1381
|
+
var want = d.ssh === undefined ? had : d.ssh;
|
|
1382
|
+
if (!had && !want) return false;
|
|
1383
|
+
if (!had || !want) return true;
|
|
1384
|
+
return String(had.host || "") !== String(want.host || "")
|
|
1385
|
+
|| String(had.port || "") !== String(want.port || "")
|
|
1386
|
+
|| String(had.user || "") !== String(want.user || "")
|
|
1387
|
+
|| String(had.authKind || "") !== String(want.auth || "")
|
|
1388
|
+
|| String(had.proxyJump || "") !== String(want.proxyJump || "");
|
|
1389
|
+
};
|
|
1390
|
+
// An edit can change the DSN, the environment NAME, the options, or any
|
|
1391
|
+
// combination. A name-only change is a rename (no connection data to verify);
|
|
1392
|
+
// a DSN change keeps the pre-test gate and carries the rename along in the
|
|
1393
|
+
// SAME host op — two configOp writes would race on the single `configOp`
|
|
1394
|
+
// namespace field.
|
|
737
1395
|
var saveEdit = function (row) {
|
|
738
1396
|
if (!editing) return;
|
|
739
1397
|
var newEnv = String(editing.env || "").trim() || "default";
|
|
@@ -742,12 +1400,29 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
742
1400
|
// "prod " or an over-long name is not a rename onto a different env.
|
|
743
1401
|
var renaming = normalizeEnvForCompare(newEnv) !== normalizeEnvForCompare(row.env);
|
|
744
1402
|
if (!dsn) {
|
|
745
|
-
|
|
1403
|
+
var optionsChanged = optionsDiffer(row, editing);
|
|
1404
|
+
if (!renaming && !optionsChanged) { setEditing(null); return; } // nothing changed
|
|
1405
|
+
if (!optionsChanged) {
|
|
1406
|
+
// Pure rename: nothing to probe, but landing on an existing
|
|
1407
|
+
// environment still replaces that connection, so it goes through the
|
|
1408
|
+
// same overwrite gate as a connection write.
|
|
1409
|
+
requestWrite({ op: "rename", _opKind: "rename", ws: row.path, env: newEnv, dsn: "", _editRow: row, _renameFrom: row.env }, true);
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
if (!renaming) {
|
|
1413
|
+
// Options-only write on a row that keeps its name: no DSN to verify
|
|
1414
|
+
// and no name that could collide.
|
|
1415
|
+
props.configOp(withConnDraft({ op: "options", workspace: row.path, env: row.env }, editing));
|
|
1416
|
+
setEditing(null);
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
// Renamed AND re-optioned: one atomic `options` command carrying newEnv.
|
|
1420
|
+
requestWrite(withConnDraft({ op: "options", _opKind: "options", ws: row.path, env: newEnv, dsn: "", _editRow: row, _renameFrom: row.env }, editing), true);
|
|
746
1421
|
return;
|
|
747
1422
|
}
|
|
748
1423
|
// Editing the connection of the SAME environment is not an overwrite
|
|
749
1424
|
// question (it is the point of the edit); renaming onto another one is.
|
|
750
|
-
requestWrite({ ws: row.path, env: newEnv, dsn: dsn, _editRow: row, _renameFrom: renaming ? row.env : "" }, renaming);
|
|
1425
|
+
requestWrite(withConnDraft({ ws: row.path, env: newEnv, dsn: dsn, _editRow: row, _renameFrom: renaming ? row.env : "" }, editing), renaming);
|
|
751
1426
|
};
|
|
752
1427
|
|
|
753
1428
|
// ── overwrite gate ───────────────────────────────────────────────────────
|
|
@@ -762,8 +1437,15 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
762
1437
|
|
|
763
1438
|
var executeWrite = function (form) {
|
|
764
1439
|
if (form._editRow && !String(form.dsn || "").trim()) {
|
|
765
|
-
// a name-only
|
|
766
|
-
|
|
1440
|
+
// a name/option-only change carries no connection data to probe
|
|
1441
|
+
var op;
|
|
1442
|
+
if (form._opKind === "rename") {
|
|
1443
|
+
op = { op: "rename", workspace: form.ws, env: form._editRow.env, newEnv: form.env };
|
|
1444
|
+
} else {
|
|
1445
|
+
op = withConnDraft({ op: "options", workspace: form.ws, env: form._editRow.env }, draftOf(form));
|
|
1446
|
+
if (form._renameFrom && normalizeEnvForCompare(form._renameFrom) !== normalizeEnvForCompare(form.env)) op.newEnv = form.env;
|
|
1447
|
+
}
|
|
1448
|
+
props.configOp(op);
|
|
767
1449
|
setEditing(null);
|
|
768
1450
|
return;
|
|
769
1451
|
}
|
|
@@ -787,12 +1469,25 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
787
1469
|
|
|
788
1470
|
// Dispatch a raw-DSN pre-test and record the nonce so the test-result
|
|
789
1471
|
// mirror callback can route it back here. Pretest nonces are prefixed
|
|
790
|
-
// "pretest:" so the regular effect branch skips them.
|
|
1472
|
+
// "pretest:" so the regular effect branch skips them. The payload carries
|
|
1473
|
+
// the draft's options too, so "试连" really validates the tunnel/read-only
|
|
1474
|
+
// combination the user is about to save.
|
|
791
1475
|
var dispatchPretest = function (form) {
|
|
792
1476
|
var nonce = "pretest:" + (form.ws || "") + "|" + (form.env || "default") + "|" + Date.now();
|
|
793
1477
|
nonceKeys.current[nonce] = nonce;
|
|
794
1478
|
setPretest({ form: form, pendingNonce: nonce, failMessage: null });
|
|
795
|
-
|
|
1479
|
+
var draft = connDraftOf(draftOf(form));
|
|
1480
|
+
var payload = { op: "test", dsn: form.dsn, workspace: form.ws || undefined, env: form.env || "default", nonce: nonce };
|
|
1481
|
+
// An edit validates the options the row already has; a brand-new form only
|
|
1482
|
+
// asserts what it actually carries (so it cannot clear a stored tunnel).
|
|
1483
|
+
if (form._editRow || draft.readOnly === true) payload.readOnly = draft.readOnly === true;
|
|
1484
|
+
if (draft.ssh === null) payload.ssh = null;
|
|
1485
|
+
else if (draft.ssh) payload.ssh = draft.ssh;
|
|
1486
|
+
props.configOp(payload);
|
|
1487
|
+
// Watchdog tier: an SSH handshake is slower than a direct connect, so a
|
|
1488
|
+
// tunnelled validation gets a longer leash (host side is tiered to match
|
|
1489
|
+
// and stays strictly below this).
|
|
1490
|
+
var leash = draft.ssh ? PRETEST_WATCHDOG_SSH_MS : PRETEST_WATCHDOG_MS;
|
|
796
1491
|
testTimers.current[nonce] = setTimeout(function () {
|
|
797
1492
|
if (!nonceKeys.current[nonce]) return;
|
|
798
1493
|
delete nonceKeys.current[nonce];
|
|
@@ -803,29 +1498,36 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
803
1498
|
next.pendingNonce = null;
|
|
804
1499
|
return next;
|
|
805
1500
|
});
|
|
806
|
-
},
|
|
1501
|
+
}, leash);
|
|
1502
|
+
};
|
|
1503
|
+
|
|
1504
|
+
/** The `add` payload for one save (options included). */
|
|
1505
|
+
var saveOpOf = function (f) {
|
|
1506
|
+
var op = withConnDraft({
|
|
1507
|
+
op: "add", workspace: f.ws || undefined, env: f.env || "default", dsn: String(f.dsn || "").trim(),
|
|
1508
|
+
}, draftOf(f));
|
|
1509
|
+
if (f._renameFrom) op.renameFrom = f._renameFrom;
|
|
1510
|
+
return op;
|
|
807
1511
|
};
|
|
808
1512
|
|
|
809
1513
|
/** Close the add panel and clear it after a successful save. */
|
|
810
1514
|
var finishAddPanel = function (form) {
|
|
811
1515
|
if (form && form._editRow) { setEditing(null); return; }
|
|
812
|
-
setAddForm({ ws: "", env: "default", dsn: "" });
|
|
1516
|
+
setAddForm(Object.assign({ ws: "", env: "default", dsn: "", ro: false, adv: uiMemory.advOpen === true }, sshDraftOf(null)));
|
|
813
1517
|
setAdd(false, "");
|
|
814
1518
|
};
|
|
815
1519
|
|
|
816
1520
|
var confirmSave = function () {
|
|
817
1521
|
if (!pretest) return;
|
|
818
1522
|
var f = pretest.form;
|
|
819
|
-
|
|
820
|
-
if (f._renameFrom) op.renameFrom = f._renameFrom;
|
|
821
|
-
props.configOp(op);
|
|
1523
|
+
props.configOp(saveOpOf(f));
|
|
822
1524
|
finishAddPanel(f);
|
|
823
1525
|
setPretest(null);
|
|
824
1526
|
};
|
|
825
1527
|
var cancelPretest = function () { setPretest(null); };
|
|
826
1528
|
|
|
827
1529
|
var openAdd = function (target) {
|
|
828
|
-
setAddForm({ ws: String(target || ""), env: "default", dsn: "" });
|
|
1530
|
+
setAddForm(Object.assign({ ws: String(target || ""), env: "default", dsn: "", ro: false, adv: uiMemory.advOpen === true }, sshDraftOf(null)));
|
|
829
1531
|
setEditing(null);
|
|
830
1532
|
setPretest(null);
|
|
831
1533
|
setAdd(true, target || "");
|
|
@@ -877,17 +1579,69 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
877
1579
|
nonceKeys.current[nonce] = key;
|
|
878
1580
|
setTests(function (prev) {
|
|
879
1581
|
var next = Object.assign({}, prev);
|
|
880
|
-
|
|
1582
|
+
// `startedAt` drives the elapsed readout: a probe really can take several
|
|
1583
|
+
// seconds, and an unlabelled spinner reads as "hung".
|
|
1584
|
+
next[key] = { phase: "testing", nonce: nonce, startedAt: Date.now() };
|
|
881
1585
|
return next;
|
|
882
1586
|
});
|
|
883
1587
|
props.configOp({ op: "test", workspace: row.path, env: row.env, nonce: nonce });
|
|
884
1588
|
// Backstop: the host reports on every path (incl. failures); if the
|
|
885
1589
|
// mirror never carries our nonce back, surface a timeout, don't spin.
|
|
1590
|
+
var tunnelled = !!(row && row.ssh);
|
|
886
1591
|
testTimers.current[nonce] = setTimeout(function () {
|
|
887
1592
|
if (!nonceKeys.current[nonce]) return;
|
|
888
1593
|
finishTest(key, nonce, false, t("test.timeout"));
|
|
889
|
-
},
|
|
1594
|
+
}, tunnelled ? TEST_WATCHDOG_SSH_MS : TEST_WATCHDOG_MS);
|
|
890
1595
|
};
|
|
1596
|
+
/**
|
|
1597
|
+
* SSH-LAYER-ONLY test: is the bastion reachable / does the tunnel come up,
|
|
1598
|
+
* independently of the database? Answers the "which side is wrong?" question
|
|
1599
|
+
* that a plain connection test cannot. Shares the nonce + test-result
|
|
1600
|
+
* machinery with the row tests, under its own key.
|
|
1601
|
+
*/
|
|
1602
|
+
var startSshTest = function (form, targetOf, key) {
|
|
1603
|
+
var cur = tests[key];
|
|
1604
|
+
if (cur && cur.phase === "testing") return;
|
|
1605
|
+
var target = targetOf ? targetOf() : {};
|
|
1606
|
+
var nonce = key + "|" + Date.now();
|
|
1607
|
+
nonceKeys.current[nonce] = key;
|
|
1608
|
+
setTests(function (prev) {
|
|
1609
|
+
var next = Object.assign({}, prev);
|
|
1610
|
+
next[key] = { phase: "testing", nonce: nonce, startedAt: Date.now() };
|
|
1611
|
+
return next;
|
|
1612
|
+
});
|
|
1613
|
+
var d = connDraftOf(form);
|
|
1614
|
+
var ssh = d.ssh && typeof d.ssh === "object" ? d.ssh : null;
|
|
1615
|
+
if (!ssh) {
|
|
1616
|
+
finishTest(key, nonce, false, t("adv.testSshNeedFields"));
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
props.configOp({
|
|
1620
|
+
op: "test", kind: "ssh",
|
|
1621
|
+
workspace: target.ws || undefined, env: target.env || "default",
|
|
1622
|
+
ssh: ssh, nonce: nonce,
|
|
1623
|
+
});
|
|
1624
|
+
testTimers.current[nonce] = setTimeout(function () {
|
|
1625
|
+
if (!nonceKeys.current[nonce]) return;
|
|
1626
|
+
finishTest(key, nonce, false, t("test.timeout"));
|
|
1627
|
+
}, TEST_WATCHDOG_SSH_MS);
|
|
1628
|
+
};
|
|
1629
|
+
// ── elapsed readout for a running probe ──
|
|
1630
|
+
//
|
|
1631
|
+
// The 1s ticker exists ONLY while at least one row is testing: an idle page
|
|
1632
|
+
// must not hold an interval (the offscreen render gate fails the run if one
|
|
1633
|
+
// survives), and the effect's cleanup covers finish/unmount alike.
|
|
1634
|
+
var tickState = react.useState(0);
|
|
1635
|
+
var setTick = tickState[1];
|
|
1636
|
+
var anyTesting = false;
|
|
1637
|
+
for (var tk in tests) {
|
|
1638
|
+
if (Object.prototype.hasOwnProperty.call(tests, tk) && tests[tk] && tests[tk].phase === "testing") { anyTesting = true; break; }
|
|
1639
|
+
}
|
|
1640
|
+
react.useEffect(function () {
|
|
1641
|
+
if (!anyTesting || typeof setInterval !== "function") return undefined;
|
|
1642
|
+
var id = setInterval(function () { setTick(function (n) { return n + 1; }); }, 1000);
|
|
1643
|
+
return function () { clearInterval(id); };
|
|
1644
|
+
}, [anyTesting]);
|
|
891
1645
|
react.useEffect(function () {
|
|
892
1646
|
var raw = typeof value.testResult === "string" ? value.testResult : "";
|
|
893
1647
|
if (!raw) return;
|
|
@@ -905,8 +1659,8 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
905
1659
|
if (!pt || pt.pendingNonce !== res.nonce) return;
|
|
906
1660
|
var f = pt.form;
|
|
907
1661
|
if (res.ok === true) {
|
|
908
|
-
// Test passed — save immediately.
|
|
909
|
-
props.configOp(
|
|
1662
|
+
// Test passed — save immediately (options included, in the same op).
|
|
1663
|
+
props.configOp(saveOpOf(f));
|
|
910
1664
|
finishAddPanel(f);
|
|
911
1665
|
setPretest(null);
|
|
912
1666
|
} else {
|
|
@@ -922,6 +1676,7 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
922
1676
|
return function () {
|
|
923
1677
|
for (var id in timers) { if (Object.prototype.hasOwnProperty.call(timers, id)) clearTimeout(timers[id]); }
|
|
924
1678
|
if (copyTimer.current) clearTimeout(copyTimer.current);
|
|
1679
|
+
if (checkTimer.current) { clearTimeout(checkTimer.current); checkTimer.current = null; }
|
|
925
1680
|
};
|
|
926
1681
|
}, []);
|
|
927
1682
|
|
|
@@ -949,7 +1704,14 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
949
1704
|
};
|
|
950
1705
|
|
|
951
1706
|
// ── derived copy ──
|
|
952
|
-
|
|
1707
|
+
//
|
|
1708
|
+
// Modern surfaces show only what a user acts on: state + connection count.
|
|
1709
|
+
// The legacy (0.1.x) DOM keeps its original status sentence verbatim.
|
|
1710
|
+
var statusText = t(meta.key);
|
|
1711
|
+
var connSummary = workspaces.length > 0
|
|
1712
|
+
? t("status.conns", { n: String(workspaces.length) })
|
|
1713
|
+
: t("desc.unconfigured");
|
|
1714
|
+
var legacyStatusText = meta.emoji + " " + t(meta.key) + " · " + t("tools.fixed", { n: String(toolCount) })
|
|
953
1715
|
+ (workspaces.length === 0 ? " · " + t("desc.unconfigured") : "");
|
|
954
1716
|
var cfgSummary = t("cfg.summary", {
|
|
955
1717
|
n: typeof draft.updateIntervalDays === "string" && draft.updateIntervalDays ? draft.updateIntervalDays : "7",
|
|
@@ -966,8 +1728,42 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
966
1728
|
var originTitle = function (w, base) {
|
|
967
1729
|
var text = (base ? String(base) + " · " : "") + t("ws.source") + ":" + sourceLabelOf(w, t);
|
|
968
1730
|
if (!w.persisted) text += " " + t("ws.autoHint");
|
|
1731
|
+
// Read-only and the tunnel are per-row ATTRIBUTES (they change what a query
|
|
1732
|
+
// may do and how it is routed), so they ride the chip: the flag is visible,
|
|
1733
|
+
// the details are one hover away.
|
|
1734
|
+
if (w.ro === true) text += " · " + t("ro.title");
|
|
1735
|
+
if (w.ssh) text += " · " + t("ro.sshTitle", { host: String(w.ssh.host || ""), port: String(w.ssh.port || ""), user: String(w.ssh.user || "") });
|
|
969
1736
|
return text;
|
|
970
1737
|
};
|
|
1738
|
+
// Environment chip: the name plus, on the legacy layout, an inline read-only
|
|
1739
|
+
// marker. The modern layout renders read-only as its own row control below so
|
|
1740
|
+
// it can be flipped without opening the edit form.
|
|
1741
|
+
var envChip = function (w) {
|
|
1742
|
+
return h("span", { className: "dbh-envname", title: originTitle(w, w.env) },
|
|
1743
|
+
w.env,
|
|
1744
|
+
!modern && w.ro === true ? h("span", { className: "dbh-ro", title: t("ro.title") }, t("ro.chip")) : null);
|
|
1745
|
+
};
|
|
1746
|
+
/**
|
|
1747
|
+
* Per-row read-only toggle, on EVERY modern layout (dialog, wide page, phone).
|
|
1748
|
+
* One click writes `{op:'options', readOnly}` — no edit form, no re-typing a
|
|
1749
|
+
* DSN. The chip itself is the state: outlined = writable, filled + lock = the
|
|
1750
|
+
* model cannot write. The legacy layout keeps its marker-in-the-chip form.
|
|
1751
|
+
*/
|
|
1752
|
+
var roChip = function (w) {
|
|
1753
|
+
if (!modern) return null;
|
|
1754
|
+
var on = w.ro === true;
|
|
1755
|
+
return h("button", {
|
|
1756
|
+
type: "button",
|
|
1757
|
+
className: "dbh-rochip" + (on ? " dbh-rochip-on" : ""),
|
|
1758
|
+
title: roSupported ? (on ? t("ro.chipOnHint") : t("ro.chipOffHint")) : capHint,
|
|
1759
|
+
"aria-label": on ? t("ro.turnOff") : t("ro.turnOn"),
|
|
1760
|
+
"aria-pressed": on,
|
|
1761
|
+
disabled: !roSupported,
|
|
1762
|
+
onClick: function () {
|
|
1763
|
+
props.configOp({ op: "options", workspace: w.path, env: w.env, readOnly: w.ro !== true });
|
|
1764
|
+
},
|
|
1765
|
+
}, h("span", { className: "dbh-roemoji", "aria-hidden": true }, on ? "🔒" : "🔓"), t(on ? "ro.chip" : "ro.writable"));
|
|
1766
|
+
};
|
|
971
1767
|
// The source handle is a first-class, visibly-copyable chip: it shows the
|
|
972
1768
|
// trailing characters that tell look-alike handles apart, and its dashed
|
|
973
1769
|
// outline + copy glyph say "this is an id", not "copy the row". Action
|
|
@@ -997,19 +1793,23 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
997
1793
|
"aria-busy": isTesting,
|
|
998
1794
|
disabled: isTesting,
|
|
999
1795
|
onClick: function () { startTest(w, key); },
|
|
1000
|
-
}, h(RunIcon, { size:
|
|
1796
|
+
}, h(RunIcon, { size: 16 })),
|
|
1001
1797
|
h("button", {
|
|
1002
1798
|
type: "button", className: "dbh-iconbtn",
|
|
1003
1799
|
title: isEditing ? t("btn.cancel") : t("btn.edit"),
|
|
1004
1800
|
"aria-label": isEditing ? t("btn.cancel") : t("btn.edit"),
|
|
1005
|
-
onClick: function () {
|
|
1006
|
-
|
|
1801
|
+
onClick: function () {
|
|
1802
|
+
setEditing(isEditing ? null : Object.assign({
|
|
1803
|
+
key: key, dsn: "", env: w.env, ro: w.ro === true, adv: uiMemory.advOpen === true,
|
|
1804
|
+
}, sshDraftOf(w)));
|
|
1805
|
+
},
|
|
1806
|
+
}, isEditing ? h(CloseIcon, { size: 14 }) : h(EditIcon, { size: 16 })),
|
|
1007
1807
|
w.persisted ? h("button", {
|
|
1008
1808
|
type: "button", className: "dbh-iconbtn dbh-iconbtn-danger",
|
|
1009
1809
|
title: t("btn.delete"),
|
|
1010
1810
|
"aria-label": t("btn.delete"),
|
|
1011
1811
|
onClick: function () { props.configOp({ op: "remove", workspace: w.path, env: w.env }); },
|
|
1012
|
-
}, h(TrashIcon, { size:
|
|
1812
|
+
}, h(TrashIcon, { size: 16 })) : null);
|
|
1013
1813
|
};
|
|
1014
1814
|
// Browsers serialize a copy of adjacent BLOCK-level boxes with a separator,
|
|
1015
1815
|
// and the split label is two flex items — pasting it into the DSN field used
|
|
@@ -1025,32 +1825,163 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
1025
1825
|
} catch (err) { /* fall back to the browser's own serialization */ }
|
|
1026
1826
|
};
|
|
1027
1827
|
var pendingPretest = !!(pretest && pretest.pendingNonce);
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1828
|
+
|
|
1829
|
+
/**
|
|
1830
|
+
* The advanced (SSH) section shared by the add panel and the row editor.
|
|
1831
|
+
*
|
|
1832
|
+
* Layout red line: these inputs must NOT use `.dbh-field` and must appear
|
|
1833
|
+
* AFTER the DSN input in document order — the offscreen gate pins the add
|
|
1834
|
+
* panel's field count and the 0..2 input indices.
|
|
1835
|
+
*
|
|
1836
|
+
* Secrets are never echoed: the stored password / passphrase / key path stay
|
|
1837
|
+
* in the Host, and a blank input means "keep what is stored".
|
|
1838
|
+
*/
|
|
1839
|
+
var advPanel = function (form, update, keyPrefix, targetOf) {
|
|
1840
|
+
var open = form.adv === true || uiMemory.advOpen === true;
|
|
1841
|
+
var toggle = function () {
|
|
1842
|
+
var next = !open;
|
|
1843
|
+
uiMemory.advOpen = next;
|
|
1844
|
+
update({ adv: next });
|
|
1845
|
+
};
|
|
1846
|
+
var children = [];
|
|
1847
|
+
children.push(h("button", {
|
|
1848
|
+
type: "button", className: "dbh-advtoggle", "aria-expanded": open,
|
|
1849
|
+
disabled: !sshSupported,
|
|
1850
|
+
title: sshSupported ? t("add.adv") : capHint,
|
|
1851
|
+
key: keyPrefix + "-toggle",
|
|
1852
|
+
onClick: toggle,
|
|
1853
|
+
}, (open ? "▾ " : "▸ ") + t("add.adv")));
|
|
1854
|
+
if (open) {
|
|
1855
|
+
var body = [];
|
|
1856
|
+
body.push(h("label", { className: "dbh-check", key: keyPrefix + "-on" },
|
|
1857
|
+
h("input", {
|
|
1858
|
+
type: "checkbox", className: "dbh-checkbox",
|
|
1859
|
+
checked: form.sshOn === true,
|
|
1860
|
+
onChange: function (e) { update({ sshOn: e.target.checked }); },
|
|
1861
|
+
}),
|
|
1862
|
+
h("span", null, t("adv.enable"))));
|
|
1863
|
+
if (form.sshOn === true) {
|
|
1864
|
+
var field = function (key, label, props, valueKey) {
|
|
1865
|
+
return h("div", { className: "dbh-advrow", key: keyPrefix + "-" + key },
|
|
1866
|
+
h("span", { className: "dbh-advlabel" }, label),
|
|
1867
|
+
h("input", Object.assign({
|
|
1868
|
+
className: "dbh-input",
|
|
1869
|
+
value: String(form[valueKey] || ""),
|
|
1870
|
+
onChange: function (e) { var patch = {}; patch[valueKey] = e.target.value; update(patch); },
|
|
1871
|
+
}, props)));
|
|
1872
|
+
};
|
|
1873
|
+
body.push(field("host", t("adv.host"), { placeholder: "bastion.example.com", "aria-label": t("adv.host") }, "sshHost"));
|
|
1874
|
+
body.push(field("port", t("adv.port"), { placeholder: "22", "aria-label": t("adv.port") }, "sshPort"));
|
|
1875
|
+
body.push(field("user", t("adv.user"), { placeholder: "ops", "aria-label": t("adv.user") }, "sshUser"));
|
|
1876
|
+
body.push(h("label", { className: "dbh-advrow", key: keyPrefix + "-auth" },
|
|
1877
|
+
h("span", { className: "dbh-advlabel" }, t("adv.auth")),
|
|
1878
|
+
h("span", { className: "dbh-check" },
|
|
1879
|
+
h("input", {
|
|
1880
|
+
type: "radio", name: keyPrefix + "-authkind", className: "dbh-checkbox",
|
|
1881
|
+
checked: form.sshAuthKind !== "password",
|
|
1882
|
+
onChange: function () { update({ sshAuthKind: "key" }); },
|
|
1883
|
+
}),
|
|
1884
|
+
h("span", null, t("adv.auth.key"))),
|
|
1885
|
+
h("span", { className: "dbh-check" },
|
|
1886
|
+
h("input", {
|
|
1887
|
+
type: "radio", name: keyPrefix + "-authkind", className: "dbh-checkbox",
|
|
1888
|
+
checked: form.sshAuthKind === "password",
|
|
1889
|
+
onChange: function () { update({ sshAuthKind: "password" }); },
|
|
1890
|
+
}),
|
|
1891
|
+
h("span", null, t("adv.auth.password")))));
|
|
1892
|
+
if (form.sshAuthKind === "password") {
|
|
1893
|
+
body.push(field("password", t("adv.password"), { type: "password", placeholder: t("adv.keepSecret"), "aria-label": t("adv.password") }, "sshPassword"));
|
|
1894
|
+
} else {
|
|
1895
|
+
body.push(field("keyPath", t("adv.keyPath"), { placeholder: "~/.ssh/id_ed25519", "aria-label": t("adv.keyPath") }, "sshKeyPath"));
|
|
1896
|
+
body.push(field("passphrase", t("adv.passphrase"), { type: "password", placeholder: t("adv.keepSecret"), "aria-label": t("adv.passphrase") }, "sshPassphrase"));
|
|
1897
|
+
}
|
|
1898
|
+
body.push(field("proxyJump", t("adv.proxyJump"), { placeholder: "jump.example.com:2222", "aria-label": t("adv.proxyJump") }, "sshProxyJump"));
|
|
1899
|
+
// The SSH-layer test: reachability of the bastion, the local key file,
|
|
1900
|
+
// and whether dbhub can bring the tunnel up — WITHOUT the database in
|
|
1901
|
+
// the picture, so a failure can be attributed to one side.
|
|
1902
|
+
var sshKey = keyPrefix + "|sshtest";
|
|
1903
|
+
var sshEntry = tests[sshKey];
|
|
1904
|
+
var sshBusy = !!(sshEntry && sshEntry.phase === "testing");
|
|
1905
|
+
body.push(h("div", { className: "dbh-advrow", key: keyPrefix + "-sshtest" },
|
|
1906
|
+
h("button", {
|
|
1907
|
+
type: "button", className: "dbh-btn",
|
|
1908
|
+
disabled: sshBusy,
|
|
1909
|
+
title: t("adv.testSshHint"),
|
|
1910
|
+
onClick: function () { startSshTest(form, targetOf, sshKey); },
|
|
1911
|
+
}, t("btn.testSsh"))));
|
|
1912
|
+
if (sshBusy) {
|
|
1913
|
+
var sshSecs = sshEntry.startedAt ? Math.max(0, Math.round((Date.now() - sshEntry.startedAt) / 1000)) : 0;
|
|
1914
|
+
body.push(h("div", { className: "dbh-test-ok", key: keyPrefix + "-sshrun" }, "⏳ " + t("test.elapsed", { s: String(sshSecs) })));
|
|
1915
|
+
} else if (sshEntry && sshEntry.phase === "done") {
|
|
1916
|
+
body.push(h("div", {
|
|
1917
|
+
className: sshEntry.ok ? "dbh-test-ok" : "dbh-test-fail",
|
|
1918
|
+
key: keyPrefix + "-sshres",
|
|
1919
|
+
}, (sshEntry.ok ? "✓ " : "✗ ") + sshEntry.message));
|
|
1920
|
+
}
|
|
1921
|
+
body.push(h("div", { className: "dbh-hint", key: keyPrefix + "-hint" }, t("adv.hostHint") + " " + t("adv.slow")));
|
|
1922
|
+
}
|
|
1923
|
+
children.push(h("div", { className: "dbh-advbody", key: keyPrefix + "-body" }, body));
|
|
1924
|
+
}
|
|
1925
|
+
return h("div", { className: "dbh-adv", key: keyPrefix + "-adv" }, children);
|
|
1926
|
+
};
|
|
1927
|
+
|
|
1928
|
+
/** The read-only checkbox shared by both forms. */
|
|
1929
|
+
var roCheck = function (form, update, keyPrefix) {
|
|
1930
|
+
return h("label", {
|
|
1931
|
+
className: "dbh-check",
|
|
1932
|
+
key: keyPrefix + "-ro",
|
|
1933
|
+
title: roSupported ? t("ro.title") : capHint,
|
|
1934
|
+
},
|
|
1038
1935
|
h("input", {
|
|
1039
|
-
className: "dbh-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
onChange: function (e) {
|
|
1936
|
+
type: "checkbox", className: "dbh-checkbox",
|
|
1937
|
+
checked: form.ro === true,
|
|
1938
|
+
disabled: !roSupported,
|
|
1939
|
+
"aria-label": t("add.readOnly"),
|
|
1940
|
+
onChange: function (e) { update({ ro: e.target.checked }); },
|
|
1044
1941
|
}),
|
|
1045
|
-
h("
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1942
|
+
h("span", null, t("add.readOnly")));
|
|
1943
|
+
};
|
|
1944
|
+
|
|
1945
|
+
/** Editor for the options of an auto-discovered (options-only) row. */
|
|
1946
|
+
var editRow = function (w, key) {
|
|
1947
|
+
if (!(editing && editing.key === key)) return null;
|
|
1948
|
+
var update = function (patch) { updateEditing(patch); };
|
|
1949
|
+
return h("div", { className: "dbh-editbox", key: "edit" },
|
|
1950
|
+
h("div", { className: "dbh-editrow" },
|
|
1951
|
+
h("input", {
|
|
1952
|
+
className: "dbh-input dbh-envinput",
|
|
1953
|
+
"aria-label": t("add.env"),
|
|
1954
|
+
placeholder: t("env.newPlaceholder"),
|
|
1955
|
+
value: typeof editing.env === "string" ? editing.env : w.env,
|
|
1956
|
+
onChange: function (e) { updateEditing({ env: e.target.value }); },
|
|
1957
|
+
}),
|
|
1958
|
+
h("input", {
|
|
1959
|
+
className: "dbh-input",
|
|
1960
|
+
"aria-label": t("add.dsn"),
|
|
1961
|
+
placeholder: t("dsn.editPlaceholder"),
|
|
1962
|
+
value: editing.dsn,
|
|
1963
|
+
onChange: function (e) { updateEditing({ dsn: e.target.value }); },
|
|
1964
|
+
}),
|
|
1965
|
+
roCheck(editing, update, "ed"),
|
|
1966
|
+
h("button", {
|
|
1967
|
+
type: "button", className: "dbh-btn dbh-btn-primary",
|
|
1968
|
+
disabled: pendingPretest,
|
|
1969
|
+
onClick: function () { saveEdit(w); },
|
|
1970
|
+
}, t("btn.saved"))),
|
|
1971
|
+
advPanel(editing, update, "ed|" + w.path + "|" + w.env, function () {
|
|
1972
|
+
return { ws: w.path, env: String(editing.env || "").trim() || w.env };
|
|
1973
|
+
}));
|
|
1050
1974
|
};
|
|
1051
1975
|
var testRow = function (w, key) {
|
|
1052
1976
|
var entry = tests[key];
|
|
1053
|
-
if (!entry
|
|
1977
|
+
if (!entry) return null;
|
|
1978
|
+
if (entry.phase === "testing") {
|
|
1979
|
+
// Elapsed seconds, so a slow (tunnelled) probe reads as "working", not
|
|
1980
|
+
// as a frozen page. Re-rendered by the testing-only ticker above.
|
|
1981
|
+
var secs = entry.startedAt ? Math.max(0, Math.round((Date.now() - entry.startedAt) / 1000)) : 0;
|
|
1982
|
+
return h("div", { key: "test", className: "dbh-test-ok" }, "⏳ " + t("test.elapsed", { s: String(secs) }));
|
|
1983
|
+
}
|
|
1984
|
+
if (entry.phase !== "done") return null;
|
|
1054
1985
|
return h("div", { key: "test", className: entry.ok ? "dbh-test-ok" : "dbh-test-fail" },
|
|
1055
1986
|
(entry.ok ? "✓ " : "✗ ") + entry.message);
|
|
1056
1987
|
};
|
|
@@ -1061,7 +1992,11 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
1061
1992
|
var connLabelView = function (w) {
|
|
1062
1993
|
var parts = splitConn(w.conn);
|
|
1063
1994
|
return h("span", { className: "dbh-conn", title: w.conn, onCopy: function (e) { onConnCopy(w, e); } },
|
|
1064
|
-
|
|
1995
|
+
// The lock used to sit in front of every connection string, which read
|
|
1996
|
+
// like "this row needs a password" and clashed with the read-only lock.
|
|
1997
|
+
// Modern rows show the plain type://host:port/db; the only lock left is
|
|
1998
|
+
// the read-only chip. Legacy keeps its original marker.
|
|
1999
|
+
!modern ? h("span", { className: "dbh-conn-lock", "aria-hidden": true }, "🔒") : null,
|
|
1065
2000
|
h("span", { className: "dbh-conn-pre" }, parts.prefix),
|
|
1066
2001
|
parts.tail ? h("span", { className: "dbh-conn-db" }, parts.tail) : null);
|
|
1067
2002
|
};
|
|
@@ -1069,8 +2004,9 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
1069
2004
|
var key = w.path + "|" + w.env;
|
|
1070
2005
|
return h("div", { className: "dbh-envwrap", key: key },
|
|
1071
2006
|
h("div", { className: "dbh-envrow" },
|
|
1072
|
-
|
|
2007
|
+
envChip(w),
|
|
1073
2008
|
connLabelView(w),
|
|
2009
|
+
roChip(w),
|
|
1074
2010
|
rowActions(w, key)),
|
|
1075
2011
|
editRow(w, key),
|
|
1076
2012
|
testRow(w, key));
|
|
@@ -1108,17 +2044,204 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
1108
2044
|
h("div", { className: "dbh-tilehead" },
|
|
1109
2045
|
h("span", { className: "dbh-tiletitle", title: g.path }, label)),
|
|
1110
2046
|
h("div", { className: "dbh-tilemeta" },
|
|
1111
|
-
|
|
1112
|
-
connLabelView(w)
|
|
2047
|
+
envChip(w),
|
|
2048
|
+
connLabelView(w),
|
|
2049
|
+
roChip(w)),
|
|
1113
2050
|
h("div", { className: "dbh-tileacts" }, rowActions(w, key)),
|
|
1114
2051
|
editRow(w, key),
|
|
1115
2052
|
testRow(w, key));
|
|
1116
2053
|
};
|
|
1117
2054
|
|
|
1118
2055
|
// ── assemble the page ──
|
|
1119
|
-
|
|
2056
|
+
//
|
|
2057
|
+
// Modern header: title + subdued version plus a quiet "可升级到 x" link that
|
|
2058
|
+
// expands into a neutral panel on demand. The legacy (0.1.x) header below is
|
|
2059
|
+
// byte-for-byte the old one.
|
|
2060
|
+
var latestSpec = update.latestSpec || (update.latest ? "dsh-dbhub-live@" + update.latest : "");
|
|
2061
|
+
var webCmd = update.latest ? "dsh plugin --profile web up dsh-dbhub-live@" + update.latest : "";
|
|
2062
|
+
var checkedMinutes = update.checkedAt ? Math.max(0, Math.round((Date.now() - update.checkedAt) / 60000)) : 0;
|
|
2063
|
+
var copyLatest = function () { if (latestSpec) copySource(latestSpec, "ver"); };
|
|
2064
|
+
var copyWebCmd = function () { if (webCmd) copySource(webCmd, "cmd"); };
|
|
2065
|
+
var checkLabel = checking
|
|
2066
|
+
? t("ver.checking")
|
|
2067
|
+
: update.error
|
|
2068
|
+
? t("ver.checkFailed") + ":" + update.error
|
|
2069
|
+
: update.checkedAt
|
|
2070
|
+
? t("ver.checkedAgo", { n: String(checkedMinutes) })
|
|
2071
|
+
: t("ver.check");
|
|
2072
|
+
var titlebar = modern ? h("div", { className: "dbh-titlebar" },
|
|
2073
|
+
h("h2", { className: "dbh-title" }, t("title")),
|
|
2074
|
+
pluginVersion ? h("span", { className: "dbh-ver", title: dshVersion ? "DSH " + dshVersion : "" }, "v" + pluginVersion) : null,
|
|
2075
|
+
h("span", { className: "dbh-grow" }),
|
|
2076
|
+
update.hasUpdate ? h("button", {
|
|
2077
|
+
type: "button", className: "dbh-updatelink",
|
|
2078
|
+
title: t("ver.copySpec", { v: update.latest }),
|
|
2079
|
+
onClick: function () { setUpdateOpen(!updateOpen); },
|
|
2080
|
+
}, "↑ " + t("ver.link", { v: update.latest })) : null,
|
|
2081
|
+
h("button", {
|
|
2082
|
+
type: "button", className: "dbh-vercheck", disabled: checking,
|
|
2083
|
+
title: checkLabel, "aria-label": t("ver.check"),
|
|
2084
|
+
onClick: function () { runUpdateCheck(true); },
|
|
2085
|
+
}, h(RefreshIcon, { size: 12 }))) : null;
|
|
2086
|
+
|
|
2087
|
+
// Update guidance, expanded on demand: the Host names the runtime (web or
|
|
2088
|
+
// desktop) because the two profiles update differently. Neutral panel — the
|
|
2089
|
+
// old red "error" styling made users think something had broken.
|
|
2090
|
+
var updateView = null;
|
|
2091
|
+
if (modern && updateOpen && update.hasUpdate) {
|
|
2092
|
+
updateView = h("div", { className: "dbh-updatepanel" },
|
|
2093
|
+
h("div", { className: "dbh-updaterow" },
|
|
2094
|
+
h("code", { className: "dbh-spec" }, latestSpec),
|
|
2095
|
+
h("button", {
|
|
2096
|
+
type: "button", className: "dbh-linkbtn",
|
|
2097
|
+
title: t("ver.copySpec", { v: update.latest }),
|
|
2098
|
+
onClick: copyLatest,
|
|
2099
|
+
}, copiedKey === "ver" ? t("copy.ok") : t("ver.copy"))),
|
|
2100
|
+
h("div", { className: "dbh-hint" }, runtime === "desktop"
|
|
2101
|
+
? t("ver.desktopHint", { v: update.latest })
|
|
2102
|
+
: runtime === "web"
|
|
2103
|
+
? t("ver.webHint", { v: update.latest })
|
|
2104
|
+
: t("ver.genericHint", { v: update.latest })),
|
|
2105
|
+
runtime !== "desktop" && webCmd ? h("div", { className: "dbh-updaterow" },
|
|
2106
|
+
h("code", { className: "dbh-spec" }, webCmd),
|
|
2107
|
+
h("button", { type: "button", className: "dbh-linkbtn", onClick: copyWebCmd },
|
|
2108
|
+
copiedKey === "cmd" ? t("copy.ok") : t("ver.copyCmd"))) : null,
|
|
2109
|
+
h("a", {
|
|
2110
|
+
className: "dbh-link",
|
|
2111
|
+
href: "https://github.com/mr-mihu/dsh-dbhub-live/releases/tag/v" + update.latest,
|
|
2112
|
+
target: "_blank", rel: "noreferrer noopener",
|
|
2113
|
+
}, t("ver.releaseNotes")));
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// One status line: state + connection count (+ the dialog's 设置 toggle).
|
|
2117
|
+
var statusline = modern ? h("div", { className: "dbh-statusline" },
|
|
2118
|
+
h("span", { className: "dbh-dot" + (enabled ? "" : " dbh-dot-off"), "aria-hidden": true }),
|
|
2119
|
+
h("span", null, statusText),
|
|
2120
|
+
h("span", { className: "dbh-statussep" }, "·"),
|
|
2121
|
+
h("span", null, connSummary),
|
|
2122
|
+
h("span", { className: "dbh-grow" }),
|
|
2123
|
+
!isWide ? h("button", {
|
|
2124
|
+
type: "button", className: "dbh-linkbtn",
|
|
2125
|
+
"aria-expanded": settingsOpen,
|
|
2126
|
+
onClick: toggleSettings,
|
|
2127
|
+
}, t("settings.toggle") + (settingsOpen ? " ▴" : " ▾")) : null) : null;
|
|
2128
|
+
var errorLine = modern && lastError ? h("div", { className: "dbh-error" }, lastError) : null;
|
|
2129
|
+
|
|
2130
|
+
// Technical details live here, collapsed, so the rows above stay actionable.
|
|
2131
|
+
var notesView = h("div", { className: "dbh-notes" },
|
|
2132
|
+
h("div", null, t("note.tools", { n: String(toolCount) })),
|
|
2133
|
+
h("div", null, t("note.update")),
|
|
2134
|
+
h("div", null, t("note.sidebar")),
|
|
2135
|
+
h("div", null, t("note.policy")),
|
|
2136
|
+
h("div", null, t("note.version", {
|
|
2137
|
+
plugin: pluginVersion || "?",
|
|
2138
|
+
dsh: dshVersion || "?",
|
|
2139
|
+
runtime: runtime === "desktop" ? t("ver.runtimeDesktop") : t("ver.runtimeWeb"),
|
|
2140
|
+
})),
|
|
2141
|
+
capHint ? h("div", null, capHint) : null);
|
|
2142
|
+
|
|
2143
|
+
// Shared settings rows: the wide page shows them directly, the dialog behind
|
|
2144
|
+
// its 设置 toggle. Controls are the harness's own Switch primitive; the state
|
|
2145
|
+
// word (显示/隐藏, 启用/停用, 开启/关闭) sits next to it so the row's meaning
|
|
2146
|
+
// never depends on a colour or a tick alone.
|
|
2147
|
+
// The four settings controls, shared by the wide one-line bar and the narrow
|
|
2148
|
+
// stacked rows so both surfaces write through the same callbacks.
|
|
2149
|
+
var ctlEnable = function () {
|
|
2150
|
+
return switchControl({
|
|
2151
|
+
checked: enabled, ariaLabel: t("row.enable"),
|
|
2152
|
+
onChange: function (next) { props.setEnabled(next); },
|
|
2153
|
+
});
|
|
2154
|
+
};
|
|
2155
|
+
var ctlSidebar = function () {
|
|
2156
|
+
return switchControl({
|
|
2157
|
+
checked: draft.showSidebarEntry !== false, ariaLabel: t("row.sidebar"),
|
|
2158
|
+
title: t("cfg.sidebarHint"), onChange: toggleSidebar,
|
|
2159
|
+
});
|
|
2160
|
+
};
|
|
2161
|
+
var ctlPolicy = function () {
|
|
2162
|
+
return switchControl({
|
|
2163
|
+
checked: draft.announceAccessPolicy !== false, ariaLabel: t("row.policy"),
|
|
2164
|
+
title: t("cfg.policyHint"), onChange: togglePolicy,
|
|
2165
|
+
});
|
|
2166
|
+
};
|
|
2167
|
+
var ctlInterval = function () {
|
|
2168
|
+
return [
|
|
2169
|
+
h("input", {
|
|
2170
|
+
key: "input", className: "dbh-input dbh-srow-input", type: "number", min: "0",
|
|
2171
|
+
"aria-label": t("row.update"), title: t("cfg.updateHint"),
|
|
2172
|
+
value: typeof draft.updateIntervalDays === "string" ? draft.updateIntervalDays : "",
|
|
2173
|
+
onChange: function (e) { setSaved(false); setDraft(Object.assign({}, draft, { updateIntervalDays: e.target.value })); },
|
|
2174
|
+
}),
|
|
2175
|
+
h("span", { key: "unit", className: "dbh-srow-unit" }, t("unit.days")),
|
|
2176
|
+
h("button", { key: "save", type: "button", className: "dbh-btn dbh-btn-sm", onClick: saveConfig }, t("btn.save")),
|
|
2177
|
+
];
|
|
2178
|
+
};
|
|
2179
|
+
var notesButton = function (labelKey) {
|
|
2180
|
+
return h("button", {
|
|
2181
|
+
type: "button", className: "dbh-linkbtn",
|
|
2182
|
+
"aria-expanded": notesOpen,
|
|
2183
|
+
onClick: function () { setNotesOpen(!notesOpen); },
|
|
2184
|
+
}, "ⓘ " + t(notesOpen ? "settings.less" : labelKey));
|
|
2185
|
+
};
|
|
2186
|
+
|
|
2187
|
+
// Wide page: one compact line so the four controls (and their labels) sit
|
|
2188
|
+
// together instead of stretching across the page with a gaping middle. It
|
|
2189
|
+
// wraps only when the docked window is genuinely narrow.
|
|
2190
|
+
var settingsBar = function (narrow) {
|
|
2191
|
+
return h("div", { className: "dbh-settings dbh-settingsbar" + (narrow ? " dbh-barnarrow" : "") },
|
|
2192
|
+
h("div", { className: "dbh-barline" },
|
|
2193
|
+
h("span", { className: "dbh-bartitle" }, t("settings.title")),
|
|
2194
|
+
h("div", { className: "dbh-bargroup" },
|
|
2195
|
+
h("span", { className: "dbh-barlabel" }, t("row.enable")),
|
|
2196
|
+
h("span", { className: "dbh-barstate" }, t(enabled ? "state.enable" : "state.disable")),
|
|
2197
|
+
ctlEnable()),
|
|
2198
|
+
h("div", { className: "dbh-bargroup" },
|
|
2199
|
+
h("span", { className: "dbh-barlabel" }, t("row.sidebar")),
|
|
2200
|
+
h("span", { className: "dbh-barstate" }, t(draft.showSidebarEntry !== false ? "state.show" : "state.hide")),
|
|
2201
|
+
ctlSidebar()),
|
|
2202
|
+
h("div", { className: "dbh-bargroup" },
|
|
2203
|
+
h("span", { className: "dbh-barlabel" }, t("row.policy")),
|
|
2204
|
+
h("span", { className: "dbh-barstate" }, t(draft.announceAccessPolicy !== false ? "state.on" : "state.off")),
|
|
2205
|
+
ctlPolicy()),
|
|
2206
|
+
h("div", { className: "dbh-bargroup" },
|
|
2207
|
+
h("span", { className: "dbh-barlabel" }, t("row.update")),
|
|
2208
|
+
ctlInterval()),
|
|
2209
|
+
h("span", { className: "dbh-grow" }),
|
|
2210
|
+
notesButton("settings.notes"),
|
|
2211
|
+
saved ? h("span", { className: "dbh-saved" }, t("saved.ok")) : null),
|
|
2212
|
+
notesOpen ? notesView : null);
|
|
2213
|
+
};
|
|
2214
|
+
|
|
2215
|
+
// Narrow surfaces (dialog + phone): compact rows, still behind the 设置 toggle.
|
|
2216
|
+
var settingsRows = function () {
|
|
2217
|
+
return h("div", { className: "dbh-settings" },
|
|
2218
|
+
h("div", { className: "dbh-settings-title" }, t("settings.title")),
|
|
2219
|
+
h("div", { className: "dbh-srow" },
|
|
2220
|
+
h("div", { className: "dbh-srow-main" }, h("div", { className: "dbh-srow-title" }, t("row.enable"))),
|
|
2221
|
+
h("div", { className: "dbh-srow-ctl" },
|
|
2222
|
+
h("span", { className: "dbh-srow-state" }, t(enabled ? "state.enable" : "state.disable")),
|
|
2223
|
+
ctlEnable())),
|
|
2224
|
+
h("div", { className: "dbh-srow" },
|
|
2225
|
+
h("div", { className: "dbh-srow-main" }, h("div", { className: "dbh-srow-title" }, t("row.sidebar"))),
|
|
2226
|
+
h("div", { className: "dbh-srow-ctl" },
|
|
2227
|
+
h("span", { className: "dbh-srow-state" }, t(draft.showSidebarEntry !== false ? "state.show" : "state.hide")),
|
|
2228
|
+
ctlSidebar())),
|
|
2229
|
+
h("div", { className: "dbh-srow" },
|
|
2230
|
+
h("div", { className: "dbh-srow-main" }, h("div", { className: "dbh-srow-title" }, t("row.policy"))),
|
|
2231
|
+
h("div", { className: "dbh-srow-ctl" },
|
|
2232
|
+
h("span", { className: "dbh-srow-state" }, t(draft.announceAccessPolicy !== false ? "state.on" : "state.off")),
|
|
2233
|
+
ctlPolicy())),
|
|
2234
|
+
h("div", { className: "dbh-srow" },
|
|
2235
|
+
h("div", { className: "dbh-srow-main" }, h("div", { className: "dbh-srow-title" }, t("row.update"))),
|
|
2236
|
+
h("div", { className: "dbh-srow-ctl" }, ctlInterval())),
|
|
2237
|
+
h("div", { className: "dbh-settings-foot" }, notesButton("settings.more")),
|
|
2238
|
+
notesOpen ? notesView : null);
|
|
2239
|
+
};
|
|
2240
|
+
|
|
2241
|
+
// Legacy (dsh 0.1.x): the original header + settings strip, byte-for-byte.
|
|
2242
|
+
var legacyHead = h("div", { className: "dbh-head" },
|
|
1120
2243
|
h("div", { className: "dbh-headtext" },
|
|
1121
|
-
h("span", { className: "dbh-statustext", title: modeLabel },
|
|
2244
|
+
h("span", { className: "dbh-statustext", title: modeLabel }, legacyStatusText),
|
|
1122
2245
|
lastError ? h("span", { className: "dbh-error" }, lastError) : null),
|
|
1123
2246
|
h("span", { className: "dbh-cfgsum" }, cfgSummary),
|
|
1124
2247
|
h("button", { type: "button", className: "dbh-btn", onClick: toggleSettings },
|
|
@@ -1128,7 +2251,7 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
1128
2251
|
onClick: function () { props.setEnabled(!enabled); },
|
|
1129
2252
|
}, t(enabled ? "btn.disable" : "btn.enable")));
|
|
1130
2253
|
|
|
1131
|
-
var
|
|
2254
|
+
var legacySettingsPanel = settingsOpen ? h("div", { className: "dbh-strip" },
|
|
1132
2255
|
h("div", { className: "dbh-row" },
|
|
1133
2256
|
h("span", { className: "dbh-label" }, t("cfg.update")),
|
|
1134
2257
|
h("input", {
|
|
@@ -1140,16 +2263,35 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
1140
2263
|
h("span", { className: "dbh-grow" }),
|
|
1141
2264
|
h("label", { className: "dbh-check", title: t("cfg.sidebarHint") },
|
|
1142
2265
|
h("input", {
|
|
1143
|
-
type: "checkbox",
|
|
2266
|
+
type: "checkbox", className: "dbh-checkbox",
|
|
1144
2267
|
checked: draft.showSidebarEntry !== false,
|
|
1145
2268
|
onChange: function (e) { toggleSidebar(e.target.checked); },
|
|
1146
2269
|
}),
|
|
1147
2270
|
h("span", null, t("cfg.sidebar")))),
|
|
1148
|
-
h("div", { className: "dbh-
|
|
2271
|
+
h("div", { className: "dbh-row" },
|
|
2272
|
+
h("label", { className: "dbh-check", title: t("cfg.policyHint") },
|
|
2273
|
+
h("input", {
|
|
2274
|
+
type: "checkbox", className: "dbh-checkbox",
|
|
2275
|
+
"aria-label": t("cfg.policy"),
|
|
2276
|
+
checked: draft.announceAccessPolicy !== false,
|
|
2277
|
+
onChange: function (e) { togglePolicy(e.target.checked); },
|
|
2278
|
+
}),
|
|
2279
|
+
h("span", null, t("cfg.policy")))),
|
|
2280
|
+
h("div", { className: "dbh-hint" }, (enabled ? t("cfg.enabledHint") : t("cfg.disabledHint")) + " " + t("cfg.sidebarHint")),
|
|
1149
2281
|
h("div", { className: "dbh-addfoot" },
|
|
1150
2282
|
saved ? h("span", { className: "dbh-saved" }, t("saved.ok")) : null,
|
|
1151
2283
|
h("button", { type: "button", className: "dbh-btn dbh-btn-primary", onClick: saveConfig }, t("btn.save")))) : null;
|
|
1152
2284
|
|
|
2285
|
+
// Any non-modal page (left-menu panel, Plugins page) uses the one-line bar,
|
|
2286
|
+
// even when the measured column is narrow: a docked/small desktop window used
|
|
2287
|
+
// to fall back to four stacked rows ("all buttons in a column"). Only the
|
|
2288
|
+
// dialog and the phone keep the stacked form.
|
|
2289
|
+
var useSettingsBar = modern && !isMobile && surface !== "modal";
|
|
2290
|
+
var settingsPanel = modern
|
|
2291
|
+
? (settingsOpen && !useSettingsBar ? settingsRows() : null)
|
|
2292
|
+
: legacySettingsPanel;
|
|
2293
|
+
var configBlock = useSettingsBar ? settingsBar(!isWide) : null;
|
|
2294
|
+
|
|
1153
2295
|
var section = h("div", { className: "dbh-sect" },
|
|
1154
2296
|
h("span", { className: "dbh-secttitle" }, t("conn.title")),
|
|
1155
2297
|
h("span", { className: "dbh-sectcount" }, t("conn.total", { n: String(workspaces.length), m: String(groups.length) })),
|
|
@@ -1239,6 +2381,15 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
1239
2381
|
value: addForm.dsn,
|
|
1240
2382
|
onChange: function (e) { setAddForm(Object.assign({}, addForm, { dsn: e.target.value })); },
|
|
1241
2383
|
})));
|
|
2384
|
+
// Options come AFTER the DSN input: the offscreen gate pins the field count
|
|
2385
|
+
// and the 0..2 input indices inside the add panel, so nothing may be
|
|
2386
|
+
// inserted before the DSN box.
|
|
2387
|
+
var updateAdd = function (patch) { setAddForm(Object.assign({}, addForm, patch)); };
|
|
2388
|
+
fields.push(h("div", { className: "dbh-row", key: "ro" }, roCheck(addForm, updateAdd, "add")));
|
|
2389
|
+
fields.push(advPanel(addForm, updateAdd, "add", function () {
|
|
2390
|
+
return { ws: addTarget || String(addForm.ws || "").trim(), env: String(addForm.env || "").trim() || "default" };
|
|
2391
|
+
}));
|
|
2392
|
+
if (capHint) fields.push(h("div", { className: "dbh-hint", key: "cap" }, capHint));
|
|
1242
2393
|
fields.push(h("div", { className: "dbh-addfoot", key: "foot" },
|
|
1243
2394
|
h("button", { type: "button", className: "dbh-btn", onClick: closeAdd }, t("btn.cancel")),
|
|
1244
2395
|
h("button", {
|
|
@@ -1252,9 +2403,18 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
1252
2403
|
var wsList = h("datalist", { id: "dbh-ws-list" },
|
|
1253
2404
|
groups.map(function (g) { return h("option", { key: g.key, value: g.path }, g.title); }));
|
|
1254
2405
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
2406
|
+
var innerClass = "dbh-inner"
|
|
2407
|
+
+ (modern ? " dbh-modern" : "")
|
|
2408
|
+
+ (modern && isWide ? " dbh-wide" : "")
|
|
2409
|
+
+ (modern && isMobile ? " dbh-mobile" : "")
|
|
2410
|
+
+ (modern && surface === "modal" ? " dbh-modal" : "");
|
|
2411
|
+
|
|
2412
|
+
return h("div", { className: innerClass, ref: innerRef },
|
|
2413
|
+
modern ? titlebar : legacyHead,
|
|
2414
|
+
modern ? updateView : null,
|
|
2415
|
+
modern ? statusline : null,
|
|
2416
|
+
modern ? errorLine : null,
|
|
2417
|
+
configBlock || settingsPanel,
|
|
1258
2418
|
section,
|
|
1259
2419
|
body,
|
|
1260
2420
|
overwriteView || pretestView,
|
|
@@ -1482,21 +2642,24 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
1482
2642
|
label: function () { return t("title"); },
|
|
1483
2643
|
locale: NS,
|
|
1484
2644
|
inject: function () { return face; },
|
|
1485
|
-
},
|
|
2645
|
+
}, SettingsSectionPanel);
|
|
1486
2646
|
console.log("[dsh-dbhub-live] 设置页入口已注册: settings.section id=dbhub");
|
|
1487
2647
|
return reg;
|
|
1488
2648
|
});
|
|
1489
|
-
// The sidebar shortcut is
|
|
1490
|
-
//
|
|
1491
|
-
//
|
|
1492
|
-
//
|
|
2649
|
+
// The sidebar shortcut is the AND of two independent things:
|
|
2650
|
+
// · the user's `showSidebarEntry` switch — hide the shortcut while the
|
|
2651
|
+
// plugin keeps working (its tools stay available); and
|
|
2652
|
+
// · the plugin's enabled state — a disabled plugin contributes no entry at
|
|
2653
|
+
// all, whatever the switch says.
|
|
2654
|
+
// The settings section and the Plugins-row card do NOT depend on this, so
|
|
2655
|
+
// neither state can lock anyone out of the configuration.
|
|
1493
2656
|
var sidebarReg = null;
|
|
1494
2657
|
var sidebarSlotReady = false;
|
|
1495
2658
|
var sidebarWanted = function () {
|
|
1496
2659
|
try {
|
|
1497
2660
|
var snap = face.getSnapshot();
|
|
1498
2661
|
var v = snap && typeof snap === "object" && snap.value && typeof snap.value === "object" ? snap.value : {};
|
|
1499
|
-
return v.showSidebarEntry !== false;
|
|
2662
|
+
return v.enabled !== false && v.showSidebarEntry !== false;
|
|
1500
2663
|
} catch (e) {
|
|
1501
2664
|
return true;
|
|
1502
2665
|
}
|
|
@@ -1534,7 +2697,7 @@ window.__ModuleLoader__.load({ id: "dsh-dbhub-live", factory: (require) => {
|
|
|
1534
2697
|
key: "dbhub",
|
|
1535
2698
|
locale: NS,
|
|
1536
2699
|
inject: function () { return face; },
|
|
1537
|
-
},
|
|
2700
|
+
}, MainPanel);
|
|
1538
2701
|
});
|
|
1539
2702
|
face.subscribe(syncSidebar);
|
|
1540
2703
|
}
|