@goodandready/dsh-remote-workspace 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/README.ru.md +17 -0
- package/README.zh.md +17 -0
- package/docs/plans/task_plan.md +18 -20
- package/lib/alert-service.js +109 -0
- package/lib/client.js +490 -77
- package/lib/diagnose-service.js +57 -0
- package/lib/docker-service.js +93 -0
- package/lib/env-service.js +122 -0
- package/lib/health-service.js +117 -0
- package/lib/index.js +94 -7
- package/lib/routes.js +627 -398
- package/lib/ssh-config-parser.js +89 -0
- package/lib/ssh-service.js +59 -20
- package/lib/tar-sync-service.js +85 -0
- package/lib/tools.js +175 -2
- package/lib/watcher-service.js +111 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -90,6 +90,22 @@ window.__ModuleLoader__.load({
|
|
|
90
90
|
.drw-modal-dialog{background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;width:100%;max-width:800px;max-height:85vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 12px 32px rgba(0,0,0,0.4)}
|
|
91
91
|
.drw-modal-hdr{padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l2);display:flex;align-items:center;justify-content:space-between;font-weight:600}
|
|
92
92
|
.drw-modal-body{flex:1;padding:12px 16px;overflow-y:auto}
|
|
93
|
+
|
|
94
|
+
.drw-health-bar{display:grid;grid-template-columns:repeat(auto-fit, minmax(130px, 1fr));gap:8px;padding:8px 12px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;font-size:11.5px;font-family:monospace}
|
|
95
|
+
.drw-health-item{display:flex;flex-direction:column;gap:2px}
|
|
96
|
+
.drw-progress{height:6px;background:var(--dsw-alias-bg-layer-1);border-radius:3px;overflow:hidden}
|
|
97
|
+
.drw-progress-fill{height:100%;border-radius:3px;transition:width .2s ease}
|
|
98
|
+
|
|
99
|
+
.drw-container-row{display:grid;grid-template-columns:2fr 2fr 1.5fr 1.5fr auto;align-items:center;gap:10px;padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:12.5px;font-family:monospace}
|
|
100
|
+
.drw-container-row:hover{background:var(--dsw-alias-bg-layer-3)}
|
|
101
|
+
|
|
102
|
+
.drw-env-table{width:100%;border-collapse:collapse}
|
|
103
|
+
.drw-env-row{display:grid;grid-template-columns:220px 1fr 90px;gap:8px;padding:6px 0;align-items:center;border-bottom:1px solid var(--dsw-alias-border-l2)}
|
|
104
|
+
.drw-alert-badge{padding:4px 8px;border-radius:6px;font-size:11px;font-family:monospace;display:flex;align-items:center;gap:6px}
|
|
105
|
+
.drw-alert-critical{background:rgba(239,68,68,0.15);color:var(--dsw-alias-state-error-primary);border:1px solid rgba(239,68,68,0.4)}
|
|
106
|
+
.drw-alert-warning{background:rgba(245,158,11,0.15);color:var(--dsw-alias-state-warning-primary);border:1px solid rgba(245,158,11,0.4)}
|
|
107
|
+
|
|
108
|
+
|
|
93
109
|
`
|
|
94
110
|
document.head.appendChild(style)
|
|
95
111
|
}
|
|
@@ -100,6 +116,48 @@ window.__ModuleLoader__.load({
|
|
|
100
116
|
status: 'Ready',
|
|
101
117
|
|
|
102
118
|
tabProfiles: '🖥️ Host Profiles',
|
|
119
|
+
|
|
120
|
+
tabContainers: '🐳 Containers',
|
|
121
|
+
|
|
122
|
+
tabEnv: '🗃️ .env Manager',
|
|
123
|
+
btnImportSsh: 'Import ~/.ssh/config',
|
|
124
|
+
envTitle: 'Remote Environment Variables (.env)',
|
|
125
|
+
envKey: 'Key',
|
|
126
|
+
envVal: 'Value',
|
|
127
|
+
envSaveVar: 'Save',
|
|
128
|
+
envShowVal: 'Show',
|
|
129
|
+
envHideVal: 'Hide',
|
|
130
|
+
tarSyncFast: '🚀 Fast Tarball Stream',
|
|
131
|
+
diagnoseTitle: 'Quick Diagnostics',
|
|
132
|
+
diagnosePorts: 'Open Ports',
|
|
133
|
+
diagnoseOom: 'OOM Killer Check',
|
|
134
|
+
diagnoseDisk: 'Disk Usage',
|
|
135
|
+
alertLabel: 'Alerts ({count})',
|
|
136
|
+
|
|
137
|
+
containersTitle: 'Remote Docker / Podman Engine',
|
|
138
|
+
containerName: 'Container',
|
|
139
|
+
containerImage: 'Image',
|
|
140
|
+
containerStatus: 'Status',
|
|
141
|
+
containerPorts: 'Ports',
|
|
142
|
+
btnLogs: 'Logs',
|
|
143
|
+
btnRestart: 'Restart',
|
|
144
|
+
btnStop: 'Stop',
|
|
145
|
+
noContainers: 'No active containers found or Docker daemon not running.',
|
|
146
|
+
healthCpu: 'CPU: {val}%',
|
|
147
|
+
healthMem: 'RAM: {val}% ({used}/{total} MB)',
|
|
148
|
+
healthDisk: 'Disk: {val}%',
|
|
149
|
+
healthUptime: 'Up: {val}',
|
|
150
|
+
autoSyncTitle: 'Live Auto-Sync on Save',
|
|
151
|
+
autoSyncDesc: 'Automatically push modified files to remote workspace on local save.',
|
|
152
|
+
btnTransfer: 'Copy to Host...',
|
|
153
|
+
transferTitle: 'Server-to-Server Direct Transfer',
|
|
154
|
+
transferDestHost: 'Destination Server',
|
|
155
|
+
transferDestPath: 'Destination File Path',
|
|
156
|
+
transferBtnExec: 'Transfer Directly',
|
|
157
|
+
transferSuccess: 'Successfully transferred {bytes} bytes to {host}:{path}',
|
|
158
|
+
fJumpHost: 'Jump Host (Bastion)',
|
|
159
|
+
fJumpHostNone: 'Direct Connection (No Jump Host)',
|
|
160
|
+
|
|
103
161
|
tabTerminal: '⚡ Web Terminal',
|
|
104
162
|
tabExplorer: '📂 File Explorer',
|
|
105
163
|
tabTunnels: '🔗 Port Tunnels',
|
|
@@ -191,80 +249,141 @@ window.__ModuleLoader__.load({
|
|
|
191
249
|
tOpenErr: 'Failed to open tunnel: {err}'
|
|
192
250
|
}
|
|
193
251
|
|
|
194
|
-
const
|
|
195
|
-
title: '
|
|
196
|
-
subtitle: 'SSH
|
|
197
|
-
status: '
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
252
|
+
const zh = {
|
|
253
|
+
title: '远程开发工作区',
|
|
254
|
+
subtitle: '支持原生 SSH 连接、SFTP 文件访问、3-way 镜像同步与端口转发隧道。',
|
|
255
|
+
status: '就绪',
|
|
256
|
+
|
|
257
|
+
tabProfiles: '🖥️ 主机配置',
|
|
258
|
+
tabContainers: '🐳 容器管理',
|
|
259
|
+
tabEnv: '🗃️ 环境变量',
|
|
260
|
+
btnImportSsh: '导入 ~/.ssh/config',
|
|
261
|
+
envTitle: '远程环境变量配置 (.env)',
|
|
262
|
+
envKey: '变量名',
|
|
263
|
+
envVal: '变量值',
|
|
264
|
+
envSaveVar: '保存',
|
|
265
|
+
envShowVal: '显示',
|
|
266
|
+
envHideVal: '隐藏',
|
|
267
|
+
tarSyncFast: '🚀 快速 Tarball 流式同步',
|
|
268
|
+
diagnoseTitle: '快速诊断',
|
|
269
|
+
diagnosePorts: '开放端口',
|
|
270
|
+
diagnoseOom: 'OOM 检查',
|
|
271
|
+
diagnoseDisk: '磁盘分析',
|
|
272
|
+
alertLabel: '告警 ({count})',
|
|
273
|
+
|
|
274
|
+
containersTitle: '远程 Docker / Podman 引擎',
|
|
275
|
+
containerName: '容器名',
|
|
276
|
+
containerImage: '镜像',
|
|
277
|
+
containerStatus: '运行状态',
|
|
278
|
+
containerPorts: '端口映射',
|
|
279
|
+
btnLogs: '日志',
|
|
280
|
+
btnRestart: '重启',
|
|
281
|
+
btnStop: '停止',
|
|
282
|
+
noContainers: '未找到运行中的容器或 Docker 守护进程未启动。',
|
|
283
|
+
healthCpu: 'CPU: {val}%',
|
|
284
|
+
healthMem: '内存: {val}% ({used}/{total} MB)',
|
|
285
|
+
healthDisk: '磁盘: {val}%',
|
|
286
|
+
healthUptime: '运行: {val}',
|
|
287
|
+
autoSyncTitle: '保存时自动增量同步',
|
|
288
|
+
autoSyncDesc: '本地文件保存时,后台自动增量推送到远程对应工作目录。',
|
|
289
|
+
btnTransfer: '跨主机传输...',
|
|
290
|
+
transferTitle: '主机间直接流式传输',
|
|
291
|
+
transferDestHost: '目标主机',
|
|
292
|
+
transferDestPath: '目标文件路径',
|
|
293
|
+
transferBtnExec: '立即直传',
|
|
294
|
+
transferSuccess: '成功传输 {bytes} 字节到 {host}:{path}',
|
|
295
|
+
fJumpHost: '跳板机 (Bastion)',
|
|
296
|
+
fJumpHostNone: '直连模式 (无跳板机)',
|
|
297
|
+
|
|
298
|
+
tabTerminal: '⚡ 网页终端',
|
|
299
|
+
tabExplorer: '📂 文件管理',
|
|
300
|
+
tabTunnels: '🔗 端口隧道',
|
|
301
|
+
tabSync: '🔄 同步控制台',
|
|
302
|
+
vaultProtectedBadge: '🛡️ .env 保管库加密保护',
|
|
303
|
+
termStart: '连接终端',
|
|
304
|
+
termStop: '断开连接',
|
|
305
|
+
termClear: '清屏',
|
|
306
|
+
termPlaceholder: '输入命令后回车 (如 htop, ls, docker ps)...',
|
|
307
|
+
termConnected: '已连接至主机 {host} 的 PTY 会话 {id}',
|
|
308
|
+
termDisconnected: '终端已断开。',
|
|
309
|
+
explorerTitle: '远程文件系统浏览',
|
|
310
|
+
filePreviewTitle: '文件查看器: {path}',
|
|
311
|
+
fileSize: '{size} KB',
|
|
312
|
+
btnSaveFile: '保存文件更改',
|
|
313
|
+
fileSaved: '文件保存成功。',
|
|
314
|
+
tunnelTraffic: '下行: {rx} | 上行: {tx}',
|
|
315
|
+
tunnelUptime: '运行: {sec}秒',
|
|
316
|
+
openLocalLink: '🌐 打开本地端口',
|
|
317
|
+
badgeOnline: '当前主机: {name} ({latency} ms)',
|
|
318
|
+
badgeOffline: '未选定活跃主机',
|
|
319
|
+
badgeTunnels: '活动隧道: {count}',
|
|
320
|
+
badgeKey: '🔑 私钥认证',
|
|
321
|
+
badgePassword: '🔒 密码认证',
|
|
322
|
+
profilesTitle: '🖥️ 远程 SSH 主机配置',
|
|
323
|
+
profilesDesc: '配置用于命令执行、SFTP 浏览与后台镜像同步的远程服务器。',
|
|
324
|
+
addBtn: '+ 添加主机',
|
|
325
|
+
nameCol: '配置名称',
|
|
326
|
+
hostCol: '主机 / 用户',
|
|
327
|
+
portCol: '端口',
|
|
328
|
+
pathCol: '远程工作区',
|
|
329
|
+
actionsCol: '操作',
|
|
330
|
+
noHosts: '暂无配置的主机。点击 "+ 添加主机" 建立首个连接。',
|
|
331
|
+
activeBadge: '活动中',
|
|
332
|
+
pinBtn: '设为默认',
|
|
333
|
+
editBtn: '编辑',
|
|
334
|
+
delBtn: '删除',
|
|
335
|
+
delConfirm: '确认删除主机配置 "{name}" 吗?',
|
|
336
|
+
formTitleNew: '添加新主机配置',
|
|
337
|
+
formTitleEdit: '编辑主机配置',
|
|
338
|
+
fName: '配置别名',
|
|
339
|
+
fHost: '主机名 / IP 地址',
|
|
340
|
+
fPort: '端口',
|
|
341
|
+
fUser: '登录用户名',
|
|
342
|
+
fAuthType: '身份认证方式',
|
|
343
|
+
fAuthKey: 'SSH 私钥',
|
|
344
|
+
fAuthPass: '账户密码',
|
|
345
|
+
fKeyPath: '私钥文件路径',
|
|
346
|
+
fKeyPathHint: '例如 ~/.ssh/id_rsa 或 /home/user/.ssh/id_ed25519',
|
|
347
|
+
fPassphrase: '私钥口令 (可选)',
|
|
348
|
+
fPassword: '登录密码',
|
|
349
|
+
fRemotePath: '远程项目工作区目录',
|
|
350
|
+
fRemotePathHint: '服务器上的绝对路径 (例如 /home/user/my-project)',
|
|
351
|
+
fLocalMirror: '本地镜像同步目录',
|
|
352
|
+
fLocalMirrorHint: '用于 3-way 差异比对与快速同步的本地镜像路径',
|
|
353
|
+
btnBrowse: '📁 浏览远程目录...',
|
|
354
|
+
btnSelectThis: '选择当前目录',
|
|
355
|
+
btnTest: '⚡ 测试网络连通性 (Ping)',
|
|
356
|
+
btnSave: '保存配置',
|
|
357
|
+
btnCancel: '取消',
|
|
358
|
+
testing: '正在测试连接...',
|
|
359
|
+
saving: '正在保存...',
|
|
360
|
+
testOk: '✅ 连接成功!延迟: {latency} ms | 系统: {os}',
|
|
361
|
+
testFail: '❌ 连接失败: {err}',
|
|
362
|
+
diagTitle: '📊 实时连接状态诊断',
|
|
363
|
+
diagDesc: '查看当前选定主机的网络延迟、系统资源及底层服务运行指标。',
|
|
364
|
+
diagProbeBtn: '⚡ 即时探测',
|
|
365
|
+
syncTitle: '🔄 双向工作区镜像同步',
|
|
366
|
+
syncDesc: '基于三方冲突感知的快速差异同步,支持本地与远程端双向推拉。',
|
|
367
|
+
pullBtn: '⬇️ 拉取至本地 (Pull)',
|
|
368
|
+
pushBtn: '⬆️ 推送至远程 (Push)',
|
|
369
|
+
pulling: '正在拉取文件…',
|
|
370
|
+
pushing: '正在推送文件…',
|
|
371
|
+
syncOk: '同步完成!共更新 {count} 个文件。',
|
|
372
|
+
syncConflict: '冲突警告: 检测到 {count} 个文件在两端均有修改。',
|
|
373
|
+
syncErr: '同步异常: {err}',
|
|
374
|
+
tunnelsTitle: '🔀 SSH 端口安全隧道',
|
|
375
|
+
tunnelsDesc: '通过加密 SSH 隧道将远程端口 (如数据库、开发服务、API) 映射至本地访问。',
|
|
376
|
+
tLocalPort: '本地监听端口',
|
|
377
|
+
tRemotePort: '远程目标端口',
|
|
378
|
+
tOpenBtn: '+ 建立隧道',
|
|
379
|
+
tOpening: '正在建立…',
|
|
380
|
+
tStopBtn: '关闭',
|
|
381
|
+
tNoTunnels: '当前没有运行中的端口转发隧道。',
|
|
382
|
+
tunnelsColId: '隧道标识',
|
|
383
|
+
tunnelsColRoute: '转发路由',
|
|
384
|
+
tunnelsColActions: '管理操作',
|
|
385
|
+
tOpenOk: '隧道已启动: 127.0.0.1:{localPort} -> Remote:{remotePort}',
|
|
386
|
+
tOpenErr: '启动隧道失败: {err}'
|
|
268
387
|
}
|
|
269
388
|
|
|
270
389
|
function makeT(dict, fallback) {
|
|
@@ -294,6 +413,261 @@ window.__ModuleLoader__.load({
|
|
|
294
413
|
}
|
|
295
414
|
|
|
296
415
|
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
// --- Sub-component: Remote Environment Tab (New in v0.3.1) ---
|
|
419
|
+
function EnvTab(props) {
|
|
420
|
+
const { activeProfile, t } = props
|
|
421
|
+
const [envEntries, setEnvEntries] = React.useState([])
|
|
422
|
+
const [loading, setLoading] = React.useState(false)
|
|
423
|
+
const [visibleKeys, setVisibleKeys] = React.useState({})
|
|
424
|
+
const [editValues, setEditValues] = React.useState({})
|
|
425
|
+
const [msg, setMsg] = React.useState('')
|
|
426
|
+
|
|
427
|
+
const loadEnv = async () => {
|
|
428
|
+
if (!activeProfile) return
|
|
429
|
+
setLoading(true)
|
|
430
|
+
try {
|
|
431
|
+
const res = await fetch('/dsh-remote-workspace/env/view', {
|
|
432
|
+
method: 'POST',
|
|
433
|
+
headers: { 'Content-Type': 'application/json' },
|
|
434
|
+
body: JSON.stringify({ profileId: activeProfile.id })
|
|
435
|
+
})
|
|
436
|
+
const data = await res.json()
|
|
437
|
+
if (data.ok && Array.isArray(data.entries)) {
|
|
438
|
+
setEnvEntries(data.entries.filter((e) => e.type === 'var'))
|
|
439
|
+
const initialEdits = {}
|
|
440
|
+
data.entries.forEach((e) => {
|
|
441
|
+
if (e.type === 'var') initialEdits[e.key] = e.value
|
|
442
|
+
})
|
|
443
|
+
setEditValues(initialEdits)
|
|
444
|
+
}
|
|
445
|
+
} catch (err) {
|
|
446
|
+
setMsg(`Error loading .env: ${err.message}`)
|
|
447
|
+
}
|
|
448
|
+
setLoading(false)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
React.useEffect(() => {
|
|
452
|
+
loadEnv()
|
|
453
|
+
}, [activeProfile?.id])
|
|
454
|
+
|
|
455
|
+
const toggleVisible = (key) => {
|
|
456
|
+
setVisibleKeys((prev) => ({ ...prev, [key]: !prev[key] }))
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const saveVar = async (key) => {
|
|
460
|
+
try {
|
|
461
|
+
const res = await fetch('/dsh-remote-workspace/env/save', {
|
|
462
|
+
method: 'POST',
|
|
463
|
+
headers: { 'Content-Type': 'application/json' },
|
|
464
|
+
body: JSON.stringify({
|
|
465
|
+
profileId: activeProfile.id,
|
|
466
|
+
key,
|
|
467
|
+
value: editValues[key] || ''
|
|
468
|
+
})
|
|
469
|
+
})
|
|
470
|
+
const data = await res.json()
|
|
471
|
+
if (data.ok) {
|
|
472
|
+
setMsg(`Saved ${key} successfully!`)
|
|
473
|
+
setTimeout(() => setMsg(''), 3000)
|
|
474
|
+
}
|
|
475
|
+
} catch (err) {
|
|
476
|
+
setMsg(`Save failed: ${err.message}`)
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return React.createElement(
|
|
481
|
+
'div',
|
|
482
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: '14px' } },
|
|
483
|
+
React.createElement(
|
|
484
|
+
'div',
|
|
485
|
+
{ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
486
|
+
React.createElement('div', { style: { fontWeight: 600, fontSize: '14px' } }, t('envTitle')),
|
|
487
|
+
React.createElement('button', { type: 'button', className: 'drw-btn', onClick: loadEnv, disabled: loading }, loading ? 'Loading...' : '🔄 Refresh')
|
|
488
|
+
),
|
|
489
|
+
msg ? React.createElement('div', { className: 'drw-badge drw-badge-ok' }, msg) : null,
|
|
490
|
+
React.createElement(
|
|
491
|
+
'div',
|
|
492
|
+
{ style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '8px', padding: '12px', background: 'var(--dsw-alias-bg-layer-2)' } },
|
|
493
|
+
envEntries.length === 0
|
|
494
|
+
? React.createElement('div', { style: { textAlign: 'center', padding: '16px', color: 'var(--dsw-alias-label-secondary)' } }, 'No variables found in remote .env')
|
|
495
|
+
: envEntries.map((e) =>
|
|
496
|
+
React.createElement(
|
|
497
|
+
'div',
|
|
498
|
+
{ key: e.key, className: 'drw-env-row' },
|
|
499
|
+
React.createElement('span', { style: { fontWeight: 600, fontFamily: 'monospace', fontSize: '12.5px' } }, e.key),
|
|
500
|
+
React.createElement('input', {
|
|
501
|
+
type: visibleKeys[e.key] ? 'text' : 'password',
|
|
502
|
+
className: 'drw-input',
|
|
503
|
+
value: editValues[e.key] !== undefined ? editValues[e.key] : e.value,
|
|
504
|
+
onChange: (ev) => {
|
|
505
|
+
const val = ev.target.value
|
|
506
|
+
setEditValues((prev) => ({ ...prev, [e.key]: val }))
|
|
507
|
+
}
|
|
508
|
+
}),
|
|
509
|
+
React.createElement(
|
|
510
|
+
'div',
|
|
511
|
+
{ style: { display: 'flex', gap: '4px', justifyContent: 'flex-end' } },
|
|
512
|
+
React.createElement('button', { type: 'button', className: 'drw-btn', style: { padding: '2px 6px', fontSize: '11px' }, onClick: () => toggleVisible(e.key) }, visibleKeys[e.key] ? t('envHideVal') : t('envShowVal')),
|
|
513
|
+
React.createElement('button', { type: 'button', className: 'drw-btn drw-btn-primary', style: { padding: '2px 6px', fontSize: '11px' }, onClick: () => saveVar(e.key) }, t('envSaveVar'))
|
|
514
|
+
)
|
|
515
|
+
)
|
|
516
|
+
)
|
|
517
|
+
)
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// --- Sub-component: Docker Containers Tab ---
|
|
522
|
+
function ContainersTab(props) {
|
|
523
|
+
const { activeProfile, t } = props
|
|
524
|
+
const [containers, setContainers] = React.useState([])
|
|
525
|
+
const [loading, setLoading] = React.useState(false)
|
|
526
|
+
const [logsModal, setLogsModal] = React.useState(null)
|
|
527
|
+
const [logsContent, setLogsContent] = React.useState('')
|
|
528
|
+
const [logsLoading, setLogsLoading] = React.useState(false)
|
|
529
|
+
|
|
530
|
+
const loadContainers = async () => {
|
|
531
|
+
if (!activeProfile) return
|
|
532
|
+
setLoading(true)
|
|
533
|
+
try {
|
|
534
|
+
const res = await fetch(`/dsh-remote-workspace/docker/list?profileId=${activeProfile.id}`)
|
|
535
|
+
const data = await res.json()
|
|
536
|
+
if (data.ok && Array.isArray(data.containers)) {
|
|
537
|
+
setContainers(data.containers)
|
|
538
|
+
} else {
|
|
539
|
+
setContainers([])
|
|
540
|
+
}
|
|
541
|
+
} catch (_) {}
|
|
542
|
+
setLoading(false)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
React.useEffect(() => {
|
|
546
|
+
loadContainers()
|
|
547
|
+
}, [activeProfile?.id])
|
|
548
|
+
|
|
549
|
+
const handleAction = async (containerId, action) => {
|
|
550
|
+
try {
|
|
551
|
+
await fetch('/dsh-remote-workspace/docker/action', {
|
|
552
|
+
method: 'POST',
|
|
553
|
+
headers: { 'Content-Type': 'application/json' },
|
|
554
|
+
body: JSON.stringify({ profileId: activeProfile.id, containerId, action })
|
|
555
|
+
})
|
|
556
|
+
await loadContainers()
|
|
557
|
+
} catch (_) {}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const showLogs = async (containerId, name) => {
|
|
561
|
+
setLogsModal({ id: containerId, name })
|
|
562
|
+
setLogsLoading(true)
|
|
563
|
+
try {
|
|
564
|
+
const res = await fetch('/dsh-remote-workspace/docker/logs', {
|
|
565
|
+
method: 'POST',
|
|
566
|
+
headers: { 'Content-Type': 'application/json' },
|
|
567
|
+
body: JSON.stringify({ profileId: activeProfile.id, containerId, tail: 150 })
|
|
568
|
+
})
|
|
569
|
+
const data = await res.json()
|
|
570
|
+
setLogsContent(data.logs || '[No logs output]')
|
|
571
|
+
} catch (err) {
|
|
572
|
+
setLogsContent(`[Error: ${err.message}]`)
|
|
573
|
+
}
|
|
574
|
+
setLogsLoading(false)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return React.createElement(
|
|
578
|
+
'div',
|
|
579
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: '12px' } },
|
|
580
|
+
React.createElement(
|
|
581
|
+
'div',
|
|
582
|
+
{ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
583
|
+
React.createElement('div', { style: { fontWeight: 600, fontSize: '14px' } }, t('containersTitle')),
|
|
584
|
+
React.createElement(
|
|
585
|
+
'button',
|
|
586
|
+
{
|
|
587
|
+
type: 'button',
|
|
588
|
+
className: 'drw-btn',
|
|
589
|
+
onClick: loadContainers,
|
|
590
|
+
disabled: loading
|
|
591
|
+
},
|
|
592
|
+
loading ? 'Refreshing...' : '🔄 Refresh'
|
|
593
|
+
)
|
|
594
|
+
),
|
|
595
|
+
React.createElement(
|
|
596
|
+
'div',
|
|
597
|
+
{ style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '8px', overflow: 'hidden', background: 'var(--dsw-alias-bg-layer-2)' } },
|
|
598
|
+
React.createElement(
|
|
599
|
+
'div',
|
|
600
|
+
{ className: 'drw-container-row', style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)', background: 'var(--dsw-alias-bg-layer-1)' } },
|
|
601
|
+
React.createElement('span', null, t('containerName')),
|
|
602
|
+
React.createElement('span', null, t('containerImage')),
|
|
603
|
+
React.createElement('span', null, t('containerStatus')),
|
|
604
|
+
React.createElement('span', null, t('containerPorts')),
|
|
605
|
+
React.createElement('span', { style: { textAlign: 'right' } }, t('actionsCol'))
|
|
606
|
+
),
|
|
607
|
+
containers.length === 0
|
|
608
|
+
? React.createElement('div', { style: { padding: '24px', textAlign: 'center', color: 'var(--dsw-alias-label-secondary)' } }, t('noContainers'))
|
|
609
|
+
: containers.map((c) =>
|
|
610
|
+
React.createElement(
|
|
611
|
+
'div',
|
|
612
|
+
{ key: c.id, className: 'drw-container-row' },
|
|
613
|
+
React.createElement(
|
|
614
|
+
'div',
|
|
615
|
+
{ style: { display: 'flex', alignItems: 'center', gap: '6px' } },
|
|
616
|
+
React.createElement('span', { className: `drw-badge ${c.state === 'running' ? 'drw-badge-ok' : 'drw-badge-warn'}` }, c.state),
|
|
617
|
+
React.createElement('span', { style: { fontWeight: 600 } }, c.names)
|
|
618
|
+
),
|
|
619
|
+
React.createElement('span', { style: { opacity: 0.8, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, c.image),
|
|
620
|
+
React.createElement('span', { style: { fontSize: '11px', opacity: 0.7 } }, c.status),
|
|
621
|
+
React.createElement('span', { style: { fontSize: '11px', opacity: 0.7, overflow: 'hidden', textOverflow: 'ellipsis' } }, c.ports || '—'),
|
|
622
|
+
React.createElement(
|
|
623
|
+
'div',
|
|
624
|
+
{ style: { display: 'flex', gap: '4px', justifyContent: 'flex-end' } },
|
|
625
|
+
React.createElement(
|
|
626
|
+
'button',
|
|
627
|
+
{ type: 'button', className: 'drw-btn', style: { padding: '2px 6px', fontSize: '11px' }, onClick: () => showLogs(c.id, c.names) },
|
|
628
|
+
t('btnLogs')
|
|
629
|
+
),
|
|
630
|
+
React.createElement(
|
|
631
|
+
'button',
|
|
632
|
+
{ type: 'button', className: 'drw-btn', style: { padding: '2px 6px', fontSize: '11px' }, onClick: () => handleAction(c.id, 'restart') },
|
|
633
|
+
t('btnRestart')
|
|
634
|
+
),
|
|
635
|
+
c.state === 'running'
|
|
636
|
+
? React.createElement(
|
|
637
|
+
'button',
|
|
638
|
+
{ type: 'button', className: 'drw-btn drw-btn-danger', style: { padding: '2px 6px', fontSize: '11px' }, onClick: () => handleAction(c.id, 'stop') },
|
|
639
|
+
t('btnStop')
|
|
640
|
+
)
|
|
641
|
+
: null
|
|
642
|
+
)
|
|
643
|
+
)
|
|
644
|
+
)
|
|
645
|
+
),
|
|
646
|
+
// Container Logs Modal
|
|
647
|
+
logsModal
|
|
648
|
+
? React.createElement(
|
|
649
|
+
'div',
|
|
650
|
+
{ className: 'drw-modal-overlay', onClick: () => setLogsModal(null) },
|
|
651
|
+
React.createElement(
|
|
652
|
+
'div',
|
|
653
|
+
{ className: 'drw-modal-dialog', onClick: (e) => e.stopPropagation() },
|
|
654
|
+
React.createElement(
|
|
655
|
+
'div',
|
|
656
|
+
{ className: 'drw-modal-hdr' },
|
|
657
|
+
React.createElement('span', { style: { fontFamily: 'monospace' } }, `Docker Logs: ${logsModal.name} (${logsModal.id})`),
|
|
658
|
+
React.createElement('button', { type: 'button', className: 'drw-btn', onClick: () => setLogsModal(null) }, '✕')
|
|
659
|
+
),
|
|
660
|
+
React.createElement(
|
|
661
|
+
'div',
|
|
662
|
+
{ className: 'drw-modal-body' },
|
|
663
|
+
React.createElement('pre', { style: { background: '#0d1117', color: '#c9d1d9', padding: '12px', borderRadius: '8px', fontSize: '12px', maxHeight: '420px', overflowY: 'auto', whiteSpace: 'pre-wrap' } }, logsLoading ? 'Fetching logs...' : logsContent)
|
|
664
|
+
)
|
|
665
|
+
)
|
|
666
|
+
)
|
|
667
|
+
: null
|
|
668
|
+
)
|
|
669
|
+
}
|
|
670
|
+
|
|
297
671
|
// --- Sub-component: Web Terminal Tab ---
|
|
298
672
|
function TerminalTab(props) {
|
|
299
673
|
const { activeProfile, t } = props
|
|
@@ -737,8 +1111,10 @@ window.__ModuleLoader__.load({
|
|
|
737
1111
|
|
|
738
1112
|
function SettingsView(props) {
|
|
739
1113
|
const [activeTab, setActiveTab] = React.useState('profiles')
|
|
1114
|
+
const [healthData, setHealthData] = React.useState(null)
|
|
1115
|
+
const [autoSync, setAutoSync] = React.useState(false)
|
|
740
1116
|
|
|
741
|
-
const t = props.locale === '
|
|
1117
|
+
const t = props.t || (props.locale === 'zh' ? makeT(zh, en) : makeT(en, zh))
|
|
742
1118
|
const [profiles, setProfiles] = React.useState([])
|
|
743
1119
|
const [activeId, setActiveId] = React.useState(null)
|
|
744
1120
|
const [tunnels, setTunnels] = React.useState([])
|
|
@@ -768,6 +1144,10 @@ window.__ModuleLoader__.load({
|
|
|
768
1144
|
setProfiles(data.profiles || [])
|
|
769
1145
|
setActiveId(data.activeId || null)
|
|
770
1146
|
setTunnels(data.tunnels || [])
|
|
1147
|
+
setAutoSync(Boolean(data.autoSync))
|
|
1148
|
+
if (data.activeId) {
|
|
1149
|
+
fetch(`/dsh-remote-workspace/health?profileId=${data.activeId}`).then(r => r.json()).then(h => { if (h.ok) setHealthData(h.health) }).catch(() => {})
|
|
1150
|
+
}
|
|
771
1151
|
}
|
|
772
1152
|
}
|
|
773
1153
|
} catch (_) {}
|
|
@@ -1482,6 +1862,39 @@ window.__ModuleLoader__.load({
|
|
|
1482
1862
|
{ className: 'drw-card' },
|
|
1483
1863
|
React.createElement('div', { className: 'drw-card-title' }, t('syncTitle')),
|
|
1484
1864
|
React.createElement('div', { className: 'drw-card-desc' }, t('syncDesc')),
|
|
1865
|
+
|
|
1866
|
+
React.createElement(
|
|
1867
|
+
'div',
|
|
1868
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)', marginBottom: '12px' } },
|
|
1869
|
+
React.createElement(
|
|
1870
|
+
'div',
|
|
1871
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
|
|
1872
|
+
React.createElement('span', { style: { fontWeight: 600, fontSize: '13px' } }, t('autoSyncTitle')),
|
|
1873
|
+
React.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } }, t('autoSyncDesc'))
|
|
1874
|
+
),
|
|
1875
|
+
React.createElement(
|
|
1876
|
+
'button',
|
|
1877
|
+
{
|
|
1878
|
+
type: 'button',
|
|
1879
|
+
className: `drw-btn ${autoSync ? 'drw-btn-primary' : ''}`,
|
|
1880
|
+
style: { padding: '4px 12px', fontSize: '12px' },
|
|
1881
|
+
onClick: async () => {
|
|
1882
|
+
const next = !autoSync
|
|
1883
|
+
try {
|
|
1884
|
+
const res = await fetch('/dsh-remote-workspace/watcher/toggle', {
|
|
1885
|
+
method: 'POST',
|
|
1886
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1887
|
+
body: JSON.stringify({ enabled: next })
|
|
1888
|
+
})
|
|
1889
|
+
const d = await res.json()
|
|
1890
|
+
if (d.ok) setAutoSync(d.autoSync)
|
|
1891
|
+
} catch (_) {}
|
|
1892
|
+
}
|
|
1893
|
+
},
|
|
1894
|
+
autoSync ? '⚡ Active (ON)' : '⚪ Off'
|
|
1895
|
+
)
|
|
1896
|
+
),
|
|
1897
|
+
|
|
1485
1898
|
React.createElement(
|
|
1486
1899
|
'div',
|
|
1487
1900
|
{ className: 'drw-row' },
|
|
@@ -1641,7 +2054,7 @@ window.__ModuleLoader__.load({
|
|
|
1641
2054
|
// Accordion item for Settings -> Plugins section
|
|
1642
2055
|
function PluginCard(props) {
|
|
1643
2056
|
const [open, setOpen] = React.useState(false)
|
|
1644
|
-
const t = props.locale === '
|
|
2057
|
+
const t = props.t || (props.locale === 'zh' ? makeT(zh, en) : makeT(en, zh))
|
|
1645
2058
|
|
|
1646
2059
|
React.useEffect(() => {
|
|
1647
2060
|
ensureCss()
|
|
@@ -1731,7 +2144,7 @@ window.__ModuleLoader__.load({
|
|
|
1731
2144
|
|
|
1732
2145
|
function apply(ctx) {
|
|
1733
2146
|
if (ctx.locale && typeof ctx.locale.register === 'function') {
|
|
1734
|
-
try { ctx.locale.register(NS, { en,
|
|
2147
|
+
try { ctx.locale.register(NS, { en, zh }) } catch (_) {}
|
|
1735
2148
|
}
|
|
1736
2149
|
|
|
1737
2150
|
if (ctx.slots && typeof ctx.slots.inject === 'function') {
|