@goodandready/dsh-remote-workspace 0.3.7 → 0.3.9
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 +49 -2
- package/README.ru.md +49 -2
- package/README.zh.md +49 -2
- package/lib/agent-endpoint.js +16 -0
- package/lib/client.js +1367 -418
- package/lib/cluster-service.js +57 -0
- package/lib/exec-retry.js +24 -0
- package/lib/file-transfer.js +56 -0
- package/lib/host-projection.js +42 -0
- package/lib/index.js +23 -4
- package/lib/jump-chain.js +56 -0
- package/lib/poll-gate.js +5 -0
- package/lib/profile-groups.js +60 -0
- package/lib/proxy-command.js +53 -0
- package/lib/remote-fs-service.js +25 -0
- package/lib/routes/files.js +75 -0
- package/lib/routes/profiles.js +82 -8
- package/lib/routes/terminal.js +19 -0
- package/lib/ssh-config-parser.js +111 -49
- package/lib/ssh-service.js +180 -21
- package/lib/terminal-font.js +12 -0
- package/lib/tools.js +52 -1
- package/lib/tunnel-service.js +3 -0
- package/lib/types.js +7 -0
- package/lib/updater-service.js +3 -18
- package/lib/workspace-mount.js +5 -0
- package/package.json +1 -2
- package/docs/design/DESIGN.md +0 -91
package/lib/client.js
CHANGED
|
@@ -77,10 +77,10 @@ window.__ModuleLoader__.load({
|
|
|
77
77
|
.drw-tab-btn.active{background:var(--dsw-alias-bg-layer-4);border-color:var(--dsw-alias-state-brand-primary);color:var(--dsw-alias-state-brand-primary)}
|
|
78
78
|
|
|
79
79
|
.drw-term-window{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;display:flex;flex-direction:column;height:340px;overflow:hidden;box-shadow:0 4px 14px var(--dsw-alias-border-l1)}
|
|
80
|
-
.drw-term-header{background:var(--dsw-alias-bg-layer-2);padding:6px 12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:12px;color:var(--dsw-alias-label-secondary);font-family:monospace}
|
|
81
|
-
.drw-term-body{flex:1;padding:10px 12px;overflow-y:auto;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12.5px;line-height:1.45;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-all}
|
|
80
|
+
.drw-term-header{background:var(--dsw-alias-bg-layer-2);padding:6px 12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:12px;color:var(--dsw-alias-label-secondary);font-family:var(--drw-terminal-font,monospace)}
|
|
81
|
+
.drw-term-body{flex:1;padding:10px 12px;overflow-y:auto;font-family:var(--drw-terminal-font,ui-monospace,SFMono-Regular,Consolas,monospace);font-size:12.5px;line-height:1.45;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-all}
|
|
82
82
|
.drw-term-footer{display:flex;background:var(--dsw-alias-bg-layer-2);border-top:1px solid var(--dsw-alias-border-l2);padding:6px}
|
|
83
|
-
.drw-term-input{flex:1;background:transparent;border:none;color:var(--dsw-alias-link-primary, var(--dsw-alias-label-primary));font-family:monospace;font-size:13px;padding:4px 8px;outline:none}
|
|
83
|
+
.drw-term-input{flex:1;background:transparent;border:none;color:var(--dsw-alias-link-primary, var(--dsw-alias-label-primary));font-family:var(--drw-terminal-font,monospace);font-size:13px;padding:4px 8px;outline:none}
|
|
84
84
|
|
|
85
85
|
.drw-explorer-box{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);padding:12px;display:flex;flex-direction:column;gap:10px}
|
|
86
86
|
.drw-explorer-nav{display:flex;align-items:center;gap:8px;font-family:monospace;font-size:12px;background:var(--dsw-alias-bg-layer-1);padding:6px 10px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2)}
|
|
@@ -109,14 +109,51 @@ window.__ModuleLoader__.load({
|
|
|
109
109
|
.drw-alert-warning{background:var(--dsw-alias-state-warning-bg, var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-state-warning-primary);border:1px solid var(--dsw-alias-state-warning-border, var(--dsw-alias-border-l2))}
|
|
110
110
|
|
|
111
111
|
|
|
112
|
+
.drw-ws{min-height:100%;box-sizing:border-box;color:var(--dsw-alias-label-primary)}
|
|
113
|
+
.drw-ws-bar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;padding:12px 16px;position:sticky;top:0;background:var(--dsw-alias-bg-base,#171717);z-index:2}
|
|
114
|
+
.drw-ws-panel{padding:0 16px 24px}
|
|
115
|
+
.drw-ws-panel[hidden]{display:none!important}
|
|
116
|
+
[data-pane=conversation],[class*=centerCol]{position:relative!important}
|
|
117
|
+
[data-drw-view]{display:none;position:absolute;inset:0;z-index:60;overflow:auto;background:var(--dsw-alias-bg-base,#171717)}
|
|
118
|
+
html[data-drw-active] [data-drw-view]{display:block!important}
|
|
119
|
+
html[data-drw-active] [data-pane=conversation] > *:not([data-drw-view]),html[data-drw-active] [class*=centerCol] > *:not([data-drw-view]){display:none!important}
|
|
120
|
+
.drw-entry-clone{display:flex;align-items:center;gap:8px}
|
|
121
|
+
.drw-entry-icon{display:flex;align-items:center;justify-content:center;width:16px;height:16px;flex:none}
|
|
122
|
+
[data-dsh-frame][data-sidebar-collapsed] .drw-entry-label{display:none}
|
|
123
|
+
|
|
112
124
|
`
|
|
113
125
|
document.head.appendChild(style)
|
|
114
126
|
}
|
|
115
127
|
|
|
116
128
|
const en = {
|
|
117
129
|
title: 'Remote Workspace',
|
|
130
|
+
navLabel: 'Remote',
|
|
131
|
+
wsBack: 'Back',
|
|
132
|
+
wsHosts: 'Hosts',
|
|
133
|
+
wsTerminal: 'Terminal',
|
|
134
|
+
wsFiles: 'Files',
|
|
135
|
+
wsDocker: 'Containers',
|
|
136
|
+
wsTunnels: 'Tunnels',
|
|
137
|
+
wsCluster: 'Cluster',
|
|
118
138
|
subtitle: 'Native SSH connections, SFTP file access, 3-way mirror sync, and port forwarding tunnels.',
|
|
119
139
|
status: 'Ready',
|
|
140
|
+
actionFailed: 'The request failed.',
|
|
141
|
+
versionUnknown: 'Version unknown',
|
|
142
|
+
updaterTitle: 'Plugin update',
|
|
143
|
+
currentVersion: 'Installed version',
|
|
144
|
+
latestVersion: 'Latest on npm',
|
|
145
|
+
checkUpdate: 'Check for updates',
|
|
146
|
+
checking: 'Checking...',
|
|
147
|
+
updateNow: 'Update now',
|
|
148
|
+
updating: 'Updating...',
|
|
149
|
+
upToDate: 'Up to date',
|
|
150
|
+
updateAvailable: 'Update available',
|
|
151
|
+
restartRequired: 'The update is installed. Restart DSH to use it.',
|
|
152
|
+
updateFailed: 'Update failed',
|
|
153
|
+
statusLoading: 'Checking connection',
|
|
154
|
+
statusEmpty: 'No active host',
|
|
155
|
+
statusReady: 'Host ready',
|
|
156
|
+
statusError: 'Status unavailable',
|
|
120
157
|
|
|
121
158
|
tabProfiles: '🖥️ Host Profiles',
|
|
122
159
|
|
|
@@ -124,6 +161,11 @@ window.__ModuleLoader__.load({
|
|
|
124
161
|
|
|
125
162
|
tabEnv: '🗃️ .env Manager',
|
|
126
163
|
btnImportSsh: 'Import ~/.ssh/config',
|
|
164
|
+
importAdded: 'Imported {count} hosts.',
|
|
165
|
+
skipWildcard: 'pattern is not a single host',
|
|
166
|
+
skipMatch: 'Match blocks are not imported',
|
|
167
|
+
skipDuplicate: 'already imported',
|
|
168
|
+
skipMissing: 'included file was not found',
|
|
127
169
|
envTitle: 'Remote Environment Variables (.env)',
|
|
128
170
|
envKey: 'Key',
|
|
129
171
|
envVal: 'Value',
|
|
@@ -208,6 +250,47 @@ window.__ModuleLoader__.load({
|
|
|
208
250
|
fAuthType: 'Authentication Method',
|
|
209
251
|
fAuthKey: 'SSH Private Key',
|
|
210
252
|
fAuthPass: 'Password',
|
|
253
|
+
fEnvironment: 'Environment',
|
|
254
|
+
fLocation: 'Location',
|
|
255
|
+
fTags: 'Tags',
|
|
256
|
+
fDescription: 'Description',
|
|
257
|
+
groupFlat: 'Flat',
|
|
258
|
+
groupEnvironment: 'By environment',
|
|
259
|
+
groupTag: 'By tag',
|
|
260
|
+
groupUngrouped: 'Ungrouped',
|
|
261
|
+
testGroup: 'Test group',
|
|
262
|
+
groupOnline: 'online, {latency} ms',
|
|
263
|
+
groupOffline: 'offline: {err}',
|
|
264
|
+
tabProfiles: 'Hosts',
|
|
265
|
+
tabCluster: 'Cluster',
|
|
266
|
+
tabFiles: 'Files',
|
|
267
|
+
btnUpload: 'Upload',
|
|
268
|
+
btnDownload: 'Download',
|
|
269
|
+
transferProgress: '{name}: {percent}%',
|
|
270
|
+
transferFailed: 'Transfer failed: {name}',
|
|
271
|
+
transferCancel: 'Cancel',
|
|
272
|
+
clusterTitle: 'Run on a group',
|
|
273
|
+
clusterCommand: 'Command',
|
|
274
|
+
clusterRun: 'Run',
|
|
275
|
+
clusterRunning: 'Running...',
|
|
276
|
+
clusterCount: '{count} hosts configured',
|
|
277
|
+
clusterOk: 'ok',
|
|
278
|
+
clusterFail: 'failed',
|
|
279
|
+
fAuthAgent: 'SSH Agent',
|
|
280
|
+
authPromptTitle: 'Sign-in code',
|
|
281
|
+
authPromptCode: 'One-time code',
|
|
282
|
+
authPromptSubmit: 'Continue',
|
|
283
|
+
fAuthKeyShort: 'key',
|
|
284
|
+
fAuthPassShort: 'pwd',
|
|
285
|
+
fAuthAgentShort: 'agent',
|
|
286
|
+
fAgentPath: 'Agent socket',
|
|
287
|
+
fAgentPathHint: 'Empty uses SSH_AUTH_SOCK. Use pageant on Windows.',
|
|
288
|
+
fJumpHosts: 'Jump hosts',
|
|
289
|
+
fJumpHostsHint: 'Profile ids in order, separated by commas. The first host is the entry bastion.',
|
|
290
|
+
fProxyCommand: 'Proxy command',
|
|
291
|
+
fProxyCommandHint: 'OpenSSH ProxyCommand. Tokens: %h host, %p port, %r user, %n profile name.',
|
|
292
|
+
fTerminalFont: 'Terminal font',
|
|
293
|
+
fTerminalFontHint: 'Font family for the terminal. Leave empty to keep the default monospace stack.',
|
|
211
294
|
fKeyPath: 'Private Key Path',
|
|
212
295
|
fKeyPathHint: 'Path to ~/.ssh/id_rsa or /home/user/.ssh/id_ed25519',
|
|
213
296
|
fPassphrase: 'Key Passphrase (optional)',
|
|
@@ -254,13 +337,32 @@ window.__ModuleLoader__.load({
|
|
|
254
337
|
|
|
255
338
|
const zh = {
|
|
256
339
|
title: '远程开发工作区',
|
|
340
|
+
navLabel: '远程',
|
|
341
|
+
wsBack: '返回',
|
|
342
|
+
wsHosts: '主机',
|
|
343
|
+
wsTerminal: '终端',
|
|
344
|
+
wsFiles: '文件',
|
|
345
|
+
wsDocker: '容器',
|
|
346
|
+
wsTunnels: '隧道',
|
|
347
|
+
wsCluster: '集群',
|
|
257
348
|
subtitle: '支持原生 SSH 连接、SFTP 文件访问、3-way 镜像同步与端口转发隧道。',
|
|
258
349
|
status: '就绪',
|
|
350
|
+
actionFailed: '请求失败。',
|
|
351
|
+
versionUnknown: '版本未知',
|
|
352
|
+
statusLoading: '正在检查连接',
|
|
353
|
+
statusEmpty: '没有活动主机',
|
|
354
|
+
statusReady: '主机已就绪',
|
|
355
|
+
statusError: '状态不可用',
|
|
259
356
|
|
|
260
357
|
tabProfiles: '🖥️ 主机配置',
|
|
261
358
|
tabContainers: '🐳 容器管理',
|
|
262
359
|
tabEnv: '🗃️ 环境变量',
|
|
263
360
|
btnImportSsh: '导入 ~/.ssh/config',
|
|
361
|
+
importAdded: '已导入 {count} 台主机。',
|
|
362
|
+
skipWildcard: '通配主机不会导入',
|
|
363
|
+
skipMatch: '不导入 Match 块',
|
|
364
|
+
skipDuplicate: '已经导入',
|
|
365
|
+
skipMissing: '找不到被包含的文件',
|
|
264
366
|
envTitle: '远程环境变量配置 (.env)',
|
|
265
367
|
envKey: '变量名',
|
|
266
368
|
envVal: '变量值',
|
|
@@ -356,6 +458,47 @@ window.__ModuleLoader__.load({
|
|
|
356
458
|
fAuthType: '身份认证方式',
|
|
357
459
|
fAuthKey: 'SSH 私钥',
|
|
358
460
|
fAuthPass: '账户密码',
|
|
461
|
+
fEnvironment: '环境',
|
|
462
|
+
fLocation: '位置',
|
|
463
|
+
fTags: '标签',
|
|
464
|
+
fDescription: '说明',
|
|
465
|
+
groupFlat: '平铺',
|
|
466
|
+
groupEnvironment: '按环境',
|
|
467
|
+
groupTag: '按标签',
|
|
468
|
+
groupUngrouped: '未分组',
|
|
469
|
+
testGroup: '测试本组',
|
|
470
|
+
groupOnline: '在线,{latency} 毫秒',
|
|
471
|
+
groupOffline: '离线:{err}',
|
|
472
|
+
tabProfiles: '主机',
|
|
473
|
+
tabCluster: '集群',
|
|
474
|
+
tabFiles: '文件',
|
|
475
|
+
btnUpload: '上传',
|
|
476
|
+
btnDownload: '下载',
|
|
477
|
+
transferProgress: '{name}:{percent}%',
|
|
478
|
+
transferFailed: '传输失败:{name}',
|
|
479
|
+
transferCancel: '取消',
|
|
480
|
+
clusterTitle: '在一组主机上运行',
|
|
481
|
+
clusterCommand: '命令',
|
|
482
|
+
clusterRun: '运行',
|
|
483
|
+
clusterRunning: '正在运行...',
|
|
484
|
+
clusterCount: '已配置 {count} 台主机',
|
|
485
|
+
clusterOk: '成功',
|
|
486
|
+
clusterFail: '失败',
|
|
487
|
+
fAuthAgent: 'SSH 代理',
|
|
488
|
+
authPromptTitle: '登录验证码',
|
|
489
|
+
authPromptCode: '一次性验证码',
|
|
490
|
+
authPromptSubmit: '继续',
|
|
491
|
+
fAuthKeyShort: '密钥',
|
|
492
|
+
fAuthPassShort: '密码',
|
|
493
|
+
fAuthAgentShort: '代理',
|
|
494
|
+
fAgentPath: '代理套接字',
|
|
495
|
+
fAgentPathHint: '留空则使用 SSH_AUTH_SOCK。Windows 上填写 pageant。',
|
|
496
|
+
fJumpHosts: '跳板主机',
|
|
497
|
+
fJumpHostsHint: '按经过顺序填写配置编号,用逗号分隔。第一台是入口堡垒机。',
|
|
498
|
+
fProxyCommand: '代理命令',
|
|
499
|
+
fProxyCommandHint: 'OpenSSH ProxyCommand。令牌:%h 主机,%p 端口,%r 用户,%n 配置名称。',
|
|
500
|
+
fTerminalFont: '终端字体',
|
|
501
|
+
fTerminalFontHint: '终端使用的字体族。留空则使用默认等宽字体。',
|
|
359
502
|
fKeyPath: '私钥文件路径',
|
|
360
503
|
fKeyPathHint: '例如 ~/.ssh/id_rsa 或 /home/user/.ssh/id_ed25519',
|
|
361
504
|
fPassphrase: '私钥口令 (可选)',
|
|
@@ -694,6 +837,13 @@ window.__ModuleLoader__.load({
|
|
|
694
837
|
|
|
695
838
|
// --- Sub-component: Web Terminal Tab ---
|
|
696
839
|
|
|
840
|
+
function applyTerminalFont(font) {
|
|
841
|
+
if (typeof document === 'undefined') return
|
|
842
|
+
const value = String(font || '').trim()
|
|
843
|
+
if (value) document.documentElement.style.setProperty('--drw-terminal-font', value)
|
|
844
|
+
else document.documentElement.style.removeProperty('--drw-terminal-font')
|
|
845
|
+
}
|
|
846
|
+
|
|
697
847
|
function TerminalTab(props) {
|
|
698
848
|
const { activeProfile, t } = props
|
|
699
849
|
const [connected, setConnected] = React.useState(false)
|
|
@@ -879,7 +1029,7 @@ window.__ModuleLoader__.load({
|
|
|
879
1029
|
? React.createElement(
|
|
880
1030
|
'form',
|
|
881
1031
|
{ className: 'drw-term-footer', onSubmit: sendCommand },
|
|
882
|
-
React.createElement('span', { style: { color: 'var(--dsw-alias-link-primary)', padding: '4px 6px', fontFamily: '
|
|
1032
|
+
React.createElement('span', { style: { color: 'var(--dsw-alias-link-primary)', padding: '4px 6px', fontFamily: 'inherit' } }, '$'),
|
|
883
1033
|
React.createElement('input', {
|
|
884
1034
|
className: 'drw-term-input',
|
|
885
1035
|
type: 'text',
|
|
@@ -910,6 +1060,8 @@ window.__ModuleLoader__.load({
|
|
|
910
1060
|
const [editContent, setEditContent] = React.useState('')
|
|
911
1061
|
const [previewLoading, setPreviewLoading] = React.useState(false)
|
|
912
1062
|
const [saveStatus, setSaveStatus] = React.useState(null)
|
|
1063
|
+
const [transfer, setTransfer] = React.useState(null)
|
|
1064
|
+
const transferAbort = React.useRef(null)
|
|
913
1065
|
|
|
914
1066
|
const loadDir = async (dirPath) => {
|
|
915
1067
|
if (!activeProfile) return
|
|
@@ -967,6 +1119,75 @@ window.__ModuleLoader__.load({
|
|
|
967
1119
|
setPreviewLoading(false)
|
|
968
1120
|
}
|
|
969
1121
|
|
|
1122
|
+
|
|
1123
|
+
const filePathFor = (entry) => currentPath.endsWith('/') ? currentPath + entry.filename : currentPath + '/' + entry.filename
|
|
1124
|
+
|
|
1125
|
+
const downloadFile = async (entry, event) => {
|
|
1126
|
+
if (event) event.stopPropagation()
|
|
1127
|
+
if (!activeProfile) return
|
|
1128
|
+
const ctrl = new AbortController()
|
|
1129
|
+
transferAbort.current = () => ctrl.abort()
|
|
1130
|
+
setTransfer({ name: entry.filename, percent: 0 })
|
|
1131
|
+
try {
|
|
1132
|
+
const params = new URLSearchParams({ profileId: activeProfile.id, filePath: filePathFor(entry) })
|
|
1133
|
+
const res = await fetch('/dsh-remote-workspace/file/download?' + params.toString(), { signal: ctrl.signal })
|
|
1134
|
+
if (!res.ok || !res.body) {
|
|
1135
|
+
setTransfer({ name: entry.filename, error: true })
|
|
1136
|
+
return
|
|
1137
|
+
}
|
|
1138
|
+
const total = Number(res.headers.get('content-length')) || 0
|
|
1139
|
+
const reader = res.body.getReader()
|
|
1140
|
+
const chunks = []
|
|
1141
|
+
let loaded = 0
|
|
1142
|
+
while (true) {
|
|
1143
|
+
const step = await reader.read()
|
|
1144
|
+
if (step.done) break
|
|
1145
|
+
chunks.push(step.value)
|
|
1146
|
+
loaded += step.value.length
|
|
1147
|
+
setTransfer({ name: entry.filename, percent: total ? Math.min(100, Math.round((loaded / total) * 100)) : 0 })
|
|
1148
|
+
}
|
|
1149
|
+
const blob = new Blob(chunks)
|
|
1150
|
+
const link = document.createElement('a')
|
|
1151
|
+
link.href = URL.createObjectURL(blob)
|
|
1152
|
+
link.download = entry.filename
|
|
1153
|
+
link.click()
|
|
1154
|
+
URL.revokeObjectURL(link.href)
|
|
1155
|
+
setTransfer(null)
|
|
1156
|
+
} catch (err) {
|
|
1157
|
+
setTransfer({ name: entry.filename, error: err && err.name === 'AbortError' })
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const uploadFile = () => {
|
|
1162
|
+
if (!activeProfile) return
|
|
1163
|
+
const input = document.createElement('input')
|
|
1164
|
+
input.type = 'file'
|
|
1165
|
+
input.onchange = () => {
|
|
1166
|
+
const file = input.files && input.files[0]
|
|
1167
|
+
if (!file) return
|
|
1168
|
+
const remotePath = currentPath.endsWith('/') ? currentPath + file.name : currentPath + '/' + file.name
|
|
1169
|
+
const xhr = new XMLHttpRequest()
|
|
1170
|
+
const params = new URLSearchParams({ profileId: activeProfile.id, filePath: remotePath })
|
|
1171
|
+
xhr.open('POST', '/dsh-remote-workspace/file/upload?' + params.toString())
|
|
1172
|
+
transferAbort.current = () => xhr.abort()
|
|
1173
|
+
setTransfer({ name: file.name, percent: 0 })
|
|
1174
|
+
xhr.upload.onprogress = (event) => {
|
|
1175
|
+
if (event.lengthComputable) {
|
|
1176
|
+
setTransfer({ name: file.name, percent: Math.min(100, Math.round((event.loaded / event.total) * 100)) })
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
xhr.onload = () => {
|
|
1180
|
+
setTransfer(null)
|
|
1181
|
+
if (xhr.status >= 200 && xhr.status < 300) loadDir(currentPath)
|
|
1182
|
+
else setTransfer({ name: file.name, error: true })
|
|
1183
|
+
}
|
|
1184
|
+
xhr.onerror = () => setTransfer({ name: file.name, error: true })
|
|
1185
|
+
xhr.onabort = () => setTransfer({ name: file.name, error: true })
|
|
1186
|
+
xhr.send(file)
|
|
1187
|
+
}
|
|
1188
|
+
input.click()
|
|
1189
|
+
}
|
|
1190
|
+
|
|
970
1191
|
const handleSaveFile = async () => {
|
|
971
1192
|
if (!previewFile || !activeProfile) return
|
|
972
1193
|
setSaveStatus('saving')
|
|
@@ -1004,8 +1225,15 @@ window.__ModuleLoader__.load({
|
|
|
1004
1225
|
disabled: loading
|
|
1005
1226
|
},
|
|
1006
1227
|
loading ? 'Refreshing...' : '🔄 Refresh'
|
|
1007
|
-
)
|
|
1228
|
+
),
|
|
1229
|
+
React.createElement('button', { type: 'button', className: 'drw-btn', onClick: uploadFile }, t('btnUpload'))
|
|
1008
1230
|
),
|
|
1231
|
+
transfer
|
|
1232
|
+
? React.createElement('div', { className: 'drw-hint', role: 'status' },
|
|
1233
|
+
transfer.error ? t('transferFailed', { name: transfer.name }) : t('transferProgress', { name: transfer.name, percent: transfer.percent || 0 }),
|
|
1234
|
+
React.createElement('button', { type: 'button', className: 'drw-btn', onClick: () => transferAbort.current && transferAbort.current() }, t('transferCancel'))
|
|
1235
|
+
)
|
|
1236
|
+
: null,
|
|
1009
1237
|
React.createElement(
|
|
1010
1238
|
'div',
|
|
1011
1239
|
{ className: 'drw-explorer-nav' },
|
|
@@ -1046,7 +1274,8 @@ window.__ModuleLoader__.load({
|
|
|
1046
1274
|
'div',
|
|
1047
1275
|
{ className: 'drw-file-meta' },
|
|
1048
1276
|
e.size !== undefined ? React.createElement('span', null, `${Math.round(e.size / 1024)} KB`) : null,
|
|
1049
|
-
e.permissions ? React.createElement('span', { style: { opacity: 0.6 } }, e.permissions) : null
|
|
1277
|
+
e.permissions ? React.createElement('span', { style: { opacity: 0.6 } }, e.permissions) : null,
|
|
1278
|
+
e.isDirectory ? null : React.createElement('button', { type: 'button', className: 'drw-btn', onClick: (event) => downloadFile(e, event) }, t('btnDownload'))
|
|
1050
1279
|
)
|
|
1051
1280
|
)
|
|
1052
1281
|
)
|
|
@@ -1135,237 +1364,72 @@ window.__ModuleLoader__.load({
|
|
|
1135
1364
|
)
|
|
1136
1365
|
}
|
|
1137
1366
|
|
|
1138
|
-
function SettingsView(props) {
|
|
1139
|
-
const [activeTab, setActiveTab] = React.useState('profiles')
|
|
1140
|
-
const [healthData, setHealthData] = React.useState(null)
|
|
1141
|
-
const [autoSync, setAutoSync] = React.useState(false)
|
|
1142
|
-
|
|
1143
|
-
const t = props.t || (props.locale === 'zh' ? makeT(zh, en) : makeT(en, zh))
|
|
1144
|
-
const [profiles, setProfiles] = React.useState([])
|
|
1145
|
-
const [activeId, setActiveId] = React.useState(null)
|
|
1146
|
-
const [tunnels, setTunnels] = React.useState([])
|
|
1147
|
-
const [editing, setEditing] = React.useState(null)
|
|
1148
|
-
const [busy, setBusy] = React.useState(null)
|
|
1149
|
-
const [testResult, setTestResult] = React.useState(null)
|
|
1150
|
-
const [syncMsg, setSyncMsg] = React.useState(null)
|
|
1151
|
-
const [tunnelMsg, setTunnelMsg] = React.useState(null)
|
|
1152
|
-
const [showPassword, setShowPassword] = React.useState(false)
|
|
1153
|
-
|
|
1154
|
-
// Tunnel creation inputs
|
|
1155
|
-
const [newTunnelLocal, setNewTunnelLocal] = React.useState('3000')
|
|
1156
|
-
const [newTunnelRemote, setNewTunnelRemote] = React.useState('3000')
|
|
1157
|
-
|
|
1158
|
-
// Remote Directory Browser State
|
|
1159
|
-
const [browserOpen, setBrowserOpen] = React.useState(false)
|
|
1160
|
-
const [browserPath, setBrowserPath] = React.useState('/')
|
|
1161
|
-
const [browserEntries, setBrowserEntries] = React.useState([])
|
|
1162
|
-
const [browserLoading, setBrowserLoading] = React.useState(false)
|
|
1163
|
-
|
|
1164
|
-
const loadState = async () => {
|
|
1165
|
-
try {
|
|
1166
|
-
const res = await fetch('/dsh-remote-workspace/state')
|
|
1167
|
-
if (res.ok) {
|
|
1168
|
-
const data = await res.json()
|
|
1169
|
-
if (data.ok) {
|
|
1170
|
-
setProfiles(data.profiles || [])
|
|
1171
|
-
setActiveId(data.activeId || null)
|
|
1172
|
-
setTunnels(data.tunnels || [])
|
|
1173
|
-
setAutoSync(Boolean(data.autoSync))
|
|
1174
|
-
if (data.activeId) {
|
|
1175
|
-
fetch(`/dsh-remote-workspace/health?profileId=${data.activeId}`).then(r => r.json()).then(h => { if (h.ok) setHealthData(h.health) }).catch(() => {})
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
} catch (err) { /* best-effort cleanup */ }
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
React.useEffect(() => {
|
|
1183
|
-
ensureCss()
|
|
1184
|
-
loadState()
|
|
1185
|
-
}, [])
|
|
1186
|
-
|
|
1187
|
-
const handleSave = async (profile) => {
|
|
1188
|
-
setBusy('saving')
|
|
1189
|
-
try {
|
|
1190
|
-
const res = await fetch('/dsh-remote-workspace/profiles/save', {
|
|
1191
|
-
method: 'POST',
|
|
1192
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1193
|
-
body: JSON.stringify(profile)
|
|
1194
|
-
})
|
|
1195
|
-
if (res.ok) {
|
|
1196
|
-
setEditing(null)
|
|
1197
|
-
setBrowserOpen(false)
|
|
1198
|
-
setTestResult(null)
|
|
1199
|
-
await loadState()
|
|
1200
|
-
}
|
|
1201
|
-
} catch (err) { /* best-effort cleanup */ }
|
|
1202
|
-
setBusy(null)
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
const handleDelete = async (id, name) => {
|
|
1206
|
-
if (!confirm(t('delConfirm', { name: name || id }))) return
|
|
1207
|
-
try {
|
|
1208
|
-
const res = await fetch('/dsh-remote-workspace/profiles/delete', {
|
|
1209
|
-
method: 'POST',
|
|
1210
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1211
|
-
body: JSON.stringify({ id })
|
|
1212
|
-
})
|
|
1213
|
-
if (res.ok) {
|
|
1214
|
-
if (editing && editing.id === id) setEditing(null)
|
|
1215
|
-
await loadState()
|
|
1216
|
-
}
|
|
1217
|
-
} catch (err) { /* best-effort cleanup */ }
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
const handleSetActive = async (id) => {
|
|
1221
|
-
try {
|
|
1222
|
-
const res = await fetch('/dsh-remote-workspace/profiles/active', {
|
|
1223
|
-
method: 'POST',
|
|
1224
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1225
|
-
body: JSON.stringify({ id })
|
|
1226
|
-
})
|
|
1227
|
-
if (res.ok) {
|
|
1228
|
-
await loadState()
|
|
1229
|
-
}
|
|
1230
|
-
} catch (err) { /* best-effort cleanup */ }
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
const handleTest = async (prof) => {
|
|
1234
|
-
setBusy('testing')
|
|
1235
|
-
setTestResult(null)
|
|
1236
|
-
try {
|
|
1237
|
-
const res = await fetch('/dsh-remote-workspace/test', {
|
|
1238
|
-
method: 'POST',
|
|
1239
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1240
|
-
body: JSON.stringify({ profile: prof })
|
|
1241
|
-
})
|
|
1242
|
-
const data = await res.json()
|
|
1243
|
-
setTestResult(data)
|
|
1244
|
-
} catch (err) {
|
|
1245
|
-
setTestResult({ ok: false, error: err.message })
|
|
1246
|
-
}
|
|
1247
|
-
setBusy(null)
|
|
1248
|
-
}
|
|
1249
1367
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1368
|
+
function groupProfiles(list, mode) {
|
|
1369
|
+
const profiles = Array.isArray(list) ? list : []
|
|
1370
|
+
const bucket = (map, key, profile) => {
|
|
1371
|
+
const name = key || 'ungrouped'
|
|
1372
|
+
if (!map.has(name)) map.set(name, [])
|
|
1373
|
+
map.get(name).push(profile)
|
|
1254
1374
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
const res = await fetch('/dsh-remote-workspace/browse', {
|
|
1260
|
-
method: 'POST',
|
|
1261
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1262
|
-
body: JSON.stringify({ profile: editing, remotePath: dirPath })
|
|
1263
|
-
})
|
|
1264
|
-
const data = await res.json()
|
|
1265
|
-
if (data.ok) {
|
|
1266
|
-
setBrowserPath(data.currentPath || dirPath)
|
|
1267
|
-
setBrowserEntries(data.entries || [])
|
|
1268
|
-
}
|
|
1269
|
-
} catch (err) { /* best-effort cleanup */ }
|
|
1270
|
-
setBrowserLoading(false)
|
|
1375
|
+
if (mode === 'environment') {
|
|
1376
|
+
const map = new Map()
|
|
1377
|
+
profiles.forEach((profile) => bucket(map, String(profile.environment || '').trim(), profile))
|
|
1378
|
+
return Array.from(map.entries()).map(([key, items]) => ({ key, items }))
|
|
1271
1379
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
body: JSON.stringify({ direction })
|
|
1281
|
-
})
|
|
1282
|
-
const data = await res.json()
|
|
1283
|
-
if (data.ok) {
|
|
1284
|
-
const count = (direction === 'push' ? data.pushed?.length : data.pulled?.length) || 0
|
|
1285
|
-
if (data.conflicts && data.conflicts.length > 0) {
|
|
1286
|
-
setSyncMsg({ ok: false, text: t('syncConflict', { count: data.conflicts.length }) })
|
|
1287
|
-
} else {
|
|
1288
|
-
setSyncMsg({ ok: true, text: t('syncOk', { count }) })
|
|
1289
|
-
}
|
|
1290
|
-
} else {
|
|
1291
|
-
setSyncMsg({ ok: false, text: t('syncErr', { err: data.error || 'Unknown error' }) })
|
|
1292
|
-
}
|
|
1293
|
-
} catch (err) {
|
|
1294
|
-
setSyncMsg({ ok: false, text: t('syncErr', { err: err.message }) })
|
|
1295
|
-
}
|
|
1296
|
-
setBusy(null)
|
|
1380
|
+
if (mode === 'tag') {
|
|
1381
|
+
const map = new Map()
|
|
1382
|
+
profiles.forEach((profile) => {
|
|
1383
|
+
const tags = Array.isArray(profile.tags) ? profile.tags.map((item) => String(item).trim()).filter(Boolean) : []
|
|
1384
|
+
if (!tags.length) bucket(map, '', profile)
|
|
1385
|
+
else tags.forEach((tag) => bucket(map, tag, profile))
|
|
1386
|
+
})
|
|
1387
|
+
return Array.from(map.entries()).map(([key, items]) => ({ key, items }))
|
|
1297
1388
|
}
|
|
1389
|
+
return [{ key: 'all', items: profiles }]
|
|
1390
|
+
}
|
|
1298
1391
|
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1392
|
+
function ProfilesPanel(panel) {
|
|
1393
|
+
const {
|
|
1394
|
+
t,
|
|
1395
|
+
editing,
|
|
1396
|
+
setEditing,
|
|
1397
|
+
setTestResult,
|
|
1398
|
+
profiles,
|
|
1399
|
+
activeId,
|
|
1400
|
+
busy,
|
|
1401
|
+
handleSave,
|
|
1402
|
+
testResult,
|
|
1403
|
+
handleTest,
|
|
1404
|
+
handleSetActive,
|
|
1405
|
+
handleDelete,
|
|
1406
|
+
browserOpen,
|
|
1407
|
+
setBrowserOpen,
|
|
1408
|
+
browserPath,
|
|
1409
|
+
browserEntries,
|
|
1410
|
+
browserLoading,
|
|
1411
|
+
openDirectoryBrowser,
|
|
1412
|
+
fetchRemoteDir,
|
|
1413
|
+
showPassword,
|
|
1414
|
+
setShowPassword,
|
|
1415
|
+
} = panel
|
|
1416
|
+
const [groupMode, setGroupMode] = React.useState('flat')
|
|
1417
|
+
const [groupReport, setGroupReport] = React.useState([])
|
|
1418
|
+
const sections = groupProfiles(profiles, groupMode)
|
|
1419
|
+
const testGroup = async (items) => {
|
|
1303
1420
|
try {
|
|
1304
|
-
const res = await fetch('/dsh-remote-workspace/
|
|
1421
|
+
const res = await fetch('/dsh-remote-workspace/profiles/test-group', {
|
|
1305
1422
|
method: 'POST',
|
|
1306
1423
|
headers: { 'Content-Type': 'application/json' },
|
|
1307
|
-
body: JSON.stringify({
|
|
1308
|
-
profileId: activeId,
|
|
1309
|
-
localPort: newTunnelLocal,
|
|
1310
|
-
remotePort: newTunnelRemote
|
|
1311
|
-
})
|
|
1424
|
+
body: JSON.stringify({ ids: items.map((item) => item.id) })
|
|
1312
1425
|
})
|
|
1313
1426
|
const data = await res.json()
|
|
1314
|
-
if (data.ok)
|
|
1315
|
-
setTunnelMsg({ ok: true, text: t('tOpenOk', { localPort: newTunnelLocal, remotePort: newTunnelRemote }) })
|
|
1316
|
-
await loadState()
|
|
1317
|
-
} else {
|
|
1318
|
-
setTunnelMsg({ ok: false, text: t('tOpenErr', { err: data.error || 'Failed' }) })
|
|
1319
|
-
}
|
|
1427
|
+
if (data && data.ok) setGroupReport(data.results || [])
|
|
1320
1428
|
} catch (err) {
|
|
1321
|
-
|
|
1429
|
+
setGroupReport([])
|
|
1322
1430
|
}
|
|
1323
|
-
setBusy(null)
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
const handleStopTunnel = async (tunnelId) => {
|
|
1327
|
-
try {
|
|
1328
|
-
const res = await fetch('/dsh-remote-workspace/tunnels/stop', {
|
|
1329
|
-
method: 'POST',
|
|
1330
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1331
|
-
body: JSON.stringify({ tunnelId })
|
|
1332
|
-
})
|
|
1333
|
-
if (res.ok) {
|
|
1334
|
-
await loadState()
|
|
1335
|
-
}
|
|
1336
|
-
} catch (err) { /* best-effort cleanup */ }
|
|
1337
1431
|
}
|
|
1338
|
-
|
|
1339
|
-
const activeProfile = profiles.find((p) => p.id === activeId)
|
|
1340
|
-
|
|
1341
|
-
return React.createElement(
|
|
1342
|
-
'div',
|
|
1343
|
-
{ className: 'drw-page' },
|
|
1344
|
-
|
|
1345
|
-
// Header with Live Badges (ClineBot Style)
|
|
1346
|
-
React.createElement(
|
|
1347
|
-
'div',
|
|
1348
|
-
{ className: 'drw-header' },
|
|
1349
|
-
React.createElement(
|
|
1350
|
-
'div',
|
|
1351
|
-
{ className: 'drw-header-top' },
|
|
1352
|
-
React.createElement('div', { className: 'drw-page-title' }, '🌐 ' + t('title')),
|
|
1353
|
-
React.createElement(
|
|
1354
|
-
'div',
|
|
1355
|
-
{ className: 'drw-header-badges' },
|
|
1356
|
-
activeProfile
|
|
1357
|
-
? React.createElement('span', { className: 'drw-badge drw-badge-ok' }, t('badgeOnline', { name: activeProfile.name || activeProfile.host, latency: testResult?.latencyMs ?? 15 }))
|
|
1358
|
-
: React.createElement('span', { className: 'drw-badge drw-badge-warn' }, t('badgeOffline')),
|
|
1359
|
-
activeProfile
|
|
1360
|
-
? React.createElement('span', { className: 'drw-badge' }, activeProfile.authType === 'password' ? t('badgePassword') : t('badgeKey'))
|
|
1361
|
-
: null,
|
|
1362
|
-
React.createElement('span', { className: `drw-badge ${tunnels.length > 0 ? 'drw-badge-ok' : ''}` }, t('badgeTunnels', { count: tunnels.length }))
|
|
1363
|
-
)
|
|
1364
|
-
),
|
|
1365
|
-
React.createElement('div', { className: 'drw-page-sub' }, t('subtitle'))
|
|
1366
|
-
),
|
|
1367
|
-
|
|
1368
|
-
// Section 1: SSH Profiles Table & Management
|
|
1432
|
+
return (
|
|
1369
1433
|
React.createElement(
|
|
1370
1434
|
'div',
|
|
1371
1435
|
{ className: 'drw-card' },
|
|
@@ -1391,7 +1455,11 @@ window.__ModuleLoader__.load({
|
|
|
1391
1455
|
passphrase: '',
|
|
1392
1456
|
password: '',
|
|
1393
1457
|
remoteWorkspace: '',
|
|
1394
|
-
localMirrorPath: ''
|
|
1458
|
+
localMirrorPath: '',
|
|
1459
|
+
environment: '',
|
|
1460
|
+
tags: [],
|
|
1461
|
+
location: '',
|
|
1462
|
+
description: ''
|
|
1395
1463
|
})
|
|
1396
1464
|
setTestResult(null)
|
|
1397
1465
|
}
|
|
@@ -1469,6 +1537,62 @@ window.__ModuleLoader__.load({
|
|
|
1469
1537
|
)
|
|
1470
1538
|
),
|
|
1471
1539
|
|
|
1540
|
+
React.createElement(
|
|
1541
|
+
'div',
|
|
1542
|
+
{ className: 'drw-field' },
|
|
1543
|
+
React.createElement('span', { className: 'drw-label' }, t('fJumpHosts')),
|
|
1544
|
+
React.createElement('input', {
|
|
1545
|
+
className: 'drw-input',
|
|
1546
|
+
placeholder: 'bastion-a, bastion-b',
|
|
1547
|
+
value: editing.jumpHostId || ((editing.jumpHosts || []).join(', ')),
|
|
1548
|
+
onChange: (e) => {
|
|
1549
|
+
const raw = e.target.value;
|
|
1550
|
+
const jumpHosts = raw.split(',').map((item) => item.trim()).filter(Boolean);
|
|
1551
|
+
setEditing({ ...editing, jumpHostId: raw, jumpHosts });
|
|
1552
|
+
}
|
|
1553
|
+
}),
|
|
1554
|
+
React.createElement('span', { className: 'drw-hint' }, t('fJumpHostsHint'))
|
|
1555
|
+
),
|
|
1556
|
+
|
|
1557
|
+
React.createElement(
|
|
1558
|
+
'div',
|
|
1559
|
+
{ className: 'drw-field' },
|
|
1560
|
+
React.createElement('span', { className: 'drw-label' }, t('fProxyCommand')),
|
|
1561
|
+
React.createElement('input', {
|
|
1562
|
+
className: 'drw-input',
|
|
1563
|
+
placeholder: 'cloudflared access ssh --hostname %h',
|
|
1564
|
+
value: editing.proxyCommand || '',
|
|
1565
|
+
onChange: (e) => setEditing({ ...editing, proxyCommand: e.target.value })
|
|
1566
|
+
}),
|
|
1567
|
+
React.createElement('span', { className: 'drw-hint' }, t('fProxyCommandHint'))
|
|
1568
|
+
),
|
|
1569
|
+
|
|
1570
|
+
|
|
1571
|
+
React.createElement(
|
|
1572
|
+
'div',
|
|
1573
|
+
{ className: 'drw-grid-2' },
|
|
1574
|
+
React.createElement('div', { className: 'drw-field' },
|
|
1575
|
+
React.createElement('span', { className: 'drw-label' }, t('fEnvironment')),
|
|
1576
|
+
React.createElement('input', { className: 'drw-input', value: editing.environment || '', onChange: (e) => setEditing({ ...editing, environment: e.target.value }) })
|
|
1577
|
+
),
|
|
1578
|
+
React.createElement('div', { className: 'drw-field' },
|
|
1579
|
+
React.createElement('span', { className: 'drw-label' }, t('fLocation')),
|
|
1580
|
+
React.createElement('input', { className: 'drw-input', value: editing.location || '', onChange: (e) => setEditing({ ...editing, location: e.target.value }) })
|
|
1581
|
+
),
|
|
1582
|
+
React.createElement('div', { className: 'drw-field' },
|
|
1583
|
+
React.createElement('span', { className: 'drw-label' }, t('fTags')),
|
|
1584
|
+
React.createElement('input', {
|
|
1585
|
+
className: 'drw-input',
|
|
1586
|
+
value: Array.isArray(editing.tags) ? editing.tags.join(', ') : (editing.tags || ''),
|
|
1587
|
+
onChange: (e) => setEditing({ ...editing, tags: e.target.value.split(',').map((item) => item.trim()).filter(Boolean) })
|
|
1588
|
+
})
|
|
1589
|
+
),
|
|
1590
|
+
React.createElement('div', { className: 'drw-field' },
|
|
1591
|
+
React.createElement('span', { className: 'drw-label' }, t('fDescription')),
|
|
1592
|
+
React.createElement('input', { className: 'drw-input', value: editing.description || '', onChange: (e) => setEditing({ ...editing, description: e.target.value }) })
|
|
1593
|
+
)
|
|
1594
|
+
),
|
|
1595
|
+
|
|
1472
1596
|
// Auth Method Segmented Switcher (Key vs Password)
|
|
1473
1597
|
React.createElement(
|
|
1474
1598
|
'div',
|
|
@@ -1481,11 +1605,20 @@ window.__ModuleLoader__.load({
|
|
|
1481
1605
|
'button',
|
|
1482
1606
|
{
|
|
1483
1607
|
type: 'button',
|
|
1484
|
-
className: `drw-segmented-item ${editing.authType !== 'password' ? 'drw-segmented-item-active' : ''}`,
|
|
1608
|
+
className: `drw-segmented-item ${editing.authType !== 'password' && editing.authType !== 'agent' ? 'drw-segmented-item-active' : ''}`,
|
|
1485
1609
|
onClick: () => setEditing({ ...editing, authType: 'key' })
|
|
1486
1610
|
},
|
|
1487
1611
|
'🔑 ' + t('fAuthKey')
|
|
1488
1612
|
),
|
|
1613
|
+
React.createElement(
|
|
1614
|
+
'button',
|
|
1615
|
+
{
|
|
1616
|
+
type: 'button',
|
|
1617
|
+
className: `drw-segmented-item ${editing.authType === 'agent' ? 'drw-segmented-item-active' : ''}`,
|
|
1618
|
+
onClick: () => setEditing({ ...editing, authType: 'agent' })
|
|
1619
|
+
},
|
|
1620
|
+
t('fAuthAgent')
|
|
1621
|
+
),
|
|
1489
1622
|
React.createElement(
|
|
1490
1623
|
'button',
|
|
1491
1624
|
{
|
|
@@ -1499,7 +1632,19 @@ window.__ModuleLoader__.load({
|
|
|
1499
1632
|
),
|
|
1500
1633
|
|
|
1501
1634
|
// Conditional Auth Inputs
|
|
1502
|
-
editing.authType === '
|
|
1635
|
+
editing.authType === 'agent'
|
|
1636
|
+
? React.createElement(
|
|
1637
|
+
'div',
|
|
1638
|
+
{ className: 'drw-field' },
|
|
1639
|
+
React.createElement('span', { className: 'drw-label' }, t('fAgentPath')),
|
|
1640
|
+
React.createElement('input', {
|
|
1641
|
+
className: 'drw-input',
|
|
1642
|
+
placeholder: t('fAgentPathHint'),
|
|
1643
|
+
value: editing.agentPath || '',
|
|
1644
|
+
onChange: (e) => setEditing({ ...editing, agentPath: e.target.value })
|
|
1645
|
+
})
|
|
1646
|
+
)
|
|
1647
|
+
: editing.authType === 'password'
|
|
1503
1648
|
? React.createElement(
|
|
1504
1649
|
'div',
|
|
1505
1650
|
{ className: 'drw-field' },
|
|
@@ -1734,6 +1879,23 @@ window.__ModuleLoader__.load({
|
|
|
1734
1879
|
)
|
|
1735
1880
|
: null,
|
|
1736
1881
|
|
|
1882
|
+
|
|
1883
|
+
React.createElement('div', { style: { display: 'flex', gap: '8px', marginBottom: '8px' } },
|
|
1884
|
+
['flat', 'environment', 'tag'].map((mode) => React.createElement('button', {
|
|
1885
|
+
key: mode,
|
|
1886
|
+
type: 'button',
|
|
1887
|
+
className: groupMode === mode ? 'drw-btn drw-btn-primary' : 'drw-btn',
|
|
1888
|
+
onClick: () => setGroupMode(mode)
|
|
1889
|
+
}, t(mode === 'flat' ? 'groupFlat' : mode === 'environment' ? 'groupEnvironment' : 'groupTag')))
|
|
1890
|
+
),
|
|
1891
|
+
groupReport.length
|
|
1892
|
+
? React.createElement('div', { className: 'drw-hint', role: 'status' },
|
|
1893
|
+
groupReport.map((item) => React.createElement('div', { key: item.id + (item.name || '') },
|
|
1894
|
+
(item.name || item.id) + ': ' + (item.ok ? t('groupOnline', { latency: item.latencyMs || 0 }) : t('groupOffline', { err: item.error || '' }))
|
|
1895
|
+
))
|
|
1896
|
+
)
|
|
1897
|
+
: null,
|
|
1898
|
+
|
|
1737
1899
|
// Profiles List Table
|
|
1738
1900
|
profiles.length > 0
|
|
1739
1901
|
? React.createElement(
|
|
@@ -1755,7 +1917,17 @@ window.__ModuleLoader__.load({
|
|
|
1755
1917
|
React.createElement(
|
|
1756
1918
|
'tbody',
|
|
1757
1919
|
null,
|
|
1758
|
-
|
|
1920
|
+
sections.flatMap((section) => {
|
|
1921
|
+
const header = groupMode === 'flat' ? [] : [
|
|
1922
|
+
React.createElement('tr', { key: 'group-' + section.key },
|
|
1923
|
+
React.createElement('td', { colSpan: 5 },
|
|
1924
|
+
React.createElement('strong', null, section.key === 'ungrouped' ? t('groupUngrouped') : section.key),
|
|
1925
|
+
' ',
|
|
1926
|
+
React.createElement('button', { type: 'button', className: 'drw-btn', onClick: () => testGroup(section.items) }, t('testGroup'))
|
|
1927
|
+
)
|
|
1928
|
+
)
|
|
1929
|
+
]
|
|
1930
|
+
return header.concat(section.items.map((p) => {
|
|
1759
1931
|
const isAct = p.id === activeId
|
|
1760
1932
|
return React.createElement(
|
|
1761
1933
|
'tr',
|
|
@@ -1767,7 +1939,7 @@ window.__ModuleLoader__.load({
|
|
|
1767
1939
|
React.createElement(
|
|
1768
1940
|
'span',
|
|
1769
1941
|
{ className: 'drw-badge', style: { marginLeft: '8px' } },
|
|
1770
|
-
p.authType === 'password' ? '🔒
|
|
1942
|
+
p.authType === 'password' ? '🔒 ' + t('fAuthPassShort') : p.authType === 'agent' ? t('fAuthAgentShort') : '🔑 ' + t('fAuthKeyShort')
|
|
1771
1943
|
),
|
|
1772
1944
|
isAct
|
|
1773
1945
|
? React.createElement('span', { className: 'drw-badge drw-badge-ok', style: { marginLeft: '6px' } }, t('activeBadge'))
|
|
@@ -1820,68 +1992,30 @@ window.__ModuleLoader__.load({
|
|
|
1820
1992
|
)
|
|
1821
1993
|
)
|
|
1822
1994
|
)
|
|
1995
|
+
}))
|
|
1823
1996
|
})
|
|
1824
1997
|
)
|
|
1825
1998
|
)
|
|
1826
1999
|
: !editing
|
|
1827
2000
|
? React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('noHosts'))
|
|
1828
2001
|
: null
|
|
1829
|
-
)
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
activeProfile
|
|
1833
|
-
? React.createElement(
|
|
1834
|
-
'div',
|
|
1835
|
-
{ className: 'drw-card' },
|
|
1836
|
-
React.createElement(
|
|
1837
|
-
'div',
|
|
1838
|
-
{ className: 'drw-card-title' },
|
|
1839
|
-
React.createElement('span', null, t('diagTitle')),
|
|
1840
|
-
React.createElement(
|
|
1841
|
-
'button',
|
|
1842
|
-
{
|
|
1843
|
-
type: 'button',
|
|
1844
|
-
className: 'drw-btn',
|
|
1845
|
-
style: { fontSize: '11px', padding: '3px 9px' },
|
|
1846
|
-
disabled: busy === 'testing',
|
|
1847
|
-
onClick: () => handleTest(activeProfile)
|
|
1848
|
-
},
|
|
1849
|
-
busy === 'testing' ? t('testing') : t('diagProbeBtn')
|
|
1850
|
-
)
|
|
1851
|
-
),
|
|
1852
|
-
React.createElement('div', { className: 'drw-card-desc' }, t('diagDesc')),
|
|
1853
|
-
React.createElement(
|
|
1854
|
-
'div',
|
|
1855
|
-
{ className: 'drw-grid-4' },
|
|
1856
|
-
React.createElement(
|
|
1857
|
-
'div',
|
|
1858
|
-
{ className: 'drw-stat-box' },
|
|
1859
|
-
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.name || activeProfile.host),
|
|
1860
|
-
React.createElement('div', { className: 'drw-stat-lbl' }, `SSH: ${activeProfile.username}@${activeProfile.host}:${activeProfile.port || 22}`)
|
|
1861
|
-
),
|
|
1862
|
-
React.createElement(
|
|
1863
|
-
'div',
|
|
1864
|
-
{ className: 'drw-stat-box' },
|
|
1865
|
-
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.remoteWorkspace || '—'),
|
|
1866
|
-
React.createElement('div', { className: 'drw-stat-lbl' }, 'Remote Workspace')
|
|
1867
|
-
),
|
|
1868
|
-
React.createElement(
|
|
1869
|
-
'div',
|
|
1870
|
-
{ className: 'drw-stat-box' },
|
|
1871
|
-
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.localMirrorPath || '—'),
|
|
1872
|
-
React.createElement('div', { className: 'drw-stat-lbl' }, 'Local Mirror Path')
|
|
1873
|
-
),
|
|
1874
|
-
React.createElement(
|
|
1875
|
-
'div',
|
|
1876
|
-
{ className: 'drw-stat-box' },
|
|
1877
|
-
React.createElement('div', { className: 'drw-stat-val', style: { color: 'var(--dsw-alias-state-success-primary)' } }, 'Ready ✓'),
|
|
1878
|
-
React.createElement('div', { className: 'drw-stat-lbl' }, activeProfile.authType === 'password' ? 'Password Auth' : 'SSH Key Auth')
|
|
1879
|
-
)
|
|
1880
|
-
)
|
|
1881
|
-
)
|
|
1882
|
-
: null,
|
|
2002
|
+
)
|
|
2003
|
+
)
|
|
2004
|
+
}
|
|
1883
2005
|
|
|
1884
|
-
|
|
2006
|
+
function SyncPanel(panel) {
|
|
2007
|
+
const {
|
|
2008
|
+
t,
|
|
2009
|
+
busy,
|
|
2010
|
+
activeProfile,
|
|
2011
|
+
handleSync,
|
|
2012
|
+
syncMsg,
|
|
2013
|
+
autoSync,
|
|
2014
|
+
setAutoSync,
|
|
2015
|
+
reportAction,
|
|
2016
|
+
setActionError,
|
|
2017
|
+
} = panel
|
|
2018
|
+
return (
|
|
1885
2019
|
activeProfile
|
|
1886
2020
|
? React.createElement(
|
|
1887
2021
|
'div',
|
|
@@ -1912,9 +2046,11 @@ window.__ModuleLoader__.load({
|
|
|
1912
2046
|
headers: { 'Content-Type': 'application/json' },
|
|
1913
2047
|
body: JSON.stringify({ enabled: next })
|
|
1914
2048
|
})
|
|
1915
|
-
const
|
|
1916
|
-
if (
|
|
1917
|
-
} catch (err) {
|
|
2049
|
+
const data = await reportAction(res)
|
|
2050
|
+
if (data && data.ok) setAutoSync(data.autoSync)
|
|
2051
|
+
} catch (err) {
|
|
2052
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
2053
|
+
}
|
|
1918
2054
|
}
|
|
1919
2055
|
},
|
|
1920
2056
|
autoSync ? '⚡ Active (ON)' : '⚪ Off'
|
|
@@ -1958,9 +2094,25 @@ window.__ModuleLoader__.load({
|
|
|
1958
2094
|
)
|
|
1959
2095
|
: null
|
|
1960
2096
|
)
|
|
1961
|
-
: null
|
|
2097
|
+
: null
|
|
2098
|
+
)
|
|
2099
|
+
}
|
|
1962
2100
|
|
|
1963
|
-
|
|
2101
|
+
function TunnelsPanel(panel) {
|
|
2102
|
+
const {
|
|
2103
|
+
t,
|
|
2104
|
+
busy,
|
|
2105
|
+
activeProfile,
|
|
2106
|
+
newTunnelLocal,
|
|
2107
|
+
setNewTunnelLocal,
|
|
2108
|
+
newTunnelRemote,
|
|
2109
|
+
setNewTunnelRemote,
|
|
2110
|
+
handleStartTunnel,
|
|
2111
|
+
tunnelMsg,
|
|
2112
|
+
tunnels,
|
|
2113
|
+
handleStopTunnel,
|
|
2114
|
+
} = panel
|
|
2115
|
+
return (
|
|
1964
2116
|
React.createElement(
|
|
1965
2117
|
'div',
|
|
1966
2118
|
{ className: 'drw-card' },
|
|
@@ -2077,143 +2229,692 @@ window.__ModuleLoader__.load({
|
|
|
2077
2229
|
)
|
|
2078
2230
|
}
|
|
2079
2231
|
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
const
|
|
2084
|
-
const [
|
|
2085
|
-
const [
|
|
2086
|
-
const [
|
|
2087
|
-
const [
|
|
2088
|
-
|
|
2089
|
-
const
|
|
2090
|
-
|
|
2091
|
-
setMsg(null);
|
|
2232
|
+
function ClusterPanel(props) {
|
|
2233
|
+
const t = props.t
|
|
2234
|
+
const profiles = props.profiles || []
|
|
2235
|
+
const [command, setCommand] = React.useState('')
|
|
2236
|
+
const [environment, setEnvironment] = React.useState('')
|
|
2237
|
+
const [tag, setTag] = React.useState('')
|
|
2238
|
+
const [rows, setRows] = React.useState([])
|
|
2239
|
+
const [open, setOpen] = React.useState('')
|
|
2240
|
+
const [running, setRunning] = React.useState(false)
|
|
2241
|
+
const run = async () => {
|
|
2242
|
+
setRunning(true)
|
|
2092
2243
|
try {
|
|
2093
|
-
const res = await fetch('/dsh-remote-workspace/
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2244
|
+
const res = await fetch('/dsh-remote-workspace/cluster', {
|
|
2245
|
+
method: 'POST',
|
|
2246
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2247
|
+
body: JSON.stringify({
|
|
2248
|
+
command,
|
|
2249
|
+
environment,
|
|
2250
|
+
tags: tag ? [tag] : [],
|
|
2251
|
+
maxWorkers: 8
|
|
2252
|
+
})
|
|
2253
|
+
})
|
|
2254
|
+
const data = await res.json()
|
|
2255
|
+
setRows(data && data.results ? data.results : [])
|
|
2256
|
+
} catch (err) {
|
|
2257
|
+
setRows([])
|
|
2104
2258
|
}
|
|
2105
|
-
|
|
2259
|
+
setRunning(false)
|
|
2260
|
+
}
|
|
2261
|
+
return React.createElement('div', { className: 'drw-card' },
|
|
2262
|
+
React.createElement('div', { className: 'drw-card-title' }, t('clusterTitle')),
|
|
2263
|
+
React.createElement('div', { className: 'drw-grid-2' },
|
|
2264
|
+
React.createElement('input', { className: 'drw-input', placeholder: t('fEnvironment'), value: environment, onChange: (e) => setEnvironment(e.target.value) }),
|
|
2265
|
+
React.createElement('input', { className: 'drw-input', placeholder: t('fTags'), value: tag, onChange: (e) => setTag(e.target.value) })
|
|
2266
|
+
),
|
|
2267
|
+
React.createElement('div', { style: { display: 'flex', gap: '8px', marginTop: '8px' } },
|
|
2268
|
+
React.createElement('input', { className: 'drw-input', placeholder: t('clusterCommand'), value: command, onChange: (e) => setCommand(e.target.value) }),
|
|
2269
|
+
React.createElement('button', { type: 'button', className: 'drw-btn drw-btn-primary', disabled: running || !command.trim(), onClick: run }, running ? t('clusterRunning') : t('clusterRun'))
|
|
2270
|
+
),
|
|
2271
|
+
React.createElement('div', { className: 'drw-hint' }, t('clusterCount', { count: profiles.length })),
|
|
2272
|
+
rows.map((row, index) => React.createElement('div', { key: (row.alias || '') + index, className: 'drw-field' },
|
|
2273
|
+
React.createElement('button', { type: 'button', className: 'drw-btn', onClick: () => setOpen(open === String(index) ? '' : String(index)) },
|
|
2274
|
+
(row.alias || row.host) + ' ' + (row.success ? t('clusterOk') : t('clusterFail')) + ' ' + (row.durationMs || 0) + ' ms'
|
|
2275
|
+
),
|
|
2276
|
+
open === String(index)
|
|
2277
|
+
? React.createElement('pre', { className: 'drw-term-body' }, row.error || row.stdout || row.stderr || '')
|
|
2278
|
+
: null
|
|
2279
|
+
))
|
|
2280
|
+
)
|
|
2281
|
+
}
|
|
2106
2282
|
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2283
|
+
function SettingsView(props) {
|
|
2284
|
+
const [activeTab, setActiveTab] = React.useState('profiles')
|
|
2285
|
+
const [healthData, setHealthData] = React.useState(null)
|
|
2286
|
+
const [autoSync, setAutoSync] = React.useState(false)
|
|
2287
|
+
|
|
2288
|
+
const t = props.t || (props.locale === 'zh' ? makeT(zh, en) : makeT(en, zh))
|
|
2289
|
+
const [profiles, setProfiles] = React.useState([])
|
|
2290
|
+
const [activeId, setActiveId] = React.useState(null)
|
|
2291
|
+
const [tunnels, setTunnels] = React.useState([])
|
|
2292
|
+
const [editing, setEditing] = React.useState(null)
|
|
2293
|
+
const [busy, setBusy] = React.useState(null)
|
|
2294
|
+
const [testResult, setTestResult] = React.useState(null)
|
|
2295
|
+
const [actionError, setActionError] = React.useState('')
|
|
2296
|
+
const [authPrompts, setAuthPrompts] = React.useState([])
|
|
2297
|
+
const [authAnswers, setAuthAnswers] = React.useState({})
|
|
2298
|
+
const [importReport, setImportReport] = React.useState(null)
|
|
2299
|
+
const [syncMsg, setSyncMsg] = React.useState(null)
|
|
2300
|
+
const [tunnelMsg, setTunnelMsg] = React.useState(null)
|
|
2301
|
+
const [showPassword, setShowPassword] = React.useState(false)
|
|
2302
|
+
const [terminalFont, setTerminalFont] = React.useState('')
|
|
2303
|
+
|
|
2304
|
+
// Tunnel creation inputs
|
|
2305
|
+
const [newTunnelLocal, setNewTunnelLocal] = React.useState('3000')
|
|
2306
|
+
const [newTunnelRemote, setNewTunnelRemote] = React.useState('3000')
|
|
2307
|
+
|
|
2308
|
+
// Remote Directory Browser State
|
|
2309
|
+
const [browserOpen, setBrowserOpen] = React.useState(false)
|
|
2310
|
+
const [browserPath, setBrowserPath] = React.useState('/')
|
|
2311
|
+
const [browserEntries, setBrowserEntries] = React.useState([])
|
|
2312
|
+
const [browserLoading, setBrowserLoading] = React.useState(false)
|
|
2313
|
+
|
|
2314
|
+
|
|
2315
|
+
function actionErrorText(status, data) {
|
|
2316
|
+
if (status >= 200 && status < 300 && data && data.ok !== false) return ''
|
|
2317
|
+
if (data && typeof data.error === 'string' && data.error) return data.error
|
|
2318
|
+
return 'HTTP ' + status
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
const reportAction = async (res) => {
|
|
2322
|
+
let data = null
|
|
2323
|
+
try { data = await res.json() } catch (err) { data = null }
|
|
2324
|
+
const text = actionErrorText(res.status, data)
|
|
2325
|
+
setActionError(text ? ((data && data.error) ? text : t('actionFailed')) : '')
|
|
2326
|
+
return text ? null : data
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
const loadInFlight = React.useRef(false)
|
|
2330
|
+
|
|
2331
|
+
const loadState = async (force) => {
|
|
2332
|
+
const hidden = typeof document !== 'undefined' && document.hidden
|
|
2333
|
+
if (!force && (hidden || loadInFlight.current)) return
|
|
2334
|
+
loadInFlight.current = true
|
|
2110
2335
|
try {
|
|
2111
|
-
const res = await fetch('/dsh-remote-workspace/
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2336
|
+
const res = await fetch('/dsh-remote-workspace/state')
|
|
2337
|
+
const data = await reportAction(res)
|
|
2338
|
+
if (data && data.ok) {
|
|
2339
|
+
setProfiles(data.profiles || [])
|
|
2340
|
+
setActiveId(data.activeId || null)
|
|
2341
|
+
setTunnels(data.tunnels || [])
|
|
2342
|
+
setAutoSync(Boolean(data.autoSync))
|
|
2343
|
+
setAuthPrompts(data.authPrompts || [])
|
|
2344
|
+
setTerminalFont(data.terminalFontFamily || '')
|
|
2345
|
+
applyTerminalFont(data.terminalFontFamily || '')
|
|
2346
|
+
if (data.activeId) {
|
|
2347
|
+
fetch(`/dsh-remote-workspace/health?profileId=${data.activeId}`).then(r => r.json()).then(h => { if (h.ok) setHealthData(h.health) }).catch(() => {})
|
|
2116
2348
|
}
|
|
2117
|
-
});
|
|
2118
|
-
const data = await res.json();
|
|
2119
|
-
if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status));
|
|
2120
|
-
setUpdateStatus(data);
|
|
2121
|
-
if (data.restartRequired) {
|
|
2122
|
-
setMsg({ ok: true, text: t('restartRequired') });
|
|
2123
2349
|
}
|
|
2124
|
-
} catch (
|
|
2125
|
-
|
|
2350
|
+
} catch (err) {
|
|
2351
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
2126
2352
|
} finally {
|
|
2127
|
-
|
|
2353
|
+
loadInFlight.current = false
|
|
2128
2354
|
}
|
|
2129
|
-
}
|
|
2130
|
-
|
|
2131
|
-
React.useEffect(() => {
|
|
2132
|
-
checkUpdate();
|
|
2133
|
-
}, [checkUpdate]);
|
|
2134
|
-
|
|
2135
|
-
return React.createElement(
|
|
2136
|
-
'div',
|
|
2137
|
-
{
|
|
2138
|
-
style: {
|
|
2139
|
-
display: 'flex',
|
|
2140
|
-
alignItems: 'center',
|
|
2141
|
-
justifyContent: 'space-between',
|
|
2142
|
-
padding: '10px 14px',
|
|
2143
|
-
marginBottom: '14px',
|
|
2144
|
-
borderRadius: '8px',
|
|
2145
|
-
background: 'var(--dsw-alias-bg-layer-2)',
|
|
2146
|
-
border: '1px solid var(--dsw-alias-border-l2)',
|
|
2147
|
-
fontSize: '13px'
|
|
2148
|
-
}
|
|
2149
|
-
},
|
|
2150
|
-
React.createElement(
|
|
2151
|
-
'div',
|
|
2152
|
-
{ style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
2153
|
-
React.createElement('span', null, '📦'),
|
|
2154
|
-
React.createElement(
|
|
2155
|
-
'div',
|
|
2156
|
-
null,
|
|
2157
|
-
React.createElement('span', { style: { fontWeight: 500 } }, 'v' + (updateStatus?.currentVersion || '0.3.1')),
|
|
2158
|
-
updateStatus?.latestVersion && updateStatus.latestVersion !== updateStatus.currentVersion
|
|
2159
|
-
? React.createElement('span', { style: { marginLeft: '8px', color: 'var(--dsw-alias-state-warning-text)' } }, '→ v' + updateStatus.latestVersion)
|
|
2160
|
-
: null,
|
|
2161
|
-
msg
|
|
2162
|
-
? React.createElement('div', { style: { fontSize: '11px', marginTop: '3px', color: msg.ok ? 'var(--dsw-alias-state-success-text)' : 'var(--dsw-alias-state-error-text)' } }, msg.text)
|
|
2163
|
-
: null
|
|
2164
|
-
)
|
|
2165
|
-
),
|
|
2166
|
-
React.createElement(
|
|
2167
|
-
'div',
|
|
2168
|
-
{ style: { display: 'flex', gap: '8px' } },
|
|
2169
|
-
updateStatus?.updateAvailable
|
|
2170
|
-
? React.createElement(
|
|
2171
|
-
'button',
|
|
2172
|
-
{
|
|
2173
|
-
type: 'button',
|
|
2174
|
-
className: 'drw-btn drw-btn-primary',
|
|
2175
|
-
disabled: updating,
|
|
2176
|
-
onClick: doUpdate,
|
|
2177
|
-
style: { fontSize: '12px', padding: '4px 10px' }
|
|
2178
|
-
},
|
|
2179
|
-
updating ? t('updating') : t('updateNow')
|
|
2180
|
-
)
|
|
2181
|
-
: React.createElement(
|
|
2182
|
-
'button',
|
|
2183
|
-
{
|
|
2184
|
-
type: 'button',
|
|
2185
|
-
className: 'drw-btn',
|
|
2186
|
-
disabled: loading,
|
|
2187
|
-
onClick: checkUpdate,
|
|
2188
|
-
style: { fontSize: '12px', padding: '4px 10px' }
|
|
2189
|
-
},
|
|
2190
|
-
loading ? t('checking') : (updateStatus?.updateAvailable === false ? t('upToDate') : t('checkUpdate'))
|
|
2191
|
-
)
|
|
2192
|
-
)
|
|
2193
|
-
);
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
function PluginCard(props) {
|
|
2197
|
-
const page = !!(props && props.view === 'page')
|
|
2198
|
-
const [open, setOpen] = React.useState(!!page)
|
|
2199
|
-
const t = props.t || (props.locale === 'zh' ? makeT(zh, en) : makeT(en, zh))
|
|
2355
|
+
}
|
|
2200
2356
|
|
|
2201
2357
|
React.useEffect(() => {
|
|
2202
2358
|
ensureCss()
|
|
2359
|
+
loadState(true)
|
|
2360
|
+
const timer = setInterval(() => loadState(false), 2000)
|
|
2361
|
+
const onVisible = () => {
|
|
2362
|
+
if (typeof document !== 'undefined' && !document.hidden) loadState(true)
|
|
2363
|
+
}
|
|
2364
|
+
if (typeof document !== 'undefined') document.addEventListener('visibilitychange', onVisible)
|
|
2365
|
+
return () => {
|
|
2366
|
+
clearInterval(timer)
|
|
2367
|
+
if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', onVisible)
|
|
2368
|
+
}
|
|
2203
2369
|
}, [])
|
|
2204
2370
|
|
|
2205
|
-
// Row seat (plugins.row.config): the host page draws title/icon/crumb and the
|
|
2206
|
-
// padding, so the summary is a one-liner and the page drops our card chrome.
|
|
2207
|
-
if (props && props.view === 'summary') {
|
|
2208
|
-
return React.createElement('span', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('subtitle'))
|
|
2209
|
-
}
|
|
2210
2371
|
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2372
|
+
const saveTerminalFont = async () => {
|
|
2373
|
+
try {
|
|
2374
|
+
const res = await fetch('/dsh-remote-workspace/terminal/font', {
|
|
2375
|
+
method: 'POST',
|
|
2376
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2377
|
+
body: JSON.stringify({ fontFamily: terminalFont })
|
|
2378
|
+
})
|
|
2379
|
+
const data = await reportAction(res)
|
|
2380
|
+
if (data && data.ok) {
|
|
2381
|
+
setTerminalFont(data.terminalFontFamily || '')
|
|
2382
|
+
applyTerminalFont(data.terminalFontFamily || '')
|
|
2383
|
+
}
|
|
2384
|
+
} catch (err) {
|
|
2385
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
const submitAuth = async (prompt) => {
|
|
2390
|
+
const answers = (prompt.prompts || []).map((_, index) => authAnswers[prompt.profileId + ':' + index] || '')
|
|
2391
|
+
try {
|
|
2392
|
+
const res = await fetch('/dsh-remote-workspace/auth/keyboard', {
|
|
2393
|
+
method: 'POST',
|
|
2394
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2395
|
+
body: JSON.stringify({ id: prompt.profileId, answers })
|
|
2396
|
+
})
|
|
2397
|
+
const data = await reportAction(res)
|
|
2398
|
+
if (data) await loadState()
|
|
2399
|
+
} catch (err) {
|
|
2400
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
const handleSave = async (profile) => {
|
|
2405
|
+
setBusy('saving')
|
|
2406
|
+
try {
|
|
2407
|
+
const res = await fetch('/dsh-remote-workspace/profiles/save', {
|
|
2408
|
+
method: 'POST',
|
|
2409
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2410
|
+
body: JSON.stringify(profile)
|
|
2411
|
+
})
|
|
2412
|
+
const data = await reportAction(res)
|
|
2413
|
+
if (data) {
|
|
2414
|
+
setEditing(null)
|
|
2415
|
+
setBrowserOpen(false)
|
|
2416
|
+
setTestResult(null)
|
|
2417
|
+
await loadState()
|
|
2418
|
+
}
|
|
2419
|
+
} catch (err) {
|
|
2420
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
2421
|
+
}
|
|
2422
|
+
setBusy(null)
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
const handleDelete = async (id, name) => {
|
|
2426
|
+
if (!confirm(t('delConfirm', { name: name || id }))) return
|
|
2427
|
+
try {
|
|
2428
|
+
const res = await fetch('/dsh-remote-workspace/profiles/delete', {
|
|
2429
|
+
method: 'POST',
|
|
2430
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2431
|
+
body: JSON.stringify({ id })
|
|
2432
|
+
})
|
|
2433
|
+
const data = await reportAction(res)
|
|
2434
|
+
if (data) {
|
|
2435
|
+
if (editing && editing.id === id) setEditing(null)
|
|
2436
|
+
await loadState()
|
|
2437
|
+
}
|
|
2438
|
+
} catch (err) {
|
|
2439
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
const handleSetActive = async (id) => {
|
|
2444
|
+
try {
|
|
2445
|
+
const res = await fetch('/dsh-remote-workspace/profiles/active', {
|
|
2446
|
+
method: 'POST',
|
|
2447
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2448
|
+
body: JSON.stringify({ id })
|
|
2449
|
+
})
|
|
2450
|
+
const data = await reportAction(res)
|
|
2451
|
+
if (data) await loadState()
|
|
2452
|
+
} catch (err) {
|
|
2453
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
const handleTest = async (prof) => {
|
|
2458
|
+
setBusy('testing')
|
|
2459
|
+
setTestResult(null)
|
|
2460
|
+
try {
|
|
2461
|
+
const res = await fetch('/dsh-remote-workspace/test', {
|
|
2462
|
+
method: 'POST',
|
|
2463
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2464
|
+
body: JSON.stringify(prof)
|
|
2465
|
+
})
|
|
2466
|
+
const data = await res.json()
|
|
2467
|
+
setTestResult(data)
|
|
2468
|
+
} catch (err) {
|
|
2469
|
+
setTestResult({ ok: false, error: err.message })
|
|
2470
|
+
}
|
|
2471
|
+
setBusy(null)
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
const openDirectoryBrowser = async (initialPath) => {
|
|
2475
|
+
setBrowserOpen(true)
|
|
2476
|
+
const target = initialPath && initialPath.startsWith('/') ? initialPath : '/'
|
|
2477
|
+
await fetchRemoteDir(target)
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
const fetchRemoteDir = async (dirPath) => {
|
|
2481
|
+
setBrowserLoading(true)
|
|
2482
|
+
try {
|
|
2483
|
+
const res = await fetch('/dsh-remote-workspace/browse', {
|
|
2484
|
+
method: 'POST',
|
|
2485
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2486
|
+
body: JSON.stringify({ profile: editing, remotePath: dirPath })
|
|
2487
|
+
})
|
|
2488
|
+
const data = await reportAction(res)
|
|
2489
|
+
if (data && data.ok) {
|
|
2490
|
+
setBrowserPath(data.currentPath || dirPath)
|
|
2491
|
+
setBrowserEntries(data.entries || [])
|
|
2492
|
+
}
|
|
2493
|
+
} catch (err) {
|
|
2494
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
2495
|
+
}
|
|
2496
|
+
setBrowserLoading(false)
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
const handleSync = async (direction) => {
|
|
2500
|
+
setBusy(`sync_${direction}`)
|
|
2501
|
+
setSyncMsg(null)
|
|
2502
|
+
try {
|
|
2503
|
+
const res = await fetch('/dsh-remote-workspace/sync', {
|
|
2504
|
+
method: 'POST',
|
|
2505
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2506
|
+
body: JSON.stringify({ direction })
|
|
2507
|
+
})
|
|
2508
|
+
const data = await res.json()
|
|
2509
|
+
if (data.ok) {
|
|
2510
|
+
const count = (direction === 'push' ? data.pushed?.length : data.pulled?.length) || 0
|
|
2511
|
+
if (data.conflicts && data.conflicts.length > 0) {
|
|
2512
|
+
setSyncMsg({ ok: false, text: t('syncConflict', { count: data.conflicts.length }) })
|
|
2513
|
+
} else {
|
|
2514
|
+
setSyncMsg({ ok: true, text: t('syncOk', { count }) })
|
|
2515
|
+
}
|
|
2516
|
+
} else {
|
|
2517
|
+
setSyncMsg({ ok: false, text: t('syncErr', { err: data.error || 'Unknown error' }) })
|
|
2518
|
+
}
|
|
2519
|
+
} catch (err) {
|
|
2520
|
+
setSyncMsg({ ok: false, text: t('syncErr', { err: err.message }) })
|
|
2521
|
+
}
|
|
2522
|
+
setBusy(null)
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
const handleStartTunnel = async () => {
|
|
2526
|
+
if (!activeId) return
|
|
2527
|
+
setBusy('tunnel_open')
|
|
2528
|
+
setTunnelMsg(null)
|
|
2529
|
+
try {
|
|
2530
|
+
const res = await fetch('/dsh-remote-workspace/tunnels/start', {
|
|
2531
|
+
method: 'POST',
|
|
2532
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2533
|
+
body: JSON.stringify({
|
|
2534
|
+
profileId: activeId,
|
|
2535
|
+
localPort: newTunnelLocal,
|
|
2536
|
+
remotePort: newTunnelRemote
|
|
2537
|
+
})
|
|
2538
|
+
})
|
|
2539
|
+
const data = await res.json()
|
|
2540
|
+
if (data.ok) {
|
|
2541
|
+
setTunnelMsg({ ok: true, text: t('tOpenOk', { localPort: newTunnelLocal, remotePort: newTunnelRemote }) })
|
|
2542
|
+
await loadState()
|
|
2543
|
+
} else {
|
|
2544
|
+
setTunnelMsg({ ok: false, text: t('tOpenErr', { err: data.error || 'Failed' }) })
|
|
2545
|
+
}
|
|
2546
|
+
} catch (err) {
|
|
2547
|
+
setTunnelMsg({ ok: false, text: t('tOpenErr', { err: err.message }) })
|
|
2548
|
+
}
|
|
2549
|
+
setBusy(null)
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
const handleStopTunnel = async (tunnelId) => {
|
|
2553
|
+
try {
|
|
2554
|
+
const res = await fetch('/dsh-remote-workspace/tunnels/stop', {
|
|
2555
|
+
method: 'POST',
|
|
2556
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2557
|
+
body: JSON.stringify({ tunnelId })
|
|
2558
|
+
})
|
|
2559
|
+
const data = await reportAction(res)
|
|
2560
|
+
if (data) await loadState()
|
|
2561
|
+
} catch (err) {
|
|
2562
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
const activeProfile = profiles.find((p) => p.id === activeId)
|
|
2567
|
+
|
|
2568
|
+
const handleImport = async () => {
|
|
2569
|
+
try {
|
|
2570
|
+
const res = await fetch('/dsh-remote-workspace/profiles/import-ssh-config', {
|
|
2571
|
+
method: 'POST',
|
|
2572
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2573
|
+
body: '{}'
|
|
2574
|
+
})
|
|
2575
|
+
const data = await reportAction(res)
|
|
2576
|
+
if (data) {
|
|
2577
|
+
setImportReport(data)
|
|
2578
|
+
await loadState()
|
|
2579
|
+
}
|
|
2580
|
+
} catch (err) {
|
|
2581
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
const skipReason = (reason) => {
|
|
2586
|
+
if (reason === 'wildcard') return t('skipWildcard')
|
|
2587
|
+
if (reason === 'match') return t('skipMatch')
|
|
2588
|
+
if (reason === 'duplicate') return t('skipDuplicate')
|
|
2589
|
+
if (reason === 'missing-include') return t('skipMissing')
|
|
2590
|
+
return reason
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
return React.createElement(
|
|
2594
|
+
'div',
|
|
2595
|
+
{ className: 'drw-page' },
|
|
2596
|
+
|
|
2597
|
+
// Header with Live Badges (ClineBot Style)
|
|
2598
|
+
React.createElement(
|
|
2599
|
+
'div',
|
|
2600
|
+
{ className: 'drw-header' },
|
|
2601
|
+
React.createElement(
|
|
2602
|
+
'div',
|
|
2603
|
+
{ className: 'drw-header-top' },
|
|
2604
|
+
React.createElement('div', { className: 'drw-page-title' }, '🌐 ' + t('title')),
|
|
2605
|
+
React.createElement(
|
|
2606
|
+
'div',
|
|
2607
|
+
{ className: 'drw-header-badges' },
|
|
2608
|
+
activeProfile
|
|
2609
|
+
? React.createElement('span', { className: 'drw-badge drw-badge-ok' }, t('badgeOnline', { name: activeProfile.name || activeProfile.host, latency: testResult?.latencyMs ?? 15 }))
|
|
2610
|
+
: React.createElement('span', { className: 'drw-badge drw-badge-warn' }, t('badgeOffline')),
|
|
2611
|
+
activeProfile
|
|
2612
|
+
? React.createElement('span', { className: 'drw-badge' }, activeProfile.authType === 'password' ? t('badgePassword') : t('badgeKey'))
|
|
2613
|
+
: null,
|
|
2614
|
+
React.createElement('span', { className: `drw-badge ${tunnels.length > 0 ? 'drw-badge-ok' : ''}` }, t('badgeTunnels', { count: tunnels.length }))
|
|
2615
|
+
)
|
|
2616
|
+
),
|
|
2617
|
+
React.createElement('div', { className: 'drw-page-sub' }, t('subtitle')),
|
|
2618
|
+
actionError
|
|
2619
|
+
? React.createElement('div', { className: 'drw-badge drw-badge-err', role: 'alert' }, actionError)
|
|
2620
|
+
: null
|
|
2621
|
+
),
|
|
2622
|
+
|
|
2623
|
+
|
|
2624
|
+
|
|
2625
|
+
authPrompts.length
|
|
2626
|
+
? React.createElement(
|
|
2627
|
+
'div',
|
|
2628
|
+
{ className: 'drw-card', role: 'dialog', 'aria-label': t('authPromptTitle') },
|
|
2629
|
+
React.createElement('div', { className: 'drw-page-title' }, t('authPromptTitle')),
|
|
2630
|
+
authPrompts.map((prompt) => React.createElement(
|
|
2631
|
+
'div',
|
|
2632
|
+
{ key: prompt.profileId, className: 'drw-field' },
|
|
2633
|
+
prompt.instructions
|
|
2634
|
+
? React.createElement('span', { className: 'drw-hint' }, prompt.instructions)
|
|
2635
|
+
: null,
|
|
2636
|
+
(prompt.prompts || []).map((field, index) => React.createElement('input', {
|
|
2637
|
+
key: index,
|
|
2638
|
+
className: 'drw-input',
|
|
2639
|
+
type: field.echo ? 'text' : 'password',
|
|
2640
|
+
placeholder: field.prompt || t('authPromptCode'),
|
|
2641
|
+
value: authAnswers[prompt.profileId + ':' + index] || '',
|
|
2642
|
+
onChange: (e) => setAuthAnswers({ ...authAnswers, [prompt.profileId + ':' + index]: e.target.value })
|
|
2643
|
+
})),
|
|
2644
|
+
React.createElement('button', {
|
|
2645
|
+
type: 'button',
|
|
2646
|
+
className: 'drw-btn drw-btn-primary',
|
|
2647
|
+
onClick: () => submitAuth(prompt)
|
|
2648
|
+
}, t('authPromptSubmit'))
|
|
2649
|
+
))
|
|
2650
|
+
)
|
|
2651
|
+
: null,
|
|
2652
|
+
|
|
2653
|
+
|
|
2654
|
+
React.createElement(
|
|
2655
|
+
'div',
|
|
2656
|
+
{ className: 'drw-card' },
|
|
2657
|
+
React.createElement('button', { type: 'button', className: 'drw-btn', onClick: handleImport }, t('btnImportSsh')),
|
|
2658
|
+
importReport
|
|
2659
|
+
? React.createElement(
|
|
2660
|
+
'div',
|
|
2661
|
+
{ className: 'drw-hint', role: 'status' },
|
|
2662
|
+
t('importAdded', { count: importReport.count || 0 }),
|
|
2663
|
+
(importReport.skipped || []).map((item, index) => React.createElement('div', { key: index }, (item.name || '') + ': ' + skipReason(item.reason)))
|
|
2664
|
+
)
|
|
2665
|
+
: null
|
|
2666
|
+
),
|
|
2667
|
+
|
|
2668
|
+
|
|
2669
|
+
React.createElement('div', { className: 'drw-card' },
|
|
2670
|
+
React.createElement('label', { className: 'drw-label' }, t('fTerminalFont')),
|
|
2671
|
+
React.createElement('div', { style: { display: 'flex', gap: '8px' } },
|
|
2672
|
+
React.createElement('input', {
|
|
2673
|
+
className: 'drw-input',
|
|
2674
|
+
type: 'text',
|
|
2675
|
+
value: terminalFont,
|
|
2676
|
+
placeholder: 'ui-monospace, Consolas, monospace',
|
|
2677
|
+
onChange: (e) => setTerminalFont(e.target.value)
|
|
2678
|
+
}),
|
|
2679
|
+
React.createElement('button', { type: 'button', className: 'drw-btn', onClick: saveTerminalFont }, t('btnSave'))
|
|
2680
|
+
),
|
|
2681
|
+
React.createElement('div', { className: 'drw-hint' }, t('fTerminalFontHint'))
|
|
2682
|
+
),
|
|
2683
|
+
|
|
2684
|
+
React.createElement('div', { style: { display: 'flex', gap: '8px' } },
|
|
2685
|
+
React.createElement('button', { type: 'button', className: activeTab === 'profiles' ? 'drw-btn drw-btn-primary' : 'drw-btn', onClick: () => setActiveTab('profiles') }, t('tabProfiles')),
|
|
2686
|
+
React.createElement('button', { type: 'button', className: activeTab === 'cluster' ? 'drw-btn drw-btn-primary' : 'drw-btn', onClick: () => setActiveTab('cluster') }, t('tabCluster'))
|
|
2687
|
+
, React.createElement('button', { type: 'button', className: activeTab === 'files' ? 'drw-btn drw-btn-primary' : 'drw-btn', onClick: () => setActiveTab('files') }, t('tabFiles'))
|
|
2688
|
+
),
|
|
2689
|
+
activeTab === 'cluster'
|
|
2690
|
+
? React.createElement(ClusterPanel, { t, profiles })
|
|
2691
|
+
: activeTab === 'files'
|
|
2692
|
+
? React.createElement(ExplorerTab, { t, activeProfile })
|
|
2693
|
+
: React.createElement(ProfilesPanel, { t, editing, setEditing, setTestResult, profiles, activeId, busy, handleSave, testResult, handleTest, handleSetActive, handleDelete, browserOpen, setBrowserOpen, browserPath, browserEntries, browserLoading, openDirectoryBrowser, fetchRemoteDir, showPassword, setShowPassword }),
|
|
2694
|
+
|
|
2695
|
+
|
|
2696
|
+
// Section 2: Active Connection Diagnostics (ClineBot Card 4 style)
|
|
2697
|
+
activeProfile
|
|
2698
|
+
? React.createElement(
|
|
2699
|
+
'div',
|
|
2700
|
+
{ className: 'drw-card' },
|
|
2701
|
+
React.createElement(
|
|
2702
|
+
'div',
|
|
2703
|
+
{ className: 'drw-card-title' },
|
|
2704
|
+
React.createElement('span', null, t('diagTitle')),
|
|
2705
|
+
React.createElement(
|
|
2706
|
+
'button',
|
|
2707
|
+
{
|
|
2708
|
+
type: 'button',
|
|
2709
|
+
className: 'drw-btn',
|
|
2710
|
+
style: { fontSize: '11px', padding: '3px 9px' },
|
|
2711
|
+
disabled: busy === 'testing',
|
|
2712
|
+
onClick: () => handleTest(activeProfile)
|
|
2713
|
+
},
|
|
2714
|
+
busy === 'testing' ? t('testing') : t('diagProbeBtn')
|
|
2715
|
+
)
|
|
2716
|
+
),
|
|
2717
|
+
React.createElement('div', { className: 'drw-card-desc' }, t('diagDesc')),
|
|
2718
|
+
React.createElement(
|
|
2719
|
+
'div',
|
|
2720
|
+
{ className: 'drw-grid-4' },
|
|
2721
|
+
React.createElement(
|
|
2722
|
+
'div',
|
|
2723
|
+
{ className: 'drw-stat-box' },
|
|
2724
|
+
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.name || activeProfile.host),
|
|
2725
|
+
React.createElement('div', { className: 'drw-stat-lbl' }, `SSH: ${activeProfile.username}@${activeProfile.host}:${activeProfile.port || 22}`)
|
|
2726
|
+
),
|
|
2727
|
+
React.createElement(
|
|
2728
|
+
'div',
|
|
2729
|
+
{ className: 'drw-stat-box' },
|
|
2730
|
+
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.remoteWorkspace || '—'),
|
|
2731
|
+
React.createElement('div', { className: 'drw-stat-lbl' }, 'Remote Workspace')
|
|
2732
|
+
),
|
|
2733
|
+
React.createElement(
|
|
2734
|
+
'div',
|
|
2735
|
+
{ className: 'drw-stat-box' },
|
|
2736
|
+
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.localMirrorPath || '—'),
|
|
2737
|
+
React.createElement('div', { className: 'drw-stat-lbl' }, 'Local Mirror Path')
|
|
2738
|
+
),
|
|
2739
|
+
React.createElement(
|
|
2740
|
+
'div',
|
|
2741
|
+
{ className: 'drw-stat-box' },
|
|
2742
|
+
React.createElement('div', { className: 'drw-stat-val', style: { color: 'var(--dsw-alias-state-success-primary)' } }, 'Ready ✓'),
|
|
2743
|
+
React.createElement('div', { className: 'drw-stat-lbl' }, activeProfile.authType === 'password' ? 'Password Auth' : 'SSH Key Auth')
|
|
2744
|
+
)
|
|
2745
|
+
)
|
|
2746
|
+
)
|
|
2747
|
+
: null,
|
|
2748
|
+
|
|
2749
|
+
|
|
2750
|
+
React.createElement(SyncPanel, { t, busy, activeProfile, handleSync, syncMsg, autoSync, setAutoSync, reportAction, setActionError }),
|
|
2751
|
+
|
|
2752
|
+
React.createElement(TunnelsPanel, { t, busy, activeProfile, newTunnelLocal, setNewTunnelLocal, newTunnelRemote, setNewTunnelRemote, handleStartTunnel, tunnelMsg, tunnels, handleStopTunnel }),
|
|
2753
|
+
|
|
2754
|
+
)
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
// Accordion item for Settings -> Plugins section
|
|
2758
|
+
|
|
2759
|
+
function UpdaterSection(props) {
|
|
2760
|
+
const t = props.t || ((k) => k);
|
|
2761
|
+
const [updateStatus, setUpdateStatus] = React.useState(null);
|
|
2762
|
+
const [loading, setLoading] = React.useState(false);
|
|
2763
|
+
const [updating, setUpdating] = React.useState(false);
|
|
2764
|
+
const [msg, setMsg] = React.useState(null);
|
|
2765
|
+
|
|
2766
|
+
const checkUpdate = React.useCallback(async () => {
|
|
2767
|
+
setLoading(true);
|
|
2768
|
+
setMsg(null);
|
|
2769
|
+
try {
|
|
2770
|
+
const res = await fetch('/dsh-remote-workspace/update', {
|
|
2771
|
+
headers: { accept: 'application/json' },
|
|
2772
|
+
cache: 'no-store'
|
|
2773
|
+
});
|
|
2774
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
2775
|
+
const data = await res.json();
|
|
2776
|
+
setUpdateStatus(data);
|
|
2777
|
+
} catch (e) {
|
|
2778
|
+
setMsg({ ok: false, text: (t('updateFailed') || 'Failed: ') + (e?.message || String(e)) });
|
|
2779
|
+
} finally {
|
|
2780
|
+
setLoading(false);
|
|
2781
|
+
}
|
|
2782
|
+
}, [t]);
|
|
2783
|
+
|
|
2784
|
+
const doUpdate = async () => {
|
|
2785
|
+
setUpdating(true);
|
|
2786
|
+
setMsg(null);
|
|
2787
|
+
try {
|
|
2788
|
+
const res = await fetch('/dsh-remote-workspace/update', {
|
|
2789
|
+
method: 'POST',
|
|
2790
|
+
headers: {
|
|
2791
|
+
'x-dsh-plugin-update': '1',
|
|
2792
|
+
'content-type': 'application/json'
|
|
2793
|
+
}
|
|
2794
|
+
});
|
|
2795
|
+
const data = await res.json();
|
|
2796
|
+
if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status));
|
|
2797
|
+
setUpdateStatus(data);
|
|
2798
|
+
if (data.restartRequired) {
|
|
2799
|
+
setMsg({ ok: true, text: t('restartRequired') });
|
|
2800
|
+
}
|
|
2801
|
+
} catch (e) {
|
|
2802
|
+
setMsg({ ok: false, text: (t('updateFailed') || 'Update failed: ') + (e?.message || String(e)) });
|
|
2803
|
+
} finally {
|
|
2804
|
+
setUpdating(false);
|
|
2805
|
+
}
|
|
2806
|
+
};
|
|
2807
|
+
|
|
2808
|
+
React.useEffect(() => {
|
|
2809
|
+
checkUpdate();
|
|
2810
|
+
}, [checkUpdate]);
|
|
2811
|
+
|
|
2812
|
+
return React.createElement(
|
|
2813
|
+
'div',
|
|
2814
|
+
{
|
|
2815
|
+
style: {
|
|
2816
|
+
display: 'flex',
|
|
2817
|
+
alignItems: 'center',
|
|
2818
|
+
justifyContent: 'space-between',
|
|
2819
|
+
padding: '10px 14px',
|
|
2820
|
+
marginBottom: '14px',
|
|
2821
|
+
borderRadius: '8px',
|
|
2822
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
2823
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
2824
|
+
fontSize: '13px'
|
|
2825
|
+
}
|
|
2826
|
+
},
|
|
2827
|
+
React.createElement(
|
|
2828
|
+
'div',
|
|
2829
|
+
{ style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
2830
|
+
React.createElement('span', null, '📦'),
|
|
2831
|
+
React.createElement(
|
|
2832
|
+
'div',
|
|
2833
|
+
null,
|
|
2834
|
+
React.createElement('span', { style: { fontWeight: 500 } }, (updateStatus?.currentVersion ? 'v' + updateStatus.currentVersion : t('versionUnknown'))),
|
|
2835
|
+
updateStatus?.updateAvailable
|
|
2836
|
+
? React.createElement('span', { style: { marginLeft: '8px', color: 'var(--dsw-alias-state-warning-text)' } }, '→ v' + updateStatus.latestVersion)
|
|
2837
|
+
: null,
|
|
2838
|
+
msg
|
|
2839
|
+
? React.createElement('div', { style: { fontSize: '11px', marginTop: '3px', color: msg.ok ? 'var(--dsw-alias-state-success-text)' : 'var(--dsw-alias-state-error-text)' } }, msg.text)
|
|
2840
|
+
: null
|
|
2841
|
+
)
|
|
2842
|
+
),
|
|
2843
|
+
React.createElement(
|
|
2844
|
+
'div',
|
|
2845
|
+
{ style: { display: 'flex', gap: '8px' } },
|
|
2846
|
+
updateStatus?.updateAvailable
|
|
2847
|
+
? React.createElement(
|
|
2848
|
+
'button',
|
|
2849
|
+
{
|
|
2850
|
+
type: 'button',
|
|
2851
|
+
className: 'drw-btn drw-btn-primary',
|
|
2852
|
+
disabled: updating,
|
|
2853
|
+
onClick: doUpdate,
|
|
2854
|
+
style: { fontSize: '12px', padding: '4px 10px' }
|
|
2855
|
+
},
|
|
2856
|
+
updating ? t('updating') : t('updateNow')
|
|
2857
|
+
)
|
|
2858
|
+
: React.createElement(
|
|
2859
|
+
'button',
|
|
2860
|
+
{
|
|
2861
|
+
type: 'button',
|
|
2862
|
+
className: 'drw-btn',
|
|
2863
|
+
disabled: loading,
|
|
2864
|
+
onClick: checkUpdate,
|
|
2865
|
+
style: { fontSize: '12px', padding: '4px 10px' }
|
|
2866
|
+
},
|
|
2867
|
+
loading ? t('checking') : (updateStatus?.updateAvailable === false ? t('upToDate') : t('checkUpdate'))
|
|
2868
|
+
)
|
|
2869
|
+
)
|
|
2870
|
+
);
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
function PluginCard(props) {
|
|
2874
|
+
const page = !!(props && props.view === 'page')
|
|
2875
|
+
const [open, setOpen] = React.useState(!!page)
|
|
2876
|
+
const t = props.t || (props.locale === 'zh' ? makeT(zh, en) : makeT(en, zh))
|
|
2877
|
+
|
|
2878
|
+
const [conn, setConn] = React.useState('loading')
|
|
2879
|
+
|
|
2880
|
+
React.useEffect(() => {
|
|
2881
|
+
ensureCss()
|
|
2882
|
+
}, [])
|
|
2883
|
+
|
|
2884
|
+
React.useEffect(() => {
|
|
2885
|
+
let cancelled = false
|
|
2886
|
+
fetch('/dsh-remote-workspace/state')
|
|
2887
|
+
.then((res) => {
|
|
2888
|
+
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
2889
|
+
return res.json()
|
|
2890
|
+
})
|
|
2891
|
+
.then((data) => {
|
|
2892
|
+
if (cancelled) return
|
|
2893
|
+
if (!data || data.ok === false) {
|
|
2894
|
+
setConn('error')
|
|
2895
|
+
return
|
|
2896
|
+
}
|
|
2897
|
+
const active = (data.profiles || []).find((item) => item.id === data.activeId)
|
|
2898
|
+
setConn(active ? 'ready' : 'empty')
|
|
2899
|
+
})
|
|
2900
|
+
.catch(() => {
|
|
2901
|
+
if (!cancelled) setConn('error')
|
|
2902
|
+
})
|
|
2903
|
+
return () => { cancelled = true }
|
|
2904
|
+
}, [])
|
|
2905
|
+
|
|
2906
|
+
// Row seat (plugins.row.config): the host page draws title/icon/crumb and the
|
|
2907
|
+
// padding, so the summary is a one-liner and the page drops our card chrome.
|
|
2908
|
+
if (props && props.view === 'summary') {
|
|
2909
|
+
return React.createElement('span', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('subtitle'))
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
return React.createElement(
|
|
2913
|
+
page ? 'div' : 'li',
|
|
2914
|
+
{ className: page ? 'drw-page' : 'drw-card', style: page ? undefined : { listStyle: 'none', marginBottom: '12px' } },
|
|
2915
|
+
React.createElement(
|
|
2916
|
+
'button',
|
|
2917
|
+
{
|
|
2217
2918
|
type: 'button',
|
|
2218
2919
|
style: {
|
|
2219
2920
|
background: 'none',
|
|
@@ -2235,11 +2936,22 @@ window.__ModuleLoader__.load({
|
|
|
2235
2936
|
React.createElement('div', { style: { fontWeight: 600, fontSize: '15px', color: 'var(--dsw-alias-label-primary)' } }, t('title')),
|
|
2236
2937
|
React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('subtitle'))
|
|
2237
2938
|
),
|
|
2238
|
-
React.createElement('span', {
|
|
2939
|
+
React.createElement('span', {
|
|
2940
|
+
className: 'drw-badge ' + (
|
|
2941
|
+
conn === 'ready' ? 'drw-badge-ok'
|
|
2942
|
+
: conn === 'error' ? 'drw-badge-err'
|
|
2943
|
+
: conn === 'empty' ? 'drw-badge-warn' : ''
|
|
2944
|
+
),
|
|
2945
|
+
style: { marginRight: '14px' }
|
|
2946
|
+
}, t(
|
|
2947
|
+
conn === 'ready' ? 'statusReady'
|
|
2948
|
+
: conn === 'error' ? 'statusError'
|
|
2949
|
+
: conn === 'empty' ? 'statusEmpty' : 'statusLoading'
|
|
2950
|
+
)),
|
|
2239
2951
|
React.createElement(
|
|
2240
2952
|
'span',
|
|
2241
2953
|
{ style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
|
|
2242
|
-
React.createElement(FallbackChevron)
|
|
2954
|
+
React.createElement(ChevronIcon || FallbackChevron)
|
|
2243
2955
|
)
|
|
2244
2956
|
),
|
|
2245
2957
|
(page || open)
|
|
@@ -2290,7 +3002,232 @@ window.__ModuleLoader__.load({
|
|
|
2290
3002
|
)
|
|
2291
3003
|
}
|
|
2292
3004
|
|
|
3005
|
+
const DRW_ACTIVE = 'data-drw-active'
|
|
3006
|
+
const DRW_VIEW = 'data-drw-view'
|
|
3007
|
+
const DRW_ENTRY = 'data-drw-entry'
|
|
3008
|
+
const DRW_PANEL = 'remote-workspace'
|
|
3009
|
+
const DRW_ICON = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="12" rx="2"></rect><path d="M8 20h8"></path><path d="M12 16v4"></path></svg>'
|
|
3010
|
+
|
|
3011
|
+
function workspaceTranslator(ctx) {
|
|
3012
|
+
let lang = ''
|
|
3013
|
+
try {
|
|
3014
|
+
lang = ctx && ctx.locale && typeof ctx.locale.getLocale === 'function' ? ctx.locale.getLocale() : ''
|
|
3015
|
+
} catch (err) { lang = '' }
|
|
3016
|
+
if (typeof lang === 'string' && (lang === 'zh' || lang.indexOf('zh') === 0)) return makeT(zh, en)
|
|
3017
|
+
return makeT(en, zh)
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
function WorkspaceScreen(props) {
|
|
3021
|
+
const t = workspaceTranslator(props.ctx)
|
|
3022
|
+
const [tab, setTab] = React.useState('hosts')
|
|
3023
|
+
const [profiles, setProfiles] = React.useState([])
|
|
3024
|
+
const [activeId, setActiveId] = React.useState('')
|
|
3025
|
+
const [tunnels, setTunnels] = React.useState([])
|
|
3026
|
+
const [localPort, setLocalPort] = React.useState('3000')
|
|
3027
|
+
const [remotePort, setRemotePort] = React.useState('3000')
|
|
3028
|
+
const [tunnelMsg, setTunnelMsg] = React.useState(null)
|
|
3029
|
+
const [busy, setBusy] = React.useState(null)
|
|
3030
|
+
const activeProfile = profiles.find((item) => item.id === activeId) || null
|
|
3031
|
+
|
|
3032
|
+
const loadState = async () => {
|
|
3033
|
+
try {
|
|
3034
|
+
const res = await fetch('/dsh-remote-workspace/state')
|
|
3035
|
+
const data = await res.json()
|
|
3036
|
+
if (data && data.ok) {
|
|
3037
|
+
setProfiles(data.profiles || [])
|
|
3038
|
+
setActiveId(data.activeId || '')
|
|
3039
|
+
setTunnels(data.tunnels || [])
|
|
3040
|
+
}
|
|
3041
|
+
} catch (err) { /* state refresh is best effort */ }
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
React.useEffect(() => {
|
|
3045
|
+
ensureCss()
|
|
3046
|
+
loadState()
|
|
3047
|
+
}, [])
|
|
3048
|
+
|
|
3049
|
+
const startTunnel = async () => {
|
|
3050
|
+
if (!activeId) return
|
|
3051
|
+
setBusy('tunnel_open')
|
|
3052
|
+
try {
|
|
3053
|
+
const res = await fetch('/dsh-remote-workspace/tunnels/start', {
|
|
3054
|
+
method: 'POST',
|
|
3055
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3056
|
+
body: JSON.stringify({ profileId: activeId, localPort, remotePort })
|
|
3057
|
+
})
|
|
3058
|
+
const data = await res.json()
|
|
3059
|
+
setTunnelMsg(data && data.ok
|
|
3060
|
+
? { ok: true, text: t('tOpenOk', { localPort, remotePort }) }
|
|
3061
|
+
: { ok: false, text: t('tOpenErr', { err: (data && data.error) || 'Failed' }) })
|
|
3062
|
+
if (data && data.ok) await loadState()
|
|
3063
|
+
} catch (err) {
|
|
3064
|
+
setTunnelMsg({ ok: false, text: t('tOpenErr', { err: err.message }) })
|
|
3065
|
+
}
|
|
3066
|
+
setBusy(null)
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
const stopTunnel = async (tunnelId) => {
|
|
3070
|
+
setBusy('tunnel_stop')
|
|
3071
|
+
try {
|
|
3072
|
+
await fetch('/dsh-remote-workspace/tunnels/stop', {
|
|
3073
|
+
method: 'POST',
|
|
3074
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3075
|
+
body: JSON.stringify({ tunnelId })
|
|
3076
|
+
})
|
|
3077
|
+
await loadState()
|
|
3078
|
+
} catch (err) { /* stop is reported by the next state load */ }
|
|
3079
|
+
setBusy(null)
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
const tabs = [
|
|
3083
|
+
['hosts', 'wsHosts'],
|
|
3084
|
+
['terminal', 'wsTerminal'],
|
|
3085
|
+
['files', 'wsFiles'],
|
|
3086
|
+
['docker', 'wsDocker'],
|
|
3087
|
+
['tunnels', 'wsTunnels'],
|
|
3088
|
+
['cluster', 'wsCluster']
|
|
3089
|
+
]
|
|
3090
|
+
|
|
3091
|
+
return React.createElement('div', { className: 'drw-ws' },
|
|
3092
|
+
React.createElement('div', { className: 'drw-ws-bar' },
|
|
3093
|
+
React.createElement('button', { type: 'button', className: 'drw-btn', onClick: () => props.onClose && props.onClose() }, t('wsBack')),
|
|
3094
|
+
tabs.map(([id, key]) => React.createElement('button', {
|
|
3095
|
+
key: id,
|
|
3096
|
+
type: 'button',
|
|
3097
|
+
className: tab === id ? 'drw-btn drw-btn-primary' : 'drw-btn',
|
|
3098
|
+
onClick: () => setTab(id)
|
|
3099
|
+
}, t(key)))
|
|
3100
|
+
),
|
|
3101
|
+
React.createElement('div', { className: 'drw-ws-panel', hidden: tab !== 'hosts' }, React.createElement(SettingsView, props)),
|
|
3102
|
+
React.createElement('div', { className: 'drw-ws-panel', hidden: tab !== 'terminal' }, React.createElement(TerminalTab, { t, activeProfile })),
|
|
3103
|
+
React.createElement('div', { className: 'drw-ws-panel', hidden: tab !== 'files' }, React.createElement(ExplorerTab, { t, activeProfile })),
|
|
3104
|
+
React.createElement('div', { className: 'drw-ws-panel', hidden: tab !== 'docker' }, React.createElement(ContainersTab, { t, activeProfile })),
|
|
3105
|
+
React.createElement('div', { className: 'drw-ws-panel', hidden: tab !== 'tunnels' }, React.createElement(TunnelsPanel, {
|
|
3106
|
+
t, busy, activeProfile, tunnelMsg, tunnels,
|
|
3107
|
+
newTunnelLocal: localPort, setNewTunnelLocal: setLocalPort,
|
|
3108
|
+
newTunnelRemote: remotePort, setNewTunnelRemote: setRemotePort,
|
|
3109
|
+
handleStartTunnel: startTunnel, handleStopTunnel: stopTunnel
|
|
3110
|
+
})),
|
|
3111
|
+
React.createElement('div', { className: 'drw-ws-panel', hidden: tab !== 'cluster' }, React.createElement(ClusterPanel, { t, profiles }))
|
|
3112
|
+
)
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
function createWorkspaceToggle() {
|
|
3116
|
+
let open = false
|
|
3117
|
+
const listeners = new Set()
|
|
3118
|
+
return {
|
|
3119
|
+
isOpen() { return open },
|
|
3120
|
+
set(next) {
|
|
3121
|
+
open = Boolean(next)
|
|
3122
|
+
listeners.forEach((fn) => fn(open))
|
|
3123
|
+
},
|
|
3124
|
+
toggle() { this.set(!open) },
|
|
3125
|
+
subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn) }
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
function nativeSidebarButton() {
|
|
3130
|
+
const nodes = document.querySelectorAll('button[class*="newSession"]')
|
|
3131
|
+
for (const node of nodes) {
|
|
3132
|
+
if (String(node.className).indexOf('newSessionLabel') === -1) return node
|
|
3133
|
+
}
|
|
3134
|
+
return null
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
function mountRemoteWorkspace(ctx) {
|
|
3138
|
+
if (typeof document === 'undefined') return function () {}
|
|
3139
|
+
ensureCss()
|
|
3140
|
+
const toggle = createWorkspaceToggle()
|
|
3141
|
+
const t = workspaceTranslator(ctx)
|
|
3142
|
+
let entry = null
|
|
3143
|
+
let container = null
|
|
3144
|
+
let root = null
|
|
3145
|
+
|
|
3146
|
+
const placeEntry = () => {
|
|
3147
|
+
if (entry && entry.isConnected) return
|
|
3148
|
+
const target = nativeSidebarButton()
|
|
3149
|
+
if (!target || !target.parentElement) return
|
|
3150
|
+
if (target.parentElement.querySelector('[' + DRW_ENTRY + ']')) return
|
|
3151
|
+
entry = target.cloneNode(true)
|
|
3152
|
+
entry.className = String(target.className) + ' drw-entry-clone'
|
|
3153
|
+
entry.setAttribute(DRW_ENTRY, '')
|
|
3154
|
+
entry.setAttribute('data-dsh-plugin', 'dsh-remote-workspace')
|
|
3155
|
+
entry.setAttribute('aria-label', t('navLabel'))
|
|
3156
|
+
entry.removeAttribute('id')
|
|
3157
|
+
entry.innerHTML = ''
|
|
3158
|
+
const icon = document.createElement('span')
|
|
3159
|
+
icon.className = 'drw-entry-icon'
|
|
3160
|
+
icon.innerHTML = DRW_ICON
|
|
3161
|
+
const label = document.createElement('span')
|
|
3162
|
+
label.className = 'drw-entry-label'
|
|
3163
|
+
label.textContent = t('navLabel')
|
|
3164
|
+
entry.appendChild(icon)
|
|
3165
|
+
entry.appendChild(label)
|
|
3166
|
+
entry.addEventListener('click', (event) => {
|
|
3167
|
+
event.preventDefault()
|
|
3168
|
+
event.stopPropagation()
|
|
3169
|
+
toggle.toggle()
|
|
3170
|
+
})
|
|
3171
|
+
target.insertAdjacentElement('afterend', entry)
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
const reactDom = () => {
|
|
3175
|
+
if (typeof window !== 'undefined' && window.ReactDOM) return window.ReactDOM
|
|
3176
|
+
try { return require('react-dom/client') } catch (err) { return null }
|
|
3177
|
+
}
|
|
3178
|
+
|
|
3179
|
+
const placeView = () => {
|
|
3180
|
+
const column = document.querySelector('[data-pane=conversation], [class*=centerCol]')
|
|
3181
|
+
if (!column) return
|
|
3182
|
+
if (!container) {
|
|
3183
|
+
container = document.createElement('div')
|
|
3184
|
+
container.setAttribute(DRW_VIEW, '')
|
|
3185
|
+
container.setAttribute('data-dsh-plugin', 'dsh-remote-workspace')
|
|
3186
|
+
column.appendChild(container)
|
|
3187
|
+
const ReactDOM = reactDom()
|
|
3188
|
+
if (ReactDOM && typeof ReactDOM.createRoot === 'function') {
|
|
3189
|
+
root = ReactDOM.createRoot(container)
|
|
3190
|
+
root.render(React.createElement(WorkspaceScreen, { ctx, onClose: () => toggle.set(false) }))
|
|
3191
|
+
}
|
|
3192
|
+
return
|
|
3193
|
+
}
|
|
3194
|
+
if (container.parentElement !== column) column.appendChild(container)
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
const applyOpen = () => {
|
|
3198
|
+
if (toggle.isOpen()) {
|
|
3199
|
+
document.documentElement.setAttribute(DRW_ACTIVE, '')
|
|
3200
|
+
document.dispatchEvent(new CustomEvent('dsh-panel-activate', { detail: DRW_PANEL }))
|
|
3201
|
+
if (entry) entry.dataset.active = 'true'
|
|
3202
|
+
} else {
|
|
3203
|
+
document.documentElement.removeAttribute(DRW_ACTIVE)
|
|
3204
|
+
if (entry) delete entry.dataset.active
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
const onOther = (event) => {
|
|
3209
|
+
if (event.detail !== DRW_PANEL && toggle.isOpen()) toggle.set(false)
|
|
3210
|
+
}
|
|
3211
|
+
const observer = new MutationObserver(() => { placeEntry(); placeView() })
|
|
3212
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
3213
|
+
document.addEventListener('dsh-panel-activate', onOther)
|
|
3214
|
+
const unsubscribe = toggle.subscribe(applyOpen)
|
|
3215
|
+
placeEntry()
|
|
3216
|
+
placeView()
|
|
3217
|
+
|
|
3218
|
+
return () => {
|
|
3219
|
+
observer.disconnect()
|
|
3220
|
+
document.removeEventListener('dsh-panel-activate', onOther)
|
|
3221
|
+
unsubscribe()
|
|
3222
|
+
document.documentElement.removeAttribute(DRW_ACTIVE)
|
|
3223
|
+
if (root && root.unmount) root.unmount()
|
|
3224
|
+
if (container) container.remove()
|
|
3225
|
+
if (entry) entry.remove()
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
|
|
2293
3229
|
function apply(ctx) {
|
|
3230
|
+
try { mountRemoteWorkspace(ctx) } catch (err) { console.warn('[dsh-remote-workspace] workspace mount skipped', err && err.message) }
|
|
2294
3231
|
if (ctx.locale && typeof ctx.locale.register === 'function') {
|
|
2295
3232
|
if (typeof ctx.effect === 'function') {
|
|
2296
3233
|
ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'dsh-remote-workspace: dictionaries');
|
|
@@ -2301,16 +3238,28 @@ window.__ModuleLoader__.load({
|
|
|
2301
3238
|
|
|
2302
3239
|
if (ctx.slots && typeof ctx.slots.inject === 'function') {
|
|
2303
3240
|
// Plugin-list seat (plugins.item) first: the seat the current core
|
|
2304
|
-
// (0.1.6-alpha.2) renders as the plugin's own page with its configuration.
|
|
2305
|
-
// label
|
|
2306
|
-
//
|
|
3241
|
+
// (0.1.6-alpha.2) renders as the plugin's own page with its configuration.
|
|
3242
|
+
// The label getter runs during page render. A locale-service lookup there
|
|
3243
|
+
// crashes the client batch, so the title is read from the dictionaries
|
|
3244
|
+
// already loaded in this bundle. English is the result when the active
|
|
3245
|
+
// language cannot be read.
|
|
2307
3246
|
ctx.slots.inject('plugins.item', () =>
|
|
2308
3247
|
ctx.slots.register(
|
|
2309
3248
|
{
|
|
2310
3249
|
name: 'plugins.item',
|
|
2311
3250
|
id: ROW_ID,
|
|
2312
3251
|
order: 60,
|
|
2313
|
-
label: () =>
|
|
3252
|
+
label: () => {
|
|
3253
|
+
try {
|
|
3254
|
+
const lang = ctx.locale && typeof ctx.locale.getLocale === 'function'
|
|
3255
|
+
? ctx.locale.getLocale()
|
|
3256
|
+
: ''
|
|
3257
|
+
if (typeof lang === 'string' && (lang === 'zh' || lang.indexOf('zh') === 0)) return zh.title
|
|
3258
|
+
} catch (err) {
|
|
3259
|
+
return en.title
|
|
3260
|
+
}
|
|
3261
|
+
return en.title
|
|
3262
|
+
},
|
|
2314
3263
|
locale: NS,
|
|
2315
3264
|
inject: () => ({ ctx })
|
|
2316
3265
|
},
|