dsh-remote 0.8.17 → 0.8.20
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 +40 -63
- package/README.zh.md +40 -47
- package/cordis.patch.yml +5 -55
- package/lib/binding.js +69 -0
- package/lib/client.js +369 -136
- package/lib/hostkey.js +60 -0
- package/lib/index.js +104 -924
- package/lib/pool.js +511 -0
- package/lib/remote-fs.js +143 -0
- package/lib/routes-fs.js +221 -0
- package/lib/search.js +59 -1
- package/lib/sync.js +15 -10
- package/package.json +2 -3
package/lib/client.js
CHANGED
|
@@ -94,6 +94,12 @@ window.__ModuleLoader__.load({
|
|
|
94
94
|
'settings.fwdAdd': '添加转发',
|
|
95
95
|
'settings.audit': '最近执行的远程命令(审计日志)',
|
|
96
96
|
'settings.auditEmpty': '暂无记录(在远端执行的命令会记录到 $DSH_HOME/remote-workspaces/audit.log)',
|
|
97
|
+
'settings.auditRefresh': '刷新日志',
|
|
98
|
+
'settings.badgePwd': '密码',
|
|
99
|
+
'settings.badgeKeychain': '钥匙串',
|
|
100
|
+
'settings.badgeProxy': '跳板',
|
|
101
|
+
'settings.sectionHide': '收起此栏',
|
|
102
|
+
'settings.sectionShow': '展开此栏',
|
|
97
103
|
'settings.update': '更新',
|
|
98
104
|
'settings.updateChecking': '版本信息加载中…',
|
|
99
105
|
'settings.checking': '检查中…',
|
|
@@ -188,6 +194,9 @@ window.__ModuleLoader__.load({
|
|
|
188
194
|
'picker.chooseDrive': ' 选择驱动器',
|
|
189
195
|
'picker.homeBtn': '🏠',
|
|
190
196
|
'picker.goHome': '回家目录',
|
|
197
|
+
'picker.hintKeys': 'Enter 确认 · Esc 关闭 · Ctrl/⌘+Enter 设为工作区',
|
|
198
|
+
'picker.star': '⭐ Star dsh-remote',
|
|
199
|
+
'picker.feedback': '反馈 / 提 issue',
|
|
191
200
|
'explorer.refresh': '刷新',
|
|
192
201
|
'explorer.newDir': '新建目录',
|
|
193
202
|
'explorer.more': '…',
|
|
@@ -210,6 +219,9 @@ window.__ModuleLoader__.load({
|
|
|
210
219
|
'explorer.menuDelete': '🗑 删除',
|
|
211
220
|
'explorer.menuDeleteDir': '🗑 删除(含子目录)',
|
|
212
221
|
'explorer.renamePrompt': '重命名为(新名字)',
|
|
222
|
+
'explorer.renameTitle': '重命名',
|
|
223
|
+
'explorer.mkdirTitle': '新建目录',
|
|
224
|
+
'explorer.namePlaceholder': '名称',
|
|
213
225
|
'explorer.deleteConfirm': '确认删除 {path}?',
|
|
214
226
|
'explorer.copied': '已复制',
|
|
215
227
|
'explorer.tabTitle': '远程文件',
|
|
@@ -226,6 +238,8 @@ window.__ModuleLoader__.load({
|
|
|
226
238
|
'file.empty': '(空文件)',
|
|
227
239
|
'file.truncated': '… 内容已截断(只读远程查看,完整文件请用 rw_read_file / 下载)',
|
|
228
240
|
'file.conflict': '远端文件已变化,请重新读取',
|
|
241
|
+
'file.dirty': '未保存',
|
|
242
|
+
'file.saveHint': 'Ctrl/⌘+S 保存',
|
|
229
243
|
'file.downloaded': '已下载到本地镜像{local}',
|
|
230
244
|
'file.downloadedAt': ': {path}',
|
|
231
245
|
'file.readFail': '远程读取失败: {msg}',
|
|
@@ -294,6 +308,12 @@ window.__ModuleLoader__.load({
|
|
|
294
308
|
'settings.fwdAdd': 'Add forward',
|
|
295
309
|
'settings.audit': 'Recent remote commands (audit log)',
|
|
296
310
|
'settings.auditEmpty': 'No records yet (commands run remotely are logged to $DSH_HOME/remote-workspaces/audit.log)',
|
|
311
|
+
'settings.auditRefresh': 'Refresh log',
|
|
312
|
+
'settings.badgePwd': 'password',
|
|
313
|
+
'settings.badgeKeychain': 'keychain',
|
|
314
|
+
'settings.badgeProxy': 'jump',
|
|
315
|
+
'settings.sectionHide': 'Collapse section',
|
|
316
|
+
'settings.sectionShow': 'Expand section',
|
|
297
317
|
'settings.update': 'Update',
|
|
298
318
|
'settings.updateChecking': 'Loading version info…',
|
|
299
319
|
'settings.checking': 'Checking…',
|
|
@@ -388,6 +408,9 @@ window.__ModuleLoader__.load({
|
|
|
388
408
|
'picker.chooseDrive': ' choose a drive',
|
|
389
409
|
'picker.homeBtn': '🏠',
|
|
390
410
|
'picker.goHome': 'Go to home',
|
|
411
|
+
'picker.hintKeys': 'Enter confirms · Esc closes · Ctrl/⌘+Enter sets workspace',
|
|
412
|
+
'picker.star': '⭐ Star dsh-remote',
|
|
413
|
+
'picker.feedback': 'Feedback / report issue',
|
|
391
414
|
'explorer.refresh': 'Refresh',
|
|
392
415
|
'explorer.newDir': 'New directory',
|
|
393
416
|
'explorer.more': '…',
|
|
@@ -410,6 +433,9 @@ window.__ModuleLoader__.load({
|
|
|
410
433
|
'explorer.menuDelete': '🗑 Delete',
|
|
411
434
|
'explorer.menuDeleteDir': '🗑 Delete (with contents)',
|
|
412
435
|
'explorer.renamePrompt': 'Rename to (new name)',
|
|
436
|
+
'explorer.renameTitle': 'Rename',
|
|
437
|
+
'explorer.mkdirTitle': 'New directory',
|
|
438
|
+
'explorer.namePlaceholder': 'Name',
|
|
413
439
|
'explorer.deleteConfirm': 'Delete {path}?',
|
|
414
440
|
'explorer.copied': 'Copied',
|
|
415
441
|
'explorer.tabTitle': 'Remote Files',
|
|
@@ -426,6 +452,8 @@ window.__ModuleLoader__.load({
|
|
|
426
452
|
'file.empty': ' (empty file)',
|
|
427
453
|
'file.truncated': '… content truncated (read-only remote view; use rw_read_file / Download for the full file)',
|
|
428
454
|
'file.conflict': 'The remote file changed, please re-read it',
|
|
455
|
+
'file.dirty': 'unsaved',
|
|
456
|
+
'file.saveHint': 'Ctrl/⌘+S to save',
|
|
429
457
|
'file.downloaded': 'Downloaded to local mirror{local}',
|
|
430
458
|
'file.downloadedAt': ': {path}',
|
|
431
459
|
'file.readFail': 'Failed to read remote: {msg}',
|
|
@@ -525,6 +553,19 @@ window.__ModuleLoader__.load({
|
|
|
525
553
|
? '/api' + path : path
|
|
526
554
|
}
|
|
527
555
|
|
|
556
|
+
function sessionQuery(sessionId) {
|
|
557
|
+
return sessionId ? 'sessionId=' + encodeURIComponent(sessionId) : ''
|
|
558
|
+
}
|
|
559
|
+
function withSessionQuery(path, sessionId) {
|
|
560
|
+
const q = sessionQuery(sessionId)
|
|
561
|
+
if (!q) return path
|
|
562
|
+
return path + (path.includes('?') ? '&' : '?') + q
|
|
563
|
+
}
|
|
564
|
+
function withSessionBody(body, sessionId) {
|
|
565
|
+
if (!sessionId) return body
|
|
566
|
+
return Object.assign({}, body || {}, { sessionId })
|
|
567
|
+
}
|
|
568
|
+
|
|
528
569
|
async function api(method, path, body) {
|
|
529
570
|
const opts = { method, headers: {} }
|
|
530
571
|
if (body) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body) }
|
|
@@ -561,13 +602,83 @@ window.__ModuleLoader__.load({
|
|
|
561
602
|
label: v('--dsw-alias-label-primary', '#e4e4e7'),
|
|
562
603
|
primary: v('--dsw-alias-button-primary-fill', '#2563eb'),
|
|
563
604
|
onPrimary: v('--dsw-alias-button-contrast-fill', '#fff'),
|
|
564
|
-
|
|
605
|
+
// Hover tints are a neutral translucent gray rather than a theme token:
|
|
606
|
+
// the harness's own interactive-bg-hover is a 6% tint that disappears on
|
|
607
|
+
// the settings surface, while a gray overlay darkens light themes and
|
|
608
|
+
// lightens dark ones.
|
|
609
|
+
hoverBg: 'rgba(128,128,128,0.14)',
|
|
565
610
|
}
|
|
566
611
|
const inputS = { flex: 1, padding: '6px 10px', borderRadius: T.radius, border: '1px solid ' + T.border, background: T.bg, color: T.label, outline: 'none' }
|
|
567
612
|
const buttonS = { padding: '6px 12px', borderRadius: T.radius, border: '1px solid ' + T.border, background: T.bg, color: T.label, cursor: 'pointer' }
|
|
568
613
|
const primaryBtnS = { padding: '6px 12px', borderRadius: T.radius, border: 'none', background: T.primary, color: T.onPrimary, cursor: 'pointer', fontWeight: 600 }
|
|
569
|
-
const box = { border: '1px solid ' + T.border, borderRadius:
|
|
614
|
+
const box = { border: '1px solid ' + T.border, borderRadius: 10, background: T.bg, padding: 12 }
|
|
570
615
|
const boxS = box
|
|
616
|
+
const LABEL_W = 108
|
|
617
|
+
const CHROME_CSS_ID = 'dsh-remote-chrome'
|
|
618
|
+
function ensureChromeCss() {
|
|
619
|
+
try {
|
|
620
|
+
if (typeof document === 'undefined' || !document.head || typeof document.getElementById !== 'function') return
|
|
621
|
+
if (document.getElementById(CHROME_CSS_ID)) return
|
|
622
|
+
const el = document.createElement('style')
|
|
623
|
+
el.id = CHROME_CSS_ID
|
|
624
|
+
el.textContent = [
|
|
625
|
+
'.dsh-rw-btn{transition:background .12s ease,opacity .12s ease,border-color .12s ease}',
|
|
626
|
+
'.dsh-rw-btn:hover:not(:disabled){background:rgba(128,128,128,.18)}',
|
|
627
|
+
'.dsh-rw-btn:focus-visible{outline:2px solid var(--dsw-alias-button-primary-fill,#2563eb);outline-offset:1px}',
|
|
628
|
+
'.dsh-rw-btn:disabled{opacity:.45;cursor:not-allowed}',
|
|
629
|
+
// A filled primary keeps its own fill on hover; opacity is the only
|
|
630
|
+
// cue that stays visible whether that fill is near-black or near-white.
|
|
631
|
+
'.dsh-rw-primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-fill,#2563eb);opacity:.85}',
|
|
632
|
+
'.dsh-rw-input{transition:border-color .12s ease,box-shadow .12s ease}',
|
|
633
|
+
'.dsh-rw-input:focus{border-color:var(--dsw-alias-button-primary-fill,#2563eb)!important;box-shadow:0 0 0 2px color-mix(in srgb,var(--dsw-alias-button-primary-fill,#2563eb) 28%,transparent)}',
|
|
634
|
+
'.dsh-rw-row{border-radius:8px;margin:0 -6px;padding-left:8px;padding-right:8px;transition:background .12s ease}',
|
|
635
|
+
'.dsh-rw-row:hover{background:rgba(128,128,128,.14)}',
|
|
636
|
+
'.dsh-rw-list-item{transition:background .1s ease}',
|
|
637
|
+
'.dsh-rw-list-item:hover{background:rgba(128,128,128,.18)}',
|
|
638
|
+
'.dsh-rw-tabs{display:inline-flex;padding:3px;gap:2px;border-radius:10px;background:rgba(128,128,128,.14);border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.35))}',
|
|
639
|
+
'.dsh-rw-tab{border:none!important;background:transparent!important}',
|
|
640
|
+
'.dsh-rw-tab.is-active{background:var(--dsw-alias-button-primary-fill,#2563eb)!important;color:var(--dsw-alias-button-contrast-fill,#fff)!important;font-weight:600}',
|
|
641
|
+
'.dsh-rw-pill{display:inline-flex;align-items:center;font-size:11px;line-height:1.3;padding:1px 7px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.35));opacity:.9;white-space:nowrap}',
|
|
642
|
+
'.dsh-rw-pill.is-ok{color:var(--dsw-static-green-500,#4caf7d);border-color:currentColor}',
|
|
643
|
+
'.dsh-rw-section-btn{width:100%;text-align:left;display:flex;align-items:center;gap:8px;background:transparent;border:none;cursor:pointer;padding:0;color:inherit}',
|
|
644
|
+
'@keyframes dsh-rw-in{from{opacity:0;transform:translateY(6px) scale(.98)}to{opacity:1;transform:none}}',
|
|
645
|
+
'.dsh-rw-modal-panel{animation:dsh-rw-in .14s ease-out}',
|
|
646
|
+
].join('')
|
|
647
|
+
document.head.appendChild(el)
|
|
648
|
+
} catch { /* vm tests / no DOM */ }
|
|
649
|
+
}
|
|
650
|
+
function PromptDialog(props) {
|
|
651
|
+
const { title, initial, placeholder, onSubmit, onCancel } = props
|
|
652
|
+
const [val, setVal] = React.useState(initial || '')
|
|
653
|
+
React.useEffect(() => {
|
|
654
|
+
try {
|
|
655
|
+
if (typeof document === 'undefined') return
|
|
656
|
+
const onKey = (e) => {
|
|
657
|
+
if (e.key === 'Escape') { e.preventDefault(); onCancel() }
|
|
658
|
+
if (e.key === 'Enter') { e.preventDefault(); const t = val.trim(); if (t) onSubmit(t) }
|
|
659
|
+
}
|
|
660
|
+
document.addEventListener('keydown', onKey)
|
|
661
|
+
return () => document.removeEventListener('keydown', onKey)
|
|
662
|
+
} catch { return undefined }
|
|
663
|
+
}, [val, onSubmit, onCancel])
|
|
664
|
+
return React.createElement('div', {
|
|
665
|
+
style: { position: 'fixed', inset: 0, zIndex: 1000000, background: 'rgba(0,0,0,0.45)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 },
|
|
666
|
+
onClick: onCancel,
|
|
667
|
+
},
|
|
668
|
+
React.createElement('div', {
|
|
669
|
+
className: 'dsh-rw-modal-panel',
|
|
670
|
+
style: { background: v('--dsw-alias-bg-overlay', '#1e1e1e'), border: '1px solid ' + T.borderStrong, borderRadius: 10, padding: 14, width: 'min(420px, 92vw)', display: 'flex', flexDirection: 'column', gap: 10, boxShadow: '0 12px 40px rgba(0,0,0,0.4)' },
|
|
671
|
+
onClick: (e) => e.stopPropagation(),
|
|
672
|
+
},
|
|
673
|
+
React.createElement('div', { style: { fontSize: 13, fontWeight: 600, color: T.label } }, title),
|
|
674
|
+
React.createElement('input', { autoFocus: true, value: val, onChange: (e) => setVal(e.target.value), placeholder: placeholder || '', className: 'dsh-rw-input', style: inputS }),
|
|
675
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end' } },
|
|
676
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: buttonS, onClick: onCancel }, tr('file.cancel')),
|
|
677
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: primaryBtnS, onClick: () => { const t = val.trim(); if (t) onSubmit(t) }, disabled: !val.trim() }, tr('common.confirm')),
|
|
678
|
+
),
|
|
679
|
+
),
|
|
680
|
+
)
|
|
681
|
+
}
|
|
571
682
|
|
|
572
683
|
function humanSize(n) {
|
|
573
684
|
if (n == null) return ''
|
|
@@ -629,6 +740,9 @@ window.__ModuleLoader__.load({
|
|
|
629
740
|
const [sshEntries, setSshEntries] = React.useState(null)
|
|
630
741
|
const [workspaces, setWorkspaces] = React.useState([])
|
|
631
742
|
const [advOpen, setAdvOpen] = React.useState(false) // 高级配置折叠
|
|
743
|
+
const [fwdOpen, setFwdOpen] = React.useState(true)
|
|
744
|
+
const [auditOpen, setAuditOpen] = React.useState(false)
|
|
745
|
+
React.useEffect(() => { ensureChromeCss() }, [])
|
|
632
746
|
|
|
633
747
|
const testConn = () => {
|
|
634
748
|
if (!form.host.trim() || tst === 'testing') return
|
|
@@ -771,52 +885,66 @@ window.__ModuleLoader__.load({
|
|
|
771
885
|
.catch((e) => setErr(String((e && e.message) || e)))
|
|
772
886
|
}
|
|
773
887
|
|
|
774
|
-
const row = (label, ctrl, k) => React.createElement('div', { key: k, style: { display: 'flex', gap:
|
|
775
|
-
React.createElement('label', { style: { width:
|
|
888
|
+
const row = (label, ctrl, k) => React.createElement('div', { key: k, style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 8 } },
|
|
889
|
+
React.createElement('label', { style: { width: LABEL_W, fontSize: 12, opacity: 0.8, flexShrink: 0 } }, label), ctrl)
|
|
890
|
+
const pill = (cls, text) => React.createElement('span', { className: 'dsh-rw-pill' + (cls ? ' ' + cls : ''), style: { marginLeft: 4 } }, text)
|
|
891
|
+
const sectionToggle = (open, setOpen, title, extra) => React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: open ? 8 : 0 } },
|
|
892
|
+
React.createElement('button', {
|
|
893
|
+
type: 'button', className: 'dsh-rw-section-btn',
|
|
894
|
+
title: open ? tr('settings.sectionHide') : tr('settings.sectionShow'),
|
|
895
|
+
onClick: () => setOpen(!open),
|
|
896
|
+
style: { fontSize: 13, fontWeight: 600 },
|
|
897
|
+
}, React.createElement('span', { style: { opacity: 0.7, width: 12 } }, open ? '▾' : '▸'), title),
|
|
898
|
+
extra || null,
|
|
899
|
+
)
|
|
776
900
|
|
|
777
901
|
return React.createElement('div', { style: { padding: 16, display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 860 } },
|
|
778
|
-
React.createElement('div', { style: { fontSize:
|
|
779
|
-
React.createElement('div', { style: { fontSize: 12, opacity: 0.8 } },
|
|
902
|
+
React.createElement('div', { style: { fontSize: 16, fontWeight: 650, letterSpacing: '-0.01em' } }, tr('settings.title')),
|
|
903
|
+
React.createElement('div', { style: { fontSize: 12, opacity: 0.8, lineHeight: 1.55 } },
|
|
780
904
|
tr('settings.intro')),
|
|
781
905
|
React.createElement('div', { style: boxS },
|
|
782
|
-
React.createElement('div', { style: { marginBottom:
|
|
906
|
+
React.createElement('div', { style: { marginBottom: 8, fontSize: 13, fontWeight: 600 } }, tr('settings.machines')),
|
|
783
907
|
machines.length
|
|
784
|
-
? machines.map((m) => React.createElement('div', { key: m.id, style: { display: 'flex', gap: 8, alignItems: 'center',
|
|
785
|
-
React.createElement('div', { style: { flex: 1, fontSize: 13 } },
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
React.createElement('
|
|
908
|
+
? machines.map((m) => React.createElement('div', { key: m.id, className: 'dsh-rw-row', style: { display: 'flex', gap: 8, alignItems: 'center', paddingTop: 8, paddingBottom: 8, borderBottom: '1px solid ' + T.border, flexWrap: 'wrap' } },
|
|
909
|
+
React.createElement('div', { style: { flex: 1, fontSize: 13, minWidth: 180 } },
|
|
910
|
+
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' } },
|
|
911
|
+
React.createElement('span', { style: { fontWeight: 600 } }, m.name),
|
|
912
|
+
React.createElement('code', { style: { fontSize: 12, opacity: 0.8 } }, m.username + '@' + m.host + ':' + m.port),
|
|
913
|
+
m.id === currentId ? pill('is-ok', tr('settings.current')) : null,
|
|
914
|
+
m.passwordSet ? pill('', tr('settings.badgePwd')) : null,
|
|
915
|
+
(m.credentialBackend && m.credentialBackend !== 'plain') ? pill('', tr('settings.badgeKeychain')) : null,
|
|
916
|
+
m.proxy && m.proxy.host ? pill('', tr('settings.badgeProxy')) : null,
|
|
917
|
+
m.latencyMs != null ? React.createElement('span', { style: { fontSize: 11, opacity: 0.6 } }, m.latencyMs + 'ms') : null,
|
|
918
|
+
)),
|
|
919
|
+
React.createElement('div', { style: { display: 'flex', gap: 6, flexShrink: 0 } },
|
|
920
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: buttonS, onClick: () => startEdit(m) }, tr('settings.edit')),
|
|
921
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: buttonS, onClick: () => del(m.id) }, tr('settings.delete')),
|
|
922
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn' + (m.id === currentId ? '' : ' dsh-rw-primary'), style: { ...(m.id === currentId ? buttonS : primaryBtnS), padding: '6px 10px', whiteSpace: 'nowrap' }, onClick: () => useNow(m.id), disabled: m.id === currentId }, tr('settings.setCurrent')))))
|
|
796
923
|
: React.createElement('div', { style: { opacity: 0.6, fontSize: 12 } }, tr('settings.noMachines')),
|
|
797
924
|
currentId
|
|
798
|
-
? React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', paddingTop:
|
|
925
|
+
? React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', paddingTop: 10, fontSize: 12, color: T.ok } },
|
|
799
926
|
tr('settings.currentMachine', { name: ((machines.find((m) => m.id === currentId) || {}).name || currentId) }),
|
|
800
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 8px', fontSize: 12, marginLeft: 'auto' }, onClick: clearCurrent }, tr('settings.clearCurrent')))
|
|
801
|
-
: React.createElement('div', { style: { paddingTop:
|
|
927
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 8px', fontSize: 12, marginLeft: 'auto' }, onClick: clearCurrent }, tr('settings.clearCurrent')))
|
|
928
|
+
: React.createElement('div', { style: { paddingTop: 10, fontSize: 12, opacity: 0.6 } }, tr('settings.noActive')),
|
|
802
929
|
),
|
|
803
930
|
React.createElement('div', { style: boxS },
|
|
804
931
|
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 } },
|
|
805
932
|
React.createElement('div', { style: { fontSize: 13, fontWeight: 600 } }, form.id ? tr('settings.editMachine') : tr('settings.addMachine')),
|
|
806
|
-
React.createElement('button', { style: { ...buttonS, fontSize: 12, padding: '3px 8px' }, onClick: () => (sshEntries ? setSshEntries(null) : importSsh()) }, sshEntries ? tr('settings.sshCollapse') : tr('settings.sshImport'))),
|
|
933
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, fontSize: 12, padding: '3px 8px' }, onClick: () => (sshEntries ? setSshEntries(null) : importSsh()) }, sshEntries ? tr('settings.sshCollapse') : tr('settings.sshImport'))),
|
|
807
934
|
sshEntries ? React.createElement('div', { style: { marginBottom: 8, maxHeight: 160, overflowY: 'auto', border: '1px solid ' + T.border, borderRadius: T.radius } },
|
|
808
|
-
sshEntries.length ? sshEntries.map((e) => React.createElement('div', { key: e.host, onClick: () => applySshEntry(e), style: { padding: '6px 10px', cursor: 'pointer', fontSize: 12, borderBottom: '1px solid ' + T.border } },
|
|
935
|
+
sshEntries.length ? sshEntries.map((e) => React.createElement('div', { key: e.host, className: 'dsh-rw-list-item', onClick: () => applySshEntry(e), style: { padding: '6px 10px', cursor: 'pointer', fontSize: 12, borderBottom: '1px solid ' + T.border } },
|
|
809
936
|
e.host + ' → ' + (e.hostName || '?') + (e.user ? ' (' + e.user + ')' : '') + (e.identityFile ? ' [key]' : '') + (e.proxyJump ? ' ⛳' : '')))
|
|
810
937
|
: React.createElement('div', { style: { padding: 8, fontSize: 12, opacity: 0.6 } }, tr('settings.sshNoEntries')))
|
|
811
938
|
: null,
|
|
812
|
-
row(tr('settings.name'), React.createElement('input', { value: form.name, onChange: setF('name'), placeholder: tr('settings.name') + ' (e.g. build-server)', style: inputS }), 'n'),
|
|
813
|
-
row(tr('settings.host'), React.createElement('input', { value: form.host, onChange: setF('host'), placeholder: 'IP or hostname', style: inputS }), 'h'),
|
|
814
|
-
row(tr('settings.port'), React.createElement('input', { value: form.port, onChange: setF('port'), placeholder: '22', style: { ...inputS, width: 70 } }), 'p'),
|
|
815
|
-
row(tr('settings.user'), React.createElement('input', { value: form.username, onChange: setF('username'), placeholder: 'root', style: inputS }), 'u'),
|
|
816
|
-
row(tr('settings.password'), React.createElement('input', { type: 'password', value: form.password, onChange: setF('password'), placeholder: 'SSH password (hidden)', style: inputS }), 'w'),
|
|
939
|
+
row(tr('settings.name'), React.createElement('input', { className: 'dsh-rw-input', value: form.name, onChange: setF('name'), placeholder: tr('settings.name') + ' (e.g. build-server)', style: inputS }), 'n'),
|
|
940
|
+
row(tr('settings.host'), React.createElement('input', { className: 'dsh-rw-input', value: form.host, onChange: setF('host'), placeholder: 'IP or hostname', style: inputS }), 'h'),
|
|
941
|
+
row(tr('settings.port'), React.createElement('input', { className: 'dsh-rw-input', value: form.port, onChange: setF('port'), placeholder: '22', style: { ...inputS, width: 70 } }), 'p'),
|
|
942
|
+
row(tr('settings.user'), React.createElement('input', { className: 'dsh-rw-input', value: form.username, onChange: setF('username'), placeholder: 'root', style: inputS }), 'u'),
|
|
943
|
+
row(tr('settings.password'), React.createElement('input', { className: 'dsh-rw-input', type: 'password', value: form.password, onChange: setF('password'), placeholder: 'SSH password (hidden)', style: inputS }), 'w'),
|
|
817
944
|
// 高级配置折叠开关
|
|
818
945
|
React.createElement('button', {
|
|
819
946
|
type: 'button',
|
|
947
|
+
className: 'dsh-rw-btn',
|
|
820
948
|
onClick: () => setAdvOpen(!advOpen),
|
|
821
949
|
style: { ...buttonS, width: '100%', marginBottom: advOpen ? 10 : 0, color: advOpen ? T.primary : 'inherit', fontWeight: advOpen ? 600 : 400, borderStyle: 'dashed' },
|
|
822
950
|
}, advOpen ? tr('settings.advCollapse') : tr('settings.advExpand')),
|
|
@@ -829,7 +957,7 @@ window.__ModuleLoader__.load({
|
|
|
829
957
|
React.createElement('option', { value: 'accept-new' }, tr('settings.hkAccept')),
|
|
830
958
|
React.createElement('option', { value: 'verify' }, tr('settings.hkVerify')),
|
|
831
959
|
React.createElement('option', { value: 'off' }, tr('settings.hkOff'))), 'hkm'),
|
|
832
|
-
React.createElement('div', { style: { display: 'flex', gap: 16, paddingLeft:
|
|
960
|
+
React.createElement('div', { style: { display: 'flex', gap: 16, paddingLeft: LABEL_W + 8, flexWrap: 'wrap', marginBottom: 8 } },
|
|
833
961
|
React.createElement('label', { style: { fontSize: 12, display: 'flex', gap: 4, alignItems: 'center' } },
|
|
834
962
|
React.createElement('input', { type: 'checkbox', checked: form.useAgent, onChange: setFb('useAgent') }), tr('settings.agent')),
|
|
835
963
|
React.createElement('label', { style: { fontSize: 12, display: 'flex', gap: 4, alignItems: 'center' } },
|
|
@@ -843,13 +971,13 @@ window.__ModuleLoader__.load({
|
|
|
843
971
|
React.createElement('input', { type: 'password', value: form.proxyPassword, onChange: setF('proxyPassword'), placeholder: 'Jump password', style: { ...inputS, minWidth: 140 } })), 'pu'),
|
|
844
972
|
row(tr('settings.proxyKey'), React.createElement('input', { value: form.proxyKey, onChange: setF('proxyKey'), placeholder: 'Jump private key path (optional)', style: inputS }), 'pk'),
|
|
845
973
|
) : null,
|
|
846
|
-
React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end', alignItems: 'center', flexWrap: 'wrap', marginTop:
|
|
974
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end', alignItems: 'center', flexWrap: 'wrap', marginTop: 8 } },
|
|
847
975
|
msg ? React.createElement('span', { style: { color: T.ok, fontSize: 12, marginRight: 'auto' } }, msg) : null,
|
|
848
|
-
form.id ? React.createElement('button', { style: buttonS, onClick: () => save('delete') }, tr('settings.delete')) : null,
|
|
849
|
-
React.createElement('button', { style: buttonS, onClick: () => { clearForm(); setErr(''); setTst('idle'); setAdvOpen(false) } }, form.id ? tr('settings.cancelEdit') : tr('settings.clear')),
|
|
850
|
-
React.createElement('button', { style: { ...buttonS, fontFamily: 'monospace', whiteSpace: 'nowrap' }, onClick: testConn, disabled: busy || !form.host.trim() || tst === 'testing' },
|
|
976
|
+
form.id ? React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: buttonS, onClick: () => save('delete') }, tr('settings.delete')) : null,
|
|
977
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: buttonS, onClick: () => { clearForm(); setErr(''); setTst('idle'); setAdvOpen(false) } }, form.id ? tr('settings.cancelEdit') : tr('settings.clear')),
|
|
978
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, fontFamily: 'monospace', whiteSpace: 'nowrap' }, onClick: testConn, disabled: busy || !form.host.trim() || tst === 'testing' },
|
|
851
979
|
tst === 'testing' ? tr('settings.testing') : tst === 'ok' ? tr('settings.testOk') : tr('settings.testConn')),
|
|
852
|
-
React.createElement('button', {
|
|
980
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: primaryBtnS, onClick: () => save(form.id ? 'update' : 'add'), disabled: busy || !form.host.trim() }, busy ? tr('settings.saving') : tr('settings.save')),
|
|
853
981
|
),
|
|
854
982
|
),
|
|
855
983
|
// 快捷切换工作区(该机器的最近工作区)
|
|
@@ -857,35 +985,40 @@ window.__ModuleLoader__.load({
|
|
|
857
985
|
React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 600 } }, tr('settings.recentWorkspaces')),
|
|
858
986
|
workspaces.map((w, i) => React.createElement('div', { key: w + i, style: { display: 'flex', gap: 8, alignItems: 'center', padding: '4px 0' } },
|
|
859
987
|
React.createElement('code', { style: { flex: 1, fontSize: 12, fontFamily: 'monospace' } }, w),
|
|
860
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 10px' }, onClick: () => setWorkspaceNow(w) }, tr('settings.switch'))))) : null,
|
|
988
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 10px' }, onClick: () => setWorkspaceNow(w) }, tr('settings.switch'))))) : null,
|
|
861
989
|
// 端口转发面板
|
|
862
990
|
React.createElement('div', { style: boxS },
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
React.createElement('
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
React.createElement('
|
|
873
|
-
React.createElement('
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
React.createElement('
|
|
880
|
-
|
|
991
|
+
sectionToggle(fwdOpen, setFwdOpen, tr('settings.forwards')),
|
|
992
|
+
fwdOpen ? React.createElement(React.Fragment, null,
|
|
993
|
+
forwards.length ? forwards.map((f) => React.createElement('div', { key: f.id, className: 'dsh-rw-row', style: { display: 'flex', gap: 8, alignItems: 'center', padding: '6px 0', fontSize: 12, fontFamily: 'monospace' } },
|
|
994
|
+
React.createElement('span', { style: { color: f.active ? T.ok : T.muted } }, f.active ? '●' : '○'),
|
|
995
|
+
React.createElement('span', { style: { flex: 1 } }, `${f.direction} 127.0.0.1:${f.listenPort} → ${f.targetHost}:${f.targetPort}${f.autoStart ? ' [auto]' : ''}`),
|
|
996
|
+
f.active
|
|
997
|
+
? React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 8px' }, onClick: () => fwdAction('stop', f.id) }, tr('settings.fwdStop'))
|
|
998
|
+
: React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 8px' }, onClick: () => fwdAction('start', f.id) }, tr('settings.fwdStart')),
|
|
999
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 8px' }, onClick: () => fwdAction('remove', f.id) }, tr('settings.fwdRemove')))) : null,
|
|
1000
|
+
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', marginTop: 6, flexWrap: 'wrap' } },
|
|
1001
|
+
React.createElement('select', { className: 'dsh-rw-input', value: fwdForm.direction, onChange: (e) => setFwdForm({ ...fwdForm, direction: e.target.value }), style: { ...inputS, maxWidth: 110 } },
|
|
1002
|
+
React.createElement('option', { value: 'local' }, tr('settings.fwdDirLocal')),
|
|
1003
|
+
React.createElement('option', { value: 'reverse' }, tr('settings.fwdDirReverse'))),
|
|
1004
|
+
React.createElement('input', { className: 'dsh-rw-input', value: fwdForm.listenPort, onChange: (e) => setFwdForm({ ...fwdForm, listenPort: e.target.value }), placeholder: tr('settings.fwdListen'), style: { ...inputS, maxWidth: 90 } }),
|
|
1005
|
+
React.createElement('input', { className: 'dsh-rw-input', value: fwdForm.targetHost, onChange: (e) => setFwdForm({ ...fwdForm, targetHost: e.target.value }), placeholder: tr('settings.fwdTargetHost'), style: { ...inputS, maxWidth: 120 } }),
|
|
1006
|
+
React.createElement('input', { className: 'dsh-rw-input', value: fwdForm.targetPort, onChange: (e) => setFwdForm({ ...fwdForm, targetPort: e.target.value }), placeholder: tr('settings.fwdTargetPort'), style: { ...inputS, maxWidth: 90 } }),
|
|
1007
|
+
React.createElement('label', { style: { fontSize: 12, display: 'flex', gap: 4, alignItems: 'center' } },
|
|
1008
|
+
React.createElement('input', { type: 'checkbox', checked: fwdForm.autoStart, onChange: (e) => setFwdForm({ ...fwdForm, autoStart: e.target.checked }) }), tr('settings.fwdAuto')),
|
|
1009
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: primaryBtnS, onClick: fwdDefine, disabled: !fwdForm.listenPort }, tr('settings.fwdAdd'))),
|
|
1010
|
+
) : null,
|
|
881
1011
|
),
|
|
882
1012
|
// 审计日志
|
|
883
1013
|
React.createElement('div', { style: boxS },
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1014
|
+
sectionToggle(auditOpen, setAuditOpen, tr('settings.audit'),
|
|
1015
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 8px', fontSize: 12, marginLeft: 'auto' }, onClick: (e) => { e.stopPropagation(); api('GET', '/dsh-remote/audit?limit=30').then((r) => setAuditLines(r.lines || [])).catch(() => {}) } }, tr('settings.auditRefresh'))),
|
|
1016
|
+
auditOpen
|
|
1017
|
+
? (auditLines.length
|
|
1018
|
+
? React.createElement('div', { style: { maxHeight: 180, overflowY: 'auto', fontSize: 11, fontFamily: 'monospace', lineHeight: 1.6 } },
|
|
1019
|
+
auditLines.map((l, i) => React.createElement('div', { key: i, style: { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, l)))
|
|
1020
|
+
: React.createElement('div', { style: { opacity: 0.6, fontSize: 12 } }, tr('settings.auditEmpty')))
|
|
1021
|
+
: React.createElement('div', { style: { fontSize: 11, opacity: 0.55 } }, String(auditLines.length)),
|
|
889
1022
|
),
|
|
890
1023
|
err ? React.createElement('div', { style: { color: T.danger, fontSize: 13 } }, err) : null,
|
|
891
1024
|
React.createElement('div', { style: boxS },
|
|
@@ -896,9 +1029,9 @@ window.__ModuleLoader__.load({
|
|
|
896
1029
|
'v' + upd.current,
|
|
897
1030
|
upd.updateAvailable ? React.createElement('b', { style: { color: T.ok, marginLeft: 6 } }, '→ v' + upd.latest) : null)
|
|
898
1031
|
: React.createElement('span', { style: { opacity: 0.6 } }, tr('settings.updateChecking')),
|
|
899
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 10px' }, onClick: () => checkUpdate(false), disabled: updBusy }, updBusy ? tr('settings.checking') : tr('settings.checkUpdate')),
|
|
1032
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 10px' }, onClick: () => checkUpdate(false), disabled: updBusy }, updBusy ? tr('settings.checking') : tr('settings.checkUpdate')),
|
|
900
1033
|
upd && upd.updateAvailable
|
|
901
|
-
? React.createElement('button', {
|
|
1034
|
+
? React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: { ...primaryBtnS, padding: '2px 10px' }, onClick: applyUpdateNow, disabled: updBusy }, tr('settings.updateNow'))
|
|
902
1035
|
: null,
|
|
903
1036
|
),
|
|
904
1037
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginTop: 6, fontSize: 12 } },
|
|
@@ -993,6 +1126,7 @@ window.__ModuleLoader__.load({
|
|
|
993
1126
|
// 本机浏览浮层(browse 后端):{ path, home, crumbs, entries, truncated }。
|
|
994
1127
|
const [localPop, setLocalPop] = React.useState(null)
|
|
995
1128
|
const [newDirName, setNewDirName] = React.useState('')
|
|
1129
|
+
const [mkdirPrompt, setMkdirPrompt] = React.useState(false)
|
|
996
1130
|
const suggestTimer = React.useRef(null)
|
|
997
1131
|
const suggestRef = React.useRef(null)
|
|
998
1132
|
React.useEffect(() => {
|
|
@@ -1009,6 +1143,7 @@ window.__ModuleLoader__.load({
|
|
|
1009
1143
|
document.addEventListener('mousedown', onDown)
|
|
1010
1144
|
return () => document.removeEventListener('mousedown', onDown)
|
|
1011
1145
|
}, [mOpen])
|
|
1146
|
+
React.useEffect(() => { if (open) ensureChromeCss() }, [open])
|
|
1012
1147
|
|
|
1013
1148
|
const sortItems = (list) => {
|
|
1014
1149
|
const dirs = list.filter((it) => it.dir)
|
|
@@ -1235,10 +1370,16 @@ window.__ModuleLoader__.load({
|
|
|
1235
1370
|
const mkdirHere = () => {
|
|
1236
1371
|
const last = levels && levels.length ? levels[levels.length - 1] : null
|
|
1237
1372
|
const base = (last && last.path) || '/'
|
|
1238
|
-
|
|
1239
|
-
|
|
1373
|
+
setMkdirPrompt({ base })
|
|
1374
|
+
}
|
|
1375
|
+
const confirmMkdirHere = (nm) => {
|
|
1376
|
+
const last = levels && levels.length ? levels[levels.length - 1] : null
|
|
1377
|
+
const base = (mkdirPrompt && mkdirPrompt.base) || (last && last.path) || '/'
|
|
1378
|
+
setMkdirPrompt(false)
|
|
1379
|
+
const name = String(nm || '').trim()
|
|
1380
|
+
if (!name) return
|
|
1240
1381
|
const sep = base.includes('\\') ? '\\' : '/'
|
|
1241
|
-
const full = base === '/' ? '/' +
|
|
1382
|
+
const full = base === '/' ? '/' + name : base + sep + name
|
|
1242
1383
|
api('POST', '/dsh-remote/fs', { op: 'mkdir', path: full })
|
|
1243
1384
|
.then(() => { setErr(''); loadLevels(machineId, base, (levels ? levels.length - 1 : 0)) })
|
|
1244
1385
|
.catch((e) => setErr(String((e && e.message) || e)))
|
|
@@ -1297,6 +1438,7 @@ window.__ModuleLoader__.load({
|
|
|
1297
1438
|
: (cur.entries.length ? cur.entries.slice(0, 400).map((it, i) => React.createElement('div', {
|
|
1298
1439
|
key: it.path || i, title: tr('picker.enterDir', { name: it.name }),
|
|
1299
1440
|
onClick: () => loadLocal(it.path),
|
|
1441
|
+
className: 'dsh-rw-list-item',
|
|
1300
1442
|
style: { padding: '7px 12px', cursor: 'pointer', color: T.label, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', display: 'flex', gap: 8, alignItems: 'center', fontSize: 13, borderBottom: '1px solid ' + T.border },
|
|
1301
1443
|
},
|
|
1302
1444
|
React.createElement('span', null, '📁'),
|
|
@@ -1306,10 +1448,10 @@ window.__ModuleLoader__.load({
|
|
|
1306
1448
|
),
|
|
1307
1449
|
err ? React.createElement('div', { style: { color: T.danger, fontSize: 12, padding: '4px 12px' } }, err) : null,
|
|
1308
1450
|
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', padding: '8px 12px', borderTop: '1px solid ' + T.border } },
|
|
1309
|
-
React.createElement('input', { value: newDirName, onChange: (e) => setNewDirName(e.target.value), placeholder: tr('picker.newFolder'), style: { ...inputS, width: 110, flex: '0 0 auto', padding: '4px 8px', fontSize: 12 } }),
|
|
1310
|
-
React.createElement('button', { style: { ...buttonS, padding: '4px 10px', whiteSpace: 'nowrap' }, onClick: mkdirLocal, disabled: loading || !newDirName.trim() || !cur.path }, tr('picker.create')),
|
|
1451
|
+
React.createElement('input', { className: 'dsh-rw-input', value: newDirName, onChange: (e) => setNewDirName(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter') mkdirLocal() }, placeholder: tr('picker.newFolder'), style: { ...inputS, width: 110, flex: '0 0 auto', padding: '4px 8px', fontSize: 12 } }),
|
|
1452
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '4px 10px', whiteSpace: 'nowrap' }, onClick: mkdirLocal, disabled: loading || !newDirName.trim() || !cur.path }, tr('picker.create')),
|
|
1311
1453
|
React.createElement('span', { style: { fontSize: 11, opacity: 0.75, fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 } }, tr('picker.selected', { path: cur.path })),
|
|
1312
|
-
React.createElement('button', {
|
|
1454
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: { ...primaryBtnS, whiteSpace: 'nowrap' }, onClick: () => acceptLocalPick(cur.path) }, tr('picker.chooseThis')),
|
|
1313
1455
|
),
|
|
1314
1456
|
),
|
|
1315
1457
|
)
|
|
@@ -1333,6 +1475,7 @@ window.__ModuleLoader__.load({
|
|
|
1333
1475
|
: (entries.length ? entries.slice(0, 400).map((it, i) => React.createElement('div', {
|
|
1334
1476
|
key: i, title: it.drive ? tr('picker.enterDrive', { name: it.name }) : (it.dir ? tr('picker.enterDir', { name: it.name }) : tr('picker.fileAt', { name: it.name })),
|
|
1335
1477
|
onClick: it.dir ? () => enterDir(it) : undefined,
|
|
1478
|
+
className: it.dir ? 'dsh-rw-list-item' : undefined,
|
|
1336
1479
|
style: { padding: '6px 10px', cursor: it.dir ? 'pointer' : 'default', color: T.label, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', display: 'flex', gap: 8, alignItems: 'center', fontSize: 13, borderBottom: '1px solid ' + T.border },
|
|
1337
1480
|
},
|
|
1338
1481
|
React.createElement('span', { style: { flexShrink: 0, display: 'inline-flex', color: T.label, fontSize: 13 } }, it.drive ? '💽' : (it.dir
|
|
@@ -1344,31 +1487,59 @@ window.__ModuleLoader__.load({
|
|
|
1344
1487
|
),
|
|
1345
1488
|
React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end', alignItems: 'center', padding: '6px 10px', borderTop: '1px solid ' + T.border } },
|
|
1346
1489
|
React.createElement('span', { style: { fontSize: 11, opacity: 0.75, fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 } }, tr('picker.selected', { path: (last.path || (win ? tr('picker.driveRoot') : '/')) })),
|
|
1347
|
-
React.createElement('button', {
|
|
1490
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: primaryBtnS, onClick: () => { acceptBrowserPick(last.path); setPopOpen(false) } }, tr('picker.chooseThis')),
|
|
1348
1491
|
),
|
|
1349
1492
|
)
|
|
1350
1493
|
}
|
|
1351
1494
|
|
|
1495
|
+
React.useEffect(() => {
|
|
1496
|
+
if (!open) return
|
|
1497
|
+
try {
|
|
1498
|
+
if (typeof document === 'undefined') return
|
|
1499
|
+
const onKey = (e) => {
|
|
1500
|
+
if (mkdirPrompt) return
|
|
1501
|
+
if (e.key === 'Escape') {
|
|
1502
|
+
if (popOpen) { e.preventDefault(); setPopOpen(false); return }
|
|
1503
|
+
if (suggestOpen) { e.preventDefault(); setSuggestOpen(false); return }
|
|
1504
|
+
if (!busy) { e.preventDefault(); onCancel() }
|
|
1505
|
+
return
|
|
1506
|
+
}
|
|
1507
|
+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
1508
|
+
e.preventDefault()
|
|
1509
|
+
if (tab === 'local' && path.trim()) onPicked(path)
|
|
1510
|
+
else if (tab === 'remote' && path.trim()) commitPath(path)
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
document.addEventListener('keydown', onKey)
|
|
1514
|
+
return () => document.removeEventListener('keydown', onKey)
|
|
1515
|
+
} catch { return undefined }
|
|
1516
|
+
}, [open, busy, tab, path, machineId, popOpen, suggestOpen, mkdirPrompt])
|
|
1517
|
+
|
|
1352
1518
|
if (!open) return null
|
|
1353
|
-
const tabBtn = (t, lbl) => React.createElement('button', {
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1519
|
+
const tabBtn = (t, lbl) => React.createElement('button', {
|
|
1520
|
+
type: 'button',
|
|
1521
|
+
className: 'dsh-rw-btn dsh-rw-tab' + (tab === t ? ' is-active' : ''),
|
|
1522
|
+
onClick: () => switchTab(t),
|
|
1523
|
+
style: { ...buttonS, fontWeight: tab === t ? 700 : 400, padding: '5px 14px' },
|
|
1524
|
+
}, lbl)
|
|
1525
|
+
return React.createElement('div', { style: { position: 'fixed', inset: 0, zIndex: 9999, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }, onClick: () => { if (!busy && !mkdirPrompt) onCancel() } },
|
|
1526
|
+
React.createElement('div', { className: 'dsh-rw-modal-panel', style: { background: v('--dsw-alias-bg-layer-1', '#18181b'), color: T.label, border: '1px solid ' + T.borderStrong, borderRadius: 12, boxShadow: '0 12px 48px rgba(0,0,0,0.5)', width: 'min(600px, 94vw)', maxHeight: 'min(680px, 92vh)', display: 'flex', flexDirection: 'column', padding: 16, boxSizing: 'border-box' }, onClick: (e) => e.stopPropagation() },
|
|
1527
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 } },
|
|
1528
|
+
React.createElement('div', { style: { fontSize: 15, fontWeight: 650 } }, tr('picker.title')),
|
|
1529
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 8px' }, onClick: () => { if (!busy) onCancel() }, disabled: busy }, tr('picker.close')),
|
|
1359
1530
|
),
|
|
1360
|
-
React.createElement('div', {
|
|
1531
|
+
React.createElement('div', { className: 'dsh-rw-tabs', style: { marginBottom: 10, alignSelf: 'flex-start' } },
|
|
1361
1532
|
tabBtn('local', tr('picker.tabLocal')),
|
|
1362
1533
|
tabBtn('remote', tr('picker.tabRemote')),
|
|
1363
1534
|
),
|
|
1364
1535
|
tab === 'local'
|
|
1365
1536
|
? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
|
|
1366
|
-
React.createElement('div', { style: { fontSize: 12, opacity: 0.8 } }, tr('picker.localIntro')),
|
|
1537
|
+
React.createElement('div', { style: { fontSize: 12, opacity: 0.8, lineHeight: 1.5 } }, tr('picker.localIntro')),
|
|
1367
1538
|
React.createElement('div', { style: { display: 'flex', gap: 6 } },
|
|
1368
|
-
React.createElement('input', { value: path, onChange: (e) => setPath(e.target.value), placeholder: tr('picker.localPlaceholder'), style: inputS }),
|
|
1369
|
-
React.createElement('button', { style:
|
|
1539
|
+
React.createElement('input', { className: 'dsh-rw-input', value: path, onChange: (e) => setPath(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter' && path.trim()) { e.preventDefault(); onPicked(path) } }, placeholder: tr('picker.localPlaceholder'), style: inputS }),
|
|
1540
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: primaryBtnS, onClick: () => (path.trim() ? onPicked(path) : undefined), disabled: !path.trim() }, tr('picker.useLocalPath')),
|
|
1370
1541
|
),
|
|
1371
|
-
React.createElement('button', { style: { ...buttonS, alignSelf: 'flex-start' }, onClick: chooseLocal, disabled: loading }, loading ? tr('picker.opening') : tr('picker.openSysPicker')),
|
|
1542
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, alignSelf: 'flex-start' }, onClick: chooseLocal, disabled: loading }, loading ? tr('picker.opening') : tr('picker.openSysPicker')),
|
|
1372
1543
|
err ? React.createElement('div', { style: { color: T.danger, fontSize: 12 } }, err) : null,
|
|
1373
1544
|
localPop ? renderLocalPopup() : null,
|
|
1374
1545
|
)
|
|
@@ -1376,31 +1547,44 @@ window.__ModuleLoader__.load({
|
|
|
1376
1547
|
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center' } },
|
|
1377
1548
|
React.createElement('label', { style: { fontSize: 12, opacity: 0.8, whiteSpace: 'nowrap' } }, tr('picker.remoteMachine')),
|
|
1378
1549
|
React.createElement('div', { ref: ddRef, style: { position: 'relative', display: 'inline-block' } },
|
|
1379
|
-
React.createElement('button', { style: { ...inputS, textAlign: 'left', cursor: 'pointer', minWidth: 200, maxWidth: 320, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, onClick: () => setMOpen((x) => !x) },
|
|
1550
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...inputS, textAlign: 'left', cursor: 'pointer', minWidth: 200, maxWidth: 320, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, onClick: () => setMOpen((x) => !x) },
|
|
1380
1551
|
(machines.find((m) => m.id === machineId) || {}).name || (machineId ? '…' : tr('picker.chooseMachine')),
|
|
1381
1552
|
),
|
|
1382
1553
|
mOpen
|
|
1383
|
-
? React.createElement('div', { style: { position: 'absolute', top: '100%', left: 0, zIndex: 1000, marginTop: 2, minWidth: 300, maxHeight: 260, overflow: 'auto', background: v('--dsw-alias-bg-overlay', '#1f1f23'), border: '1px solid ' + T.border, borderRadius: T.radius, padding: 4 } },
|
|
1554
|
+
? React.createElement('div', { style: { position: 'absolute', top: '100%', left: 0, zIndex: 1000, marginTop: 2, minWidth: 300, maxHeight: 260, overflow: 'auto', background: v('--dsw-alias-bg-overlay', '#1f1f23'), color: T.label, border: '1px solid ' + T.border, borderRadius: T.radius, padding: 4 } },
|
|
1384
1555
|
machines.length
|
|
1385
|
-
? machines.map((m) => React.createElement('div', { key: m.id, onClick: () => { setMachineId(m.id); setRecent(m.recentWorkspaces || []); setLevels(null); setPath(''); setMOpen(false); if (m.id) loadLevels(m.id, '', 0) }, style: { padding: '6px 8px', borderRadius: 4, cursor: 'pointer', color: m.id === machineId ? T.ok : 'inherit' } }, m.name + ' (' + m.username + '@' + m.host + ':' + m.port + ')'))
|
|
1556
|
+
? machines.map((m) => React.createElement('div', { key: m.id, className: 'dsh-rw-list-item', onClick: () => { setMachineId(m.id); setRecent(m.recentWorkspaces || []); setLevels(null); setPath(''); setMOpen(false); if (m.id) loadLevels(m.id, '', 0) }, style: { padding: '6px 8px', borderRadius: 4, cursor: 'pointer', color: m.id === machineId ? T.ok : 'inherit' } }, m.name + ' (' + m.username + '@' + m.host + ':' + m.port + ')'))
|
|
1386
1557
|
: React.createElement('div', { style: { padding: 6, opacity: 0.6 } }, tr('picker.noMachines')),
|
|
1387
1558
|
)
|
|
1388
1559
|
: null,
|
|
1389
1560
|
),
|
|
1390
1561
|
),
|
|
1391
|
-
// 最近工作区快捷入口
|
|
1392
1562
|
recent.length ? React.createElement('div', { style: { display: 'flex', gap: 4, alignItems: 'center', flexWrap: 'wrap' } },
|
|
1393
1563
|
React.createElement('span', { style: { fontSize: 11, opacity: 0.6, whiteSpace: 'nowrap' } }, tr('picker.recent')),
|
|
1394
|
-
recent.map((w, i) => React.createElement('button', { key: w + i, onClick: () => { setPath(w); setErr(''); continueSuggest(w) }, style: { ...buttonS, padding: '2px 8px', fontSize: 11, fontFamily: 'monospace' } }, w))) : null,
|
|
1564
|
+
recent.map((w, i) => React.createElement('button', { type: 'button', className: 'dsh-rw-btn', key: w + i, onClick: () => { setPath(w); setErr(''); continueSuggest(w) }, style: { ...buttonS, padding: '2px 8px', fontSize: 11, fontFamily: 'monospace' } }, w))) : null,
|
|
1395
1565
|
React.createElement('div', { ref: suggestRef, style: { position: 'relative' } },
|
|
1396
1566
|
React.createElement('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
1397
|
-
React.createElement('input', {
|
|
1398
|
-
|
|
1399
|
-
|
|
1567
|
+
React.createElement('input', {
|
|
1568
|
+
className: 'dsh-rw-input',
|
|
1569
|
+
value: path,
|
|
1570
|
+
onChange: (e) => onPathChange(e.target.value),
|
|
1571
|
+
onFocus: () => loadSuggestions(path),
|
|
1572
|
+
onKeyDown: (e) => {
|
|
1573
|
+
if (e.key === 'Enter') {
|
|
1574
|
+
e.preventDefault()
|
|
1575
|
+
if (suggestOpen && suggest.length) selectSuggestion(suggest[0])
|
|
1576
|
+
else if (path.trim()) commitPath(path)
|
|
1577
|
+
}
|
|
1578
|
+
},
|
|
1579
|
+
placeholder: (machineId ? (win ? tr('picker.pathPlaceholderWin') : tr('picker.pathPlaceholder')) : tr('picker.chooseMachineFirst')),
|
|
1580
|
+
disabled: !machineId, style: { ...inputS, flex: 1, minWidth: 120 },
|
|
1581
|
+
}),
|
|
1582
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, whiteSpace: 'nowrap' }, onClick: () => { if (!machineId) return; const p = path.trim(); loadLevels(machineId, p || '', 0); setPopOpen(true) }, disabled: !machineId }, tr('picker.browse')),
|
|
1583
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, whiteSpace: 'nowrap' }, onClick: pickHome, disabled: !machineId }, tr('picker.home')),
|
|
1400
1584
|
),
|
|
1401
1585
|
(suggestOpen && suggest.length)
|
|
1402
|
-
? React.createElement('div', { style: { marginTop: 6, background: v('--dsw-alias-bg-overlay', '#1e1e1e'), border: '1px solid ' + T.borderStrong, borderRadius: 8, maxHeight: 200, overflowY: 'auto', boxShadow: '0 6px 24px rgba(0,0,0,0.25)' } },
|
|
1403
|
-
suggest.map((s, i) => React.createElement('div', { key: s + i, onMouseDown: () => selectSuggestion(s), style: { padding: '6px 10px', cursor: 'pointer', fontSize: 12, fontFamily: 'monospace', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, s)),
|
|
1586
|
+
? React.createElement('div', { style: { marginTop: 6, background: v('--dsw-alias-bg-overlay', '#1e1e1e'), color: T.label, border: '1px solid ' + T.borderStrong, borderRadius: 8, maxHeight: 200, overflowY: 'auto', boxShadow: '0 6px 24px rgba(0,0,0,0.25)' } },
|
|
1587
|
+
suggest.map((s, i) => React.createElement('div', { key: s + i, className: 'dsh-rw-list-item', onMouseDown: () => selectSuggestion(s), style: { padding: '6px 10px', cursor: 'pointer', fontSize: 12, fontFamily: 'monospace', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, s)),
|
|
1404
1588
|
)
|
|
1405
1589
|
: null,
|
|
1406
1590
|
),
|
|
@@ -1408,15 +1592,22 @@ window.__ModuleLoader__.load({
|
|
|
1408
1592
|
err ? React.createElement('div', { style: { color: T.danger, fontSize: 12 } }, err) : null,
|
|
1409
1593
|
React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end', alignItems: 'center' } },
|
|
1410
1594
|
React.createElement('span', { style: { fontSize: 11, opacity: 0.75, fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 } }, path ? tr('picker.selected', { path }) : ''),
|
|
1411
|
-
React.createElement('button', {
|
|
1595
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: primaryBtnS, onClick: () => commitPath(path), disabled: busy || !machineId || !path.trim() }, busy ? tr('picker.mirroring') : tr('picker.setRemoteWs')),
|
|
1412
1596
|
),
|
|
1413
1597
|
),
|
|
1414
|
-
|
|
1598
|
+
React.createElement('div', { style: { fontSize: 11, opacity: 0.55, paddingTop: 8 } }, tr('picker.hintKeys')),
|
|
1599
|
+
(tab === 'local') ? React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end' } }, React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: buttonS, onClick: onCancel }, tr('picker.cancel'))) : null,
|
|
1415
1600
|
React.createElement('div', { style: { display: 'flex', gap: 10, justifyContent: 'center', alignItems: 'center', paddingTop: 6, fontSize: 10, opacity: 0.5 } },
|
|
1416
|
-
React.createElement('a', { href: 'https://github.com/flymysql/dsh-remote', target: '_blank', rel: 'noopener noreferrer', style: { color: 'inherit' } }, '
|
|
1601
|
+
React.createElement('a', { href: 'https://github.com/flymysql/dsh-remote', target: '_blank', rel: 'noopener noreferrer', style: { color: 'inherit' } }, tr('picker.star')),
|
|
1417
1602
|
React.createElement('span', {}, '·'),
|
|
1418
|
-
React.createElement('a', { href: 'https://github.com/flymysql/dsh-remote/issues/new', target: '_blank', rel: 'noopener noreferrer', style: { color: 'inherit' } }, '
|
|
1603
|
+
React.createElement('a', { href: 'https://github.com/flymysql/dsh-remote/issues/new', target: '_blank', rel: 'noopener noreferrer', style: { color: 'inherit' } }, tr('picker.feedback')),
|
|
1419
1604
|
),
|
|
1605
|
+
mkdirPrompt ? React.createElement(PromptDialog, {
|
|
1606
|
+
title: tr('picker.newDirName', { path: mkdirPrompt.base || '/' }),
|
|
1607
|
+
placeholder: tr('explorer.namePlaceholder'),
|
|
1608
|
+
onSubmit: confirmMkdirHere,
|
|
1609
|
+
onCancel: () => setMkdirPrompt(false),
|
|
1610
|
+
}) : null,
|
|
1420
1611
|
),
|
|
1421
1612
|
)
|
|
1422
1613
|
}
|
|
@@ -1425,8 +1616,8 @@ window.__ModuleLoader__.load({
|
|
|
1425
1616
|
const SIDEBAR_EXPLORER_ID = 'dsh-remote:explorer'
|
|
1426
1617
|
const SIDEBAR_FILE_ID = 'dsh-remote:file'
|
|
1427
1618
|
|
|
1428
|
-
function fetchRemoteStatus() {
|
|
1429
|
-
return api('GET', '/dsh-remote/status').catch(() => null)
|
|
1619
|
+
function fetchRemoteStatus(sessionId) {
|
|
1620
|
+
return api('GET', withSessionQuery('/dsh-remote/status', sessionId)).catch(() => null)
|
|
1430
1621
|
}
|
|
1431
1622
|
|
|
1432
1623
|
// Resolve the remote path this session shows. Issue #13: the ONLY source
|
|
@@ -1458,14 +1649,14 @@ window.__ModuleLoader__.load({
|
|
|
1458
1649
|
.catch(() => '')
|
|
1459
1650
|
}
|
|
1460
1651
|
|
|
1461
|
-
function readRemoteFile(path, maxBytes) {
|
|
1462
|
-
return api('POST', '/dsh-remote/read', { path, maxBytes }).catch((e) => {
|
|
1652
|
+
function readRemoteFile(path, maxBytes, sessionId) {
|
|
1653
|
+
return api('POST', '/dsh-remote/read', withSessionBody({ path, maxBytes }, sessionId)).catch((e) => {
|
|
1463
1654
|
throw new Error(tr('file.readFail', { msg: ((e && e.message) || e) }))
|
|
1464
1655
|
})
|
|
1465
1656
|
}
|
|
1466
1657
|
|
|
1467
|
-
function listRemoteDir(path) {
|
|
1468
|
-
return api('GET', '/dsh-remote/ls?path=' + encodeURIComponent(path || '')).catch((e) => {
|
|
1658
|
+
function listRemoteDir(path, sessionId) {
|
|
1659
|
+
return api('GET', withSessionQuery('/dsh-remote/ls?path=' + encodeURIComponent(path || ''), sessionId)).catch((e) => {
|
|
1469
1660
|
throw new Error(tr('file.listFail', { msg: ((e && e.message) || e) }))
|
|
1470
1661
|
})
|
|
1471
1662
|
}
|
|
@@ -1675,7 +1866,11 @@ window.__ModuleLoader__.load({
|
|
|
1675
1866
|
const [hoverPath, setHoverPath] = React.useState('') // row hover highlight
|
|
1676
1867
|
/** Row whose path was just copied — shows a brief「已复制」label. */
|
|
1677
1868
|
const [copiedPath, setCopiedPath] = React.useState('')
|
|
1869
|
+
const [namePrompt, setNamePrompt] = React.useState(null) // { kind, path, name }
|
|
1870
|
+
React.useEffect(() => { ensureChromeCss() }, [])
|
|
1678
1871
|
const expanded = Array.isArray(props.expanded) ? props.expanded : []
|
|
1872
|
+
const scopeCwd = props.scope && props.scope.cwd
|
|
1873
|
+
const sessionId = props.scope && props.scope.sessionId
|
|
1679
1874
|
|
|
1680
1875
|
const storeLevel = (path, level) => {
|
|
1681
1876
|
dataRef.current = { ...dataRef.current, [path]: level }
|
|
@@ -1691,7 +1886,7 @@ window.__ModuleLoader__.load({
|
|
|
1691
1886
|
if (existing && !existing.loading && opts && opts.keep) return
|
|
1692
1887
|
if (cur[dir] !== undefined && !cur[dir].loading) return
|
|
1693
1888
|
storeLevel(dir, { loading: true })
|
|
1694
|
-
listRemoteDir(dir)
|
|
1889
|
+
listRemoteDir(dir, sessionId)
|
|
1695
1890
|
.then((res) => {
|
|
1696
1891
|
const items = Array.isArray(res && res.items) ? res.items : []
|
|
1697
1892
|
const sorted = items.slice().sort((a, b) => {
|
|
@@ -1712,14 +1907,12 @@ window.__ModuleLoader__.load({
|
|
|
1712
1907
|
storeLevel(dir, { error: String((e && e.message) || e) })
|
|
1713
1908
|
}
|
|
1714
1909
|
})
|
|
1715
|
-
}, [])
|
|
1910
|
+
}, [sessionId])
|
|
1716
1911
|
|
|
1717
1912
|
// Resolve this session's remote root, then load it and every expanded dir.
|
|
1718
1913
|
// Re-runs whenever the session scope changes (switching conversations, or
|
|
1719
1914
|
// the async sessionCwd fetch landing), so the sidebar follows the session's
|
|
1720
1915
|
// own workspace dir.
|
|
1721
|
-
const scopeCwd = props.scope && props.scope.cwd
|
|
1722
|
-
const sessionId = props.scope && props.scope.sessionId
|
|
1723
1916
|
const rootRef = React.useRef('')
|
|
1724
1917
|
const refreshStatus = React.useCallback((opts) => {
|
|
1725
1918
|
const applyRoot = (r, connected) => {
|
|
@@ -1739,14 +1932,17 @@ window.__ModuleLoader__.load({
|
|
|
1739
1932
|
// Not connected (e.g. freshly restarted, machine no longer active):
|
|
1740
1933
|
// show a clear disconnected state instead of firing ls requests
|
|
1741
1934
|
// that all fail with AggregateError / 500 in a loop.
|
|
1742
|
-
if
|
|
1935
|
+
// A bound remote session lists on demand even if this process has
|
|
1936
|
+
// not yet opened the SSH socket; only an unbound/disconnected
|
|
1937
|
+
// machine-scoped view should show the empty "未连接" state.
|
|
1938
|
+
if (!connected && !sessionId) {
|
|
1743
1939
|
storeLevel(r, { error: tr('explorer.notConnected') })
|
|
1744
1940
|
return
|
|
1745
1941
|
}
|
|
1746
1942
|
if (!sameRoot) {
|
|
1747
1943
|
storeLevel(r, { loading: true })
|
|
1748
1944
|
}
|
|
1749
|
-
listRemoteDir(r)
|
|
1945
|
+
listRemoteDir(r, sessionId)
|
|
1750
1946
|
.then((res) => {
|
|
1751
1947
|
const items = Array.isArray(res && res.items) ? res.items : []
|
|
1752
1948
|
const sorted = items.slice().sort((a, b) => {
|
|
@@ -1779,7 +1975,7 @@ window.__ModuleLoader__.load({
|
|
|
1779
1975
|
return Promise.resolve('')
|
|
1780
1976
|
}
|
|
1781
1977
|
resolveRoot().then((r) => {
|
|
1782
|
-
fetchRemoteStatus().then((s) => {
|
|
1978
|
+
fetchRemoteStatus(sessionId).then((s) => {
|
|
1783
1979
|
setStatus(s)
|
|
1784
1980
|
// Issue #13: NEVER fall back to the machine-level workspace here —
|
|
1785
1981
|
// a session whose cwd is not a mirror is a LOCAL session and shows
|
|
@@ -1807,12 +2003,12 @@ window.__ModuleLoader__.load({
|
|
|
1807
2003
|
const host = document.createElement('div')
|
|
1808
2004
|
host.style.cssText = 'position:fixed;z-index:999999;pointer-events:auto'
|
|
1809
2005
|
const box = document.createElement('div')
|
|
1810
|
-
box.style.cssText = 'position:fixed;left:' + menu.x + 'px;top:' + menu.y + 'px;z-index:999999;background
|
|
2006
|
+
box.style.cssText = 'position:fixed;left:' + menu.x + 'px;top:' + menu.y + 'px;z-index:999999;background:var(--dsw-alias-bg-overlay,#1f1f23);border:1px solid var(--dsw-alias-border-l3,rgba(128,128,128,0.5));border-radius:8px;padding:4px;box-shadow:0 8px 24px rgba(0,0,0,0.4);min-width:170px;font-family:system-ui,sans-serif;color:var(--dsw-alias-label-primary,#e4e4e7)'
|
|
1811
2007
|
const mkItem = (label, danger, fn) => {
|
|
1812
2008
|
const d = document.createElement('div')
|
|
1813
2009
|
d.textContent = label
|
|
1814
|
-
d.style.cssText = 'padding:6px 10px;cursor:pointer;font-size:12px;color:' + (danger ? '
|
|
1815
|
-
d.onmouseenter = () => { d.style.background = 'rgba(128,128,128,0.14)' }
|
|
2010
|
+
d.style.cssText = 'padding:6px 10px;cursor:pointer;font-size:12px;color:' + (danger ? 'var(--dsw-static-red-400,#e06c75)' : 'inherit') + ';border-radius:4px;white-space:nowrap;transition:background 0.1s'
|
|
2011
|
+
d.onmouseenter = () => { d.style.background = 'var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,0.14))' }
|
|
1816
2012
|
d.onmouseleave = () => { d.style.background = 'transparent' }
|
|
1817
2013
|
d.addEventListener('click', (e) => { e.stopPropagation(); setMenu(null); fn() })
|
|
1818
2014
|
return d
|
|
@@ -1821,7 +2017,7 @@ window.__ModuleLoader__.load({
|
|
|
1821
2017
|
box.appendChild(mkItem(tr('explorer.menuDownload'), false, () => doFs('download', { path: menu.path })))
|
|
1822
2018
|
box.appendChild(mkItem(tr('explorer.menuCopyRel'), false, () => copyPath(relPath(menu.path), menu.path)))
|
|
1823
2019
|
box.appendChild(mkItem(tr('explorer.menuCopyAbs'), false, () => copyPath(menu.path, menu.path)))
|
|
1824
|
-
box.appendChild(mkItem(tr('explorer.menuRename'), false, () => {
|
|
2020
|
+
box.appendChild(mkItem(tr('explorer.menuRename'), false, () => { setNamePrompt({ kind: 'rename', path: menu.path, name: menu.name }) }))
|
|
1825
2021
|
box.appendChild(mkItem(menu.isDir ? tr('explorer.menuDeleteDir') : tr('explorer.menuDelete'), true, () => { if (window.confirm(tr('explorer.deleteConfirm', { path: menu.path }))) doFs('remove', { path: menu.path }, true) }))
|
|
1826
2022
|
host.appendChild(box)
|
|
1827
2023
|
document.body.appendChild(host)
|
|
@@ -1839,7 +2035,7 @@ window.__ModuleLoader__.load({
|
|
|
1839
2035
|
|
|
1840
2036
|
const doFs = (op, payload, refreshParent) => {
|
|
1841
2037
|
setBusy(op)
|
|
1842
|
-
return api('POST', '/dsh-remote/fs', { op, ...payload })
|
|
2038
|
+
return api('POST', '/dsh-remote/fs', withSessionBody({ op, ...payload }, sessionId))
|
|
1843
2039
|
.then(() => {
|
|
1844
2040
|
setErr('')
|
|
1845
2041
|
if (refreshParent) {
|
|
@@ -1859,7 +2055,8 @@ window.__ModuleLoader__.load({
|
|
|
1859
2055
|
}
|
|
1860
2056
|
const ctxItem = (label, danger, fn) => React.createElement('div', {
|
|
1861
2057
|
onClick: (e) => { e.stopPropagation(); setMenu(null); fn() },
|
|
1862
|
-
|
|
2058
|
+
className: 'dsh-rw-list-item',
|
|
2059
|
+
style: { padding: '6px 10px', cursor: 'pointer', fontSize: 12, color: danger ? T.danger : T.label, borderRadius: 4, whiteSpace: 'nowrap' },
|
|
1863
2060
|
}, label)
|
|
1864
2061
|
|
|
1865
2062
|
// The path shown by「复制相对地址」: relative to this session's remote
|
|
@@ -1949,11 +2146,11 @@ window.__ModuleLoader__.load({
|
|
|
1949
2146
|
return React.createElement('div', { key: dir + ':missing', style: { display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px', paddingLeft: depth * 22 + 6, fontSize: 13, opacity: 0.6 } }, tr('explorer.dirMissing'))
|
|
1950
2147
|
}
|
|
1951
2148
|
if (level.error !== undefined) {
|
|
1952
|
-
return React.createElement('div', { key: dir + ':err', style: { display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px', paddingLeft: depth * 22 + 6, fontSize: 13, color:
|
|
2149
|
+
return React.createElement('div', { key: dir + ':err', style: { display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px', paddingLeft: depth * 22 + 6, fontSize: 13, color: T.danger } }, level.error)
|
|
1953
2150
|
}
|
|
1954
2151
|
const items = level.entries || []
|
|
1955
2152
|
const soft = level.softError
|
|
1956
|
-
|
|
2153
|
+
? React.createElement('div', { key: dir + ':softerr', style: { display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px', paddingLeft: depth * 22 + 6, fontSize: 11, opacity: 0.6, color: T.warn } }, tr('explorer.softError', { msg: level.softError }))
|
|
1957
2154
|
: null
|
|
1958
2155
|
if (!items.length) return soft || null
|
|
1959
2156
|
return React.createElement('div', { key: dir + ':list' },
|
|
@@ -1966,13 +2163,15 @@ window.__ModuleLoader__.load({
|
|
|
1966
2163
|
const rootLevel = data[root]
|
|
1967
2164
|
|
|
1968
2165
|
return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 } },
|
|
1969
|
-
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', padding: '6px 8px', borderBottom: '1px solid
|
|
1970
|
-
React.createElement('button', { style: { ...buttonS, padding: '3px 8px', fontSize: 12 }, onClick: refreshStatus, title: tr('explorer.refresh') }, '↻'),
|
|
2166
|
+
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', padding: '6px 8px', borderBottom: '1px solid ' + T.border } },
|
|
2167
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '3px 8px', fontSize: 12 }, onClick: refreshStatus, title: tr('explorer.refresh') }, '↻'),
|
|
1971
2168
|
React.createElement('button', {
|
|
2169
|
+
type: 'button',
|
|
2170
|
+
className: 'dsh-rw-btn',
|
|
1972
2171
|
style: { ...buttonS, padding: '3px 8px', fontSize: 12 },
|
|
1973
|
-
onClick: () => {
|
|
2172
|
+
onClick: () => setNamePrompt({ kind: 'mkdir', path: root || '/', name: '' }),
|
|
1974
2173
|
}, tr('explorer.newDir')),
|
|
1975
|
-
React.createElement('button', { style: { ...buttonS, padding: '3px 8px', fontSize: 12 }, onClick: () => setPicker(true) }, tr('explorer.more')),
|
|
2174
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '3px 8px', fontSize: 12 }, onClick: () => setPicker(true) }, tr('explorer.more')),
|
|
1976
2175
|
),
|
|
1977
2176
|
status && status.workspace && root
|
|
1978
2177
|
? React.createElement('div', { style: { fontSize: 11, opacity: 0.7, padding: '4px 8px', wordBreak: 'break-all' } }, tr('explorer.remoteWs', { path: root, host: (status.host ? tr('explorer.hostTag', { user: status.username, host: status.host }) : '') }))
|
|
@@ -2000,7 +2199,25 @@ window.__ModuleLoader__.load({
|
|
|
2000
2199
|
),
|
|
2001
2200
|
// context menu — now rendered via DOM portal in useEffect above
|
|
2002
2201
|
null,
|
|
2003
|
-
err ? React.createElement('div', { style: { color:
|
|
2202
|
+
err ? React.createElement('div', { style: { color: T.danger, fontSize: 12, padding: '4px 8px' } }, (busy ? tr('explorer.busy') : '') + err) : null,
|
|
2203
|
+
namePrompt ? React.createElement(PromptDialog, {
|
|
2204
|
+
title: namePrompt.kind === 'rename' ? tr('explorer.renameTitle') : tr('explorer.mkdirTitle'),
|
|
2205
|
+
initial: namePrompt.name || '',
|
|
2206
|
+
placeholder: namePrompt.kind === 'rename' ? tr('explorer.renamePrompt') : tr('explorer.namePlaceholder'),
|
|
2207
|
+
onCancel: () => setNamePrompt(null),
|
|
2208
|
+
onSubmit: (nm) => {
|
|
2209
|
+
const name = String(nm || '').trim()
|
|
2210
|
+
const cur = namePrompt
|
|
2211
|
+
setNamePrompt(null)
|
|
2212
|
+
if (!name) return
|
|
2213
|
+
if (cur.kind === 'rename') {
|
|
2214
|
+
if (name === cur.name) return
|
|
2215
|
+
doFs('rename', { path: cur.path, dest: joinRemote(String(cur.path).replace(/[\\/][^\\/]*$/, ''), name) }, true)
|
|
2216
|
+
} else {
|
|
2217
|
+
doFs('mkdir', { path: joinRemote(cur.path, name) }, true)
|
|
2218
|
+
}
|
|
2219
|
+
},
|
|
2220
|
+
}) : null,
|
|
2004
2221
|
)
|
|
2005
2222
|
}
|
|
2006
2223
|
|
|
@@ -2008,18 +2225,23 @@ window.__ModuleLoader__.load({
|
|
|
2008
2225
|
function RemoteFileTab(props) {
|
|
2009
2226
|
const { tab, scope } = props
|
|
2010
2227
|
const path = tab.path || ''
|
|
2228
|
+
const fileSessionId = scope && scope.sessionId
|
|
2011
2229
|
const [data, setData] = React.useState(null)
|
|
2012
2230
|
const [err, setErr] = React.useState('')
|
|
2013
2231
|
const [loading, setLoading] = React.useState(true)
|
|
2014
2232
|
const [edit, setEdit] = React.useState(false)
|
|
2015
2233
|
const [draft, setDraft] = React.useState('')
|
|
2016
2234
|
const [saving, setSaving] = React.useState(false)
|
|
2235
|
+
React.useEffect(() => { ensureChromeCss() }, [])
|
|
2236
|
+
|
|
2237
|
+
const originalText = (data && data.content != null ? data.content : '').replace(/\n…\[truncated:.*$/, '')
|
|
2238
|
+
const dirty = edit && draft !== originalText
|
|
2017
2239
|
|
|
2018
2240
|
const load = () => {
|
|
2019
2241
|
if (!path) { setLoading(false); return }
|
|
2020
2242
|
let cancelled = false
|
|
2021
2243
|
setLoading(true); setErr(''); setData(null); setEdit(false)
|
|
2022
|
-
readRemoteFile(path, 256 * 1024)
|
|
2244
|
+
readRemoteFile(path, 256 * 1024, fileSessionId)
|
|
2023
2245
|
.then((d) => { if (!cancelled) { setData(d); setLoading(false) } })
|
|
2024
2246
|
.catch((e) => { if (!cancelled) { setErr(String((e && e.message) || e)); setLoading(false) } })
|
|
2025
2247
|
return () => { cancelled = true }
|
|
@@ -2033,7 +2255,7 @@ window.__ModuleLoader__.load({
|
|
|
2033
2255
|
}
|
|
2034
2256
|
const saveRemote = () => {
|
|
2035
2257
|
setSaving(true); setErr('')
|
|
2036
|
-
apiRaw('POST', '/dsh-remote/write', { path, content: draft })
|
|
2258
|
+
apiRaw('POST', '/dsh-remote/write', withSessionBody({ path, content: draft, expectedMtime: data && data.mtime }, fileSessionId))
|
|
2037
2259
|
.then((r) => {
|
|
2038
2260
|
if (r.status === 409) {
|
|
2039
2261
|
setErr(String((r.data && r.data.error) || tr('file.conflict')))
|
|
@@ -2048,38 +2270,48 @@ window.__ModuleLoader__.load({
|
|
|
2048
2270
|
}
|
|
2049
2271
|
const downloadMirror = () => {
|
|
2050
2272
|
setSaving(true); setErr('')
|
|
2051
|
-
api('POST', '/dsh-remote/fs', { op: 'download', path })
|
|
2273
|
+
api('POST', '/dsh-remote/fs', withSessionBody({ op: 'download', path }, fileSessionId))
|
|
2052
2274
|
.then((r) => setErr(tr('file.downloaded', { local: (r && r.local ? tr('file.downloadedAt', { path: r.local }) : '') })))
|
|
2053
2275
|
.catch((e) => setErr(String((e && e.message) || e)))
|
|
2054
2276
|
.finally(() => setSaving(false))
|
|
2055
2277
|
}
|
|
2056
2278
|
|
|
2057
2279
|
const baseName = path.split(/[\\/]/).pop() || path
|
|
2058
|
-
return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 } },
|
|
2059
|
-
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px', borderBottom: '1px solid
|
|
2280
|
+
return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0, color: T.label } },
|
|
2281
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px', borderBottom: '1px solid ' + T.border, fontSize: 12 } },
|
|
2060
2282
|
React.createElement('span', { style: { fontWeight: 600, whiteSpace: 'nowrap' } }, '📄 ' + baseName),
|
|
2061
2283
|
React.createElement('code', { style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', opacity: 0.75 } }, path),
|
|
2284
|
+
dirty ? React.createElement('span', { className: 'dsh-rw-pill', style: { color: T.warn } }, tr('file.dirty')) : null,
|
|
2062
2285
|
React.createElement('span', { style: { opacity: 0.7, fontSize: 11, flexShrink: 0 } }, edit ? tr('file.editing') : tr('file.readonly')),
|
|
2063
2286
|
edit
|
|
2064
2287
|
? React.createElement(React.Fragment, null,
|
|
2065
|
-
React.createElement('button', { style: { ...
|
|
2066
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: () => { setEdit(false); setErr('') } }, tr('file.cancel')))
|
|
2288
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: { ...primaryBtnS, padding: '2px 10px', fontSize: 12 }, onClick: saveRemote, disabled: saving || !dirty, title: tr('file.saveHint') }, saving ? tr('file.saving') : tr('file.saveToRemote')),
|
|
2289
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: () => { setEdit(false); setErr('') } }, tr('file.cancel')))
|
|
2067
2290
|
: React.createElement(React.Fragment, null,
|
|
2068
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: startEdit, disabled: !data || data.binary }, tr('file.edit')),
|
|
2069
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: downloadMirror, disabled: saving }, tr('file.download'))),
|
|
2291
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: startEdit, disabled: !data || data.binary }, tr('file.edit')),
|
|
2292
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: downloadMirror, disabled: saving }, tr('file.download'))),
|
|
2070
2293
|
),
|
|
2071
|
-
React.createElement('div', { style: { flex: 1, overflow: 'auto', minHeight: 0 } },
|
|
2294
|
+
React.createElement('div', { style: { flex: 1, overflow: 'auto', minHeight: 0, display: 'flex', flexDirection: 'column' } },
|
|
2295
|
+
err ? React.createElement('div', { style: { color: T.danger, padding: '8px 12px', fontSize: 13, flexShrink: 0 } }, err) : null,
|
|
2072
2296
|
loading ? React.createElement('div', { style: { opacity: 0.7, padding: 12, fontSize: 13 } }, tr('file.loading'))
|
|
2073
|
-
:
|
|
2074
|
-
|
|
2075
|
-
? React.createElement('div', { style: { padding: 12, fontSize: 13, color: '#e6c07b' } },
|
|
2297
|
+
: (data && data.binary
|
|
2298
|
+
? React.createElement('div', { style: { padding: 12, fontSize: 13, color: T.warn } },
|
|
2076
2299
|
tr('file.binary', { size: (data.size != null ? data.size + ' bytes' : tr('file.unknownSize')) }))
|
|
2077
2300
|
: (edit
|
|
2078
|
-
? React.createElement('textarea', {
|
|
2079
|
-
|
|
2080
|
-
(
|
|
2301
|
+
? React.createElement('textarea', {
|
|
2302
|
+
value: draft,
|
|
2303
|
+
onChange: (e) => setDraft(e.target.value),
|
|
2304
|
+
onKeyDown: (e) => {
|
|
2305
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 's') { e.preventDefault(); if (!saving && dirty) saveRemote() }
|
|
2306
|
+
if (e.key === 'Escape') { e.preventDefault(); setEdit(false); setErr('') }
|
|
2307
|
+
},
|
|
2308
|
+
spellCheck: false,
|
|
2309
|
+
style: { width: '100%', height: '100%', flex: 1, boxSizing: 'border-box', border: 'none', outline: 'none', resize: 'none', background: 'transparent', color: T.label, fontSize: 13, lineHeight: 1.55, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', padding: '10px 12px', whiteSpace: 'pre-wrap', wordBreak: 'break-word' },
|
|
2310
|
+
})
|
|
2311
|
+
: React.createElement('div', { style: { fontSize: 13, lineHeight: 1.55, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', padding: '10px 12px', whiteSpace: 'pre-wrap', wordBreak: 'break-word', color: T.label } },
|
|
2312
|
+
(data && data.content != null) ? data.content : (err ? '' : tr('file.empty')),
|
|
2081
2313
|
(data && data.truncated)
|
|
2082
|
-
? React.createElement('div', { style: { color:
|
|
2314
|
+
? React.createElement('div', { style: { color: T.warn, fontSize: 12, marginTop: 8 } }, tr('file.truncated'))
|
|
2083
2315
|
: null,
|
|
2084
2316
|
))),
|
|
2085
2317
|
),
|
|
@@ -2201,6 +2433,7 @@ window.__ModuleLoader__.load({
|
|
|
2201
2433
|
}
|
|
2202
2434
|
|
|
2203
2435
|
function apply(ctx) {
|
|
2436
|
+
ensureChromeCss()
|
|
2204
2437
|
const slots = ctx.get('slots')
|
|
2205
2438
|
wireLocale(ctx)
|
|
2206
2439
|
// Optional integrations must not gate the settings page. In particular,
|