@goodandready/dsh-remote-workspace 0.3.6 → 0.3.8
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 +4 -2
- package/README.ru.md +4 -2
- package/README.zh.md +4 -2
- package/lib/client.js +490 -323
- package/lib/mirror-sync-service.js +11 -6
- package/lib/remote-fs-service.js +4 -0
- package/lib/routes/docker.js +2 -0
- package/lib/routes/files.js +17 -4
- package/lib/routes/helpers.js +46 -4
- package/lib/routes/profiles.js +12 -2
- package/lib/routes/terminal.js +3 -2
- package/lib/routes/tunnels.js +1 -0
- package/lib/ssh-service.js +3 -0
- package/lib/tar-sync-service.js +12 -3
- package/lib/updater-service.js +3 -18
- package/package.json +1 -2
- package/docs/design/DESIGN.md +0 -91
package/lib/client.js
CHANGED
|
@@ -117,6 +117,23 @@ window.__ModuleLoader__.load({
|
|
|
117
117
|
title: 'Remote Workspace',
|
|
118
118
|
subtitle: 'Native SSH connections, SFTP file access, 3-way mirror sync, and port forwarding tunnels.',
|
|
119
119
|
status: 'Ready',
|
|
120
|
+
actionFailed: 'The request failed.',
|
|
121
|
+
versionUnknown: 'Version unknown',
|
|
122
|
+
updaterTitle: 'Plugin update',
|
|
123
|
+
currentVersion: 'Installed version',
|
|
124
|
+
latestVersion: 'Latest on npm',
|
|
125
|
+
checkUpdate: 'Check for updates',
|
|
126
|
+
checking: 'Checking...',
|
|
127
|
+
updateNow: 'Update now',
|
|
128
|
+
updating: 'Updating...',
|
|
129
|
+
upToDate: 'Up to date',
|
|
130
|
+
updateAvailable: 'Update available',
|
|
131
|
+
restartRequired: 'The update is installed. Restart DSH to use it.',
|
|
132
|
+
updateFailed: 'Update failed',
|
|
133
|
+
statusLoading: 'Checking connection',
|
|
134
|
+
statusEmpty: 'No active host',
|
|
135
|
+
statusReady: 'Host ready',
|
|
136
|
+
statusError: 'Status unavailable',
|
|
120
137
|
|
|
121
138
|
tabProfiles: '🖥️ Host Profiles',
|
|
122
139
|
|
|
@@ -256,6 +273,12 @@ window.__ModuleLoader__.load({
|
|
|
256
273
|
title: '远程开发工作区',
|
|
257
274
|
subtitle: '支持原生 SSH 连接、SFTP 文件访问、3-way 镜像同步与端口转发隧道。',
|
|
258
275
|
status: '就绪',
|
|
276
|
+
actionFailed: '请求失败。',
|
|
277
|
+
versionUnknown: '版本未知',
|
|
278
|
+
statusLoading: '正在检查连接',
|
|
279
|
+
statusEmpty: '没有活动主机',
|
|
280
|
+
statusReady: '主机已就绪',
|
|
281
|
+
statusError: '状态不可用',
|
|
259
282
|
|
|
260
283
|
tabProfiles: '🖥️ 主机配置',
|
|
261
284
|
tabContainers: '🐳 容器管理',
|
|
@@ -1135,237 +1158,31 @@ window.__ModuleLoader__.load({
|
|
|
1135
1158
|
)
|
|
1136
1159
|
}
|
|
1137
1160
|
|
|
1138
|
-
function
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
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
|
-
|
|
1250
|
-
const openDirectoryBrowser = async (initialPath) => {
|
|
1251
|
-
setBrowserOpen(true)
|
|
1252
|
-
const target = initialPath && initialPath.startsWith('/') ? initialPath : '/'
|
|
1253
|
-
await fetchRemoteDir(target)
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
const fetchRemoteDir = async (dirPath) => {
|
|
1257
|
-
setBrowserLoading(true)
|
|
1258
|
-
try {
|
|
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)
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
const handleSync = async (direction) => {
|
|
1274
|
-
setBusy(`sync_${direction}`)
|
|
1275
|
-
setSyncMsg(null)
|
|
1276
|
-
try {
|
|
1277
|
-
const res = await fetch('/dsh-remote-workspace/sync', {
|
|
1278
|
-
method: 'POST',
|
|
1279
|
-
headers: { 'Content-Type': 'application/json' },
|
|
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)
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
const handleStartTunnel = async () => {
|
|
1300
|
-
if (!activeId) return
|
|
1301
|
-
setBusy('tunnel_open')
|
|
1302
|
-
setTunnelMsg(null)
|
|
1303
|
-
try {
|
|
1304
|
-
const res = await fetch('/dsh-remote-workspace/tunnels/start', {
|
|
1305
|
-
method: 'POST',
|
|
1306
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1307
|
-
body: JSON.stringify({
|
|
1308
|
-
profileId: activeId,
|
|
1309
|
-
localPort: newTunnelLocal,
|
|
1310
|
-
remotePort: newTunnelRemote
|
|
1311
|
-
})
|
|
1312
|
-
})
|
|
1313
|
-
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
|
-
}
|
|
1320
|
-
} catch (err) {
|
|
1321
|
-
setTunnelMsg({ ok: false, text: t('tOpenErr', { err: err.message }) })
|
|
1322
|
-
}
|
|
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
|
-
}
|
|
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
|
|
1161
|
+
function ProfilesPanel(panel) {
|
|
1162
|
+
const {
|
|
1163
|
+
t,
|
|
1164
|
+
editing,
|
|
1165
|
+
setEditing,
|
|
1166
|
+
setTestResult,
|
|
1167
|
+
profiles,
|
|
1168
|
+
activeId,
|
|
1169
|
+
busy,
|
|
1170
|
+
handleSave,
|
|
1171
|
+
testResult,
|
|
1172
|
+
handleTest,
|
|
1173
|
+
handleSetActive,
|
|
1174
|
+
handleDelete,
|
|
1175
|
+
browserOpen,
|
|
1176
|
+
setBrowserOpen,
|
|
1177
|
+
browserPath,
|
|
1178
|
+
browserEntries,
|
|
1179
|
+
browserLoading,
|
|
1180
|
+
openDirectoryBrowser,
|
|
1181
|
+
fetchRemoteDir,
|
|
1182
|
+
showPassword,
|
|
1183
|
+
setShowPassword,
|
|
1184
|
+
} = panel
|
|
1185
|
+
return (
|
|
1369
1186
|
React.createElement(
|
|
1370
1187
|
'div',
|
|
1371
1188
|
{ className: 'drw-card' },
|
|
@@ -1826,104 +1643,67 @@ window.__ModuleLoader__.load({
|
|
|
1826
1643
|
: !editing
|
|
1827
1644
|
? React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('noHosts'))
|
|
1828
1645
|
: null
|
|
1829
|
-
)
|
|
1646
|
+
)
|
|
1647
|
+
)
|
|
1648
|
+
}
|
|
1830
1649
|
|
|
1831
|
-
|
|
1650
|
+
function SyncPanel(panel) {
|
|
1651
|
+
const {
|
|
1652
|
+
t,
|
|
1653
|
+
busy,
|
|
1654
|
+
activeProfile,
|
|
1655
|
+
handleSync,
|
|
1656
|
+
syncMsg,
|
|
1657
|
+
autoSync,
|
|
1658
|
+
setAutoSync,
|
|
1659
|
+
reportAction,
|
|
1660
|
+
setActionError,
|
|
1661
|
+
} = panel
|
|
1662
|
+
return (
|
|
1832
1663
|
activeProfile
|
|
1833
1664
|
? React.createElement(
|
|
1834
1665
|
'div',
|
|
1835
1666
|
{ className: 'drw-card' },
|
|
1667
|
+
React.createElement('div', { className: 'drw-card-title' }, t('syncTitle')),
|
|
1668
|
+
React.createElement('div', { className: 'drw-card-desc' }, t('syncDesc')),
|
|
1669
|
+
|
|
1836
1670
|
React.createElement(
|
|
1837
1671
|
'div',
|
|
1838
|
-
{
|
|
1839
|
-
React.createElement(
|
|
1672
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)', marginBottom: '12px' } },
|
|
1673
|
+
React.createElement(
|
|
1674
|
+
'div',
|
|
1675
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
|
|
1676
|
+
React.createElement('span', { style: { fontWeight: 600, fontSize: '13px' } }, t('autoSyncTitle')),
|
|
1677
|
+
React.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } }, t('autoSyncDesc'))
|
|
1678
|
+
),
|
|
1840
1679
|
React.createElement(
|
|
1841
1680
|
'button',
|
|
1842
1681
|
{
|
|
1843
1682
|
type: 'button',
|
|
1844
|
-
className: 'drw-btn'
|
|
1845
|
-
style: {
|
|
1846
|
-
|
|
1847
|
-
|
|
1683
|
+
className: `drw-btn ${autoSync ? 'drw-btn-primary' : ''}`,
|
|
1684
|
+
style: { padding: '4px 12px', fontSize: '12px' },
|
|
1685
|
+
onClick: async () => {
|
|
1686
|
+
const next = !autoSync
|
|
1687
|
+
try {
|
|
1688
|
+
const res = await fetch('/dsh-remote-workspace/watcher/toggle', {
|
|
1689
|
+
method: 'POST',
|
|
1690
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1691
|
+
body: JSON.stringify({ enabled: next })
|
|
1692
|
+
})
|
|
1693
|
+
const data = await reportAction(res)
|
|
1694
|
+
if (data && data.ok) setAutoSync(data.autoSync)
|
|
1695
|
+
} catch (err) {
|
|
1696
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1848
1699
|
},
|
|
1849
|
-
|
|
1700
|
+
autoSync ? '⚡ Active (ON)' : '⚪ Off'
|
|
1850
1701
|
)
|
|
1851
1702
|
),
|
|
1852
|
-
|
|
1703
|
+
|
|
1853
1704
|
React.createElement(
|
|
1854
1705
|
'div',
|
|
1855
|
-
{ className: 'drw-
|
|
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,
|
|
1883
|
-
|
|
1884
|
-
// Section 3: Mirror Sync (ClineBot Action Card style)
|
|
1885
|
-
activeProfile
|
|
1886
|
-
? React.createElement(
|
|
1887
|
-
'div',
|
|
1888
|
-
{ className: 'drw-card' },
|
|
1889
|
-
React.createElement('div', { className: 'drw-card-title' }, t('syncTitle')),
|
|
1890
|
-
React.createElement('div', { className: 'drw-card-desc' }, t('syncDesc')),
|
|
1891
|
-
|
|
1892
|
-
React.createElement(
|
|
1893
|
-
'div',
|
|
1894
|
-
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)', marginBottom: '12px' } },
|
|
1895
|
-
React.createElement(
|
|
1896
|
-
'div',
|
|
1897
|
-
{ style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
|
|
1898
|
-
React.createElement('span', { style: { fontWeight: 600, fontSize: '13px' } }, t('autoSyncTitle')),
|
|
1899
|
-
React.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } }, t('autoSyncDesc'))
|
|
1900
|
-
),
|
|
1901
|
-
React.createElement(
|
|
1902
|
-
'button',
|
|
1903
|
-
{
|
|
1904
|
-
type: 'button',
|
|
1905
|
-
className: `drw-btn ${autoSync ? 'drw-btn-primary' : ''}`,
|
|
1906
|
-
style: { padding: '4px 12px', fontSize: '12px' },
|
|
1907
|
-
onClick: async () => {
|
|
1908
|
-
const next = !autoSync
|
|
1909
|
-
try {
|
|
1910
|
-
const res = await fetch('/dsh-remote-workspace/watcher/toggle', {
|
|
1911
|
-
method: 'POST',
|
|
1912
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1913
|
-
body: JSON.stringify({ enabled: next })
|
|
1914
|
-
})
|
|
1915
|
-
const d = await res.json()
|
|
1916
|
-
if (d.ok) setAutoSync(d.autoSync)
|
|
1917
|
-
} catch (err) { /* best-effort cleanup */ }
|
|
1918
|
-
}
|
|
1919
|
-
},
|
|
1920
|
-
autoSync ? '⚡ Active (ON)' : '⚪ Off'
|
|
1921
|
-
)
|
|
1922
|
-
),
|
|
1923
|
-
|
|
1924
|
-
React.createElement(
|
|
1925
|
-
'div',
|
|
1926
|
-
{ className: 'drw-row' },
|
|
1706
|
+
{ className: 'drw-row' },
|
|
1927
1707
|
React.createElement(
|
|
1928
1708
|
'button',
|
|
1929
1709
|
{
|
|
@@ -1958,9 +1738,25 @@ window.__ModuleLoader__.load({
|
|
|
1958
1738
|
)
|
|
1959
1739
|
: null
|
|
1960
1740
|
)
|
|
1961
|
-
: null
|
|
1741
|
+
: null
|
|
1742
|
+
)
|
|
1743
|
+
}
|
|
1962
1744
|
|
|
1963
|
-
|
|
1745
|
+
function TunnelsPanel(panel) {
|
|
1746
|
+
const {
|
|
1747
|
+
t,
|
|
1748
|
+
busy,
|
|
1749
|
+
activeProfile,
|
|
1750
|
+
newTunnelLocal,
|
|
1751
|
+
setNewTunnelLocal,
|
|
1752
|
+
newTunnelRemote,
|
|
1753
|
+
setNewTunnelRemote,
|
|
1754
|
+
handleStartTunnel,
|
|
1755
|
+
tunnelMsg,
|
|
1756
|
+
tunnels,
|
|
1757
|
+
handleStopTunnel,
|
|
1758
|
+
} = panel
|
|
1759
|
+
return (
|
|
1964
1760
|
React.createElement(
|
|
1965
1761
|
'div',
|
|
1966
1762
|
{ className: 'drw-card' },
|
|
@@ -2077,6 +1873,330 @@ window.__ModuleLoader__.load({
|
|
|
2077
1873
|
)
|
|
2078
1874
|
}
|
|
2079
1875
|
|
|
1876
|
+
function SettingsView(props) {
|
|
1877
|
+
const [activeTab, setActiveTab] = React.useState('profiles')
|
|
1878
|
+
const [healthData, setHealthData] = React.useState(null)
|
|
1879
|
+
const [autoSync, setAutoSync] = React.useState(false)
|
|
1880
|
+
|
|
1881
|
+
const t = props.t || (props.locale === 'zh' ? makeT(zh, en) : makeT(en, zh))
|
|
1882
|
+
const [profiles, setProfiles] = React.useState([])
|
|
1883
|
+
const [activeId, setActiveId] = React.useState(null)
|
|
1884
|
+
const [tunnels, setTunnels] = React.useState([])
|
|
1885
|
+
const [editing, setEditing] = React.useState(null)
|
|
1886
|
+
const [busy, setBusy] = React.useState(null)
|
|
1887
|
+
const [testResult, setTestResult] = React.useState(null)
|
|
1888
|
+
const [actionError, setActionError] = React.useState('')
|
|
1889
|
+
const [syncMsg, setSyncMsg] = React.useState(null)
|
|
1890
|
+
const [tunnelMsg, setTunnelMsg] = React.useState(null)
|
|
1891
|
+
const [showPassword, setShowPassword] = React.useState(false)
|
|
1892
|
+
|
|
1893
|
+
// Tunnel creation inputs
|
|
1894
|
+
const [newTunnelLocal, setNewTunnelLocal] = React.useState('3000')
|
|
1895
|
+
const [newTunnelRemote, setNewTunnelRemote] = React.useState('3000')
|
|
1896
|
+
|
|
1897
|
+
// Remote Directory Browser State
|
|
1898
|
+
const [browserOpen, setBrowserOpen] = React.useState(false)
|
|
1899
|
+
const [browserPath, setBrowserPath] = React.useState('/')
|
|
1900
|
+
const [browserEntries, setBrowserEntries] = React.useState([])
|
|
1901
|
+
const [browserLoading, setBrowserLoading] = React.useState(false)
|
|
1902
|
+
|
|
1903
|
+
|
|
1904
|
+
function actionErrorText(status, data) {
|
|
1905
|
+
if (status >= 200 && status < 300 && data && data.ok !== false) return ''
|
|
1906
|
+
if (data && typeof data.error === 'string' && data.error) return data.error
|
|
1907
|
+
return 'HTTP ' + status
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
const reportAction = async (res) => {
|
|
1911
|
+
let data = null
|
|
1912
|
+
try { data = await res.json() } catch (err) { data = null }
|
|
1913
|
+
const text = actionErrorText(res.status, data)
|
|
1914
|
+
setActionError(text ? ((data && data.error) ? text : t('actionFailed')) : '')
|
|
1915
|
+
return text ? null : data
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
const loadState = async () => {
|
|
1919
|
+
try {
|
|
1920
|
+
const res = await fetch('/dsh-remote-workspace/state')
|
|
1921
|
+
const data = await reportAction(res)
|
|
1922
|
+
if (data && data.ok) {
|
|
1923
|
+
setProfiles(data.profiles || [])
|
|
1924
|
+
setActiveId(data.activeId || null)
|
|
1925
|
+
setTunnels(data.tunnels || [])
|
|
1926
|
+
setAutoSync(Boolean(data.autoSync))
|
|
1927
|
+
if (data.activeId) {
|
|
1928
|
+
fetch(`/dsh-remote-workspace/health?profileId=${data.activeId}`).then(r => r.json()).then(h => { if (h.ok) setHealthData(h.health) }).catch(() => {})
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
} catch (err) {
|
|
1932
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
React.useEffect(() => {
|
|
1937
|
+
ensureCss()
|
|
1938
|
+
loadState()
|
|
1939
|
+
}, [])
|
|
1940
|
+
|
|
1941
|
+
const handleSave = async (profile) => {
|
|
1942
|
+
setBusy('saving')
|
|
1943
|
+
try {
|
|
1944
|
+
const res = await fetch('/dsh-remote-workspace/profiles/save', {
|
|
1945
|
+
method: 'POST',
|
|
1946
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1947
|
+
body: JSON.stringify(profile)
|
|
1948
|
+
})
|
|
1949
|
+
const data = await reportAction(res)
|
|
1950
|
+
if (data) {
|
|
1951
|
+
setEditing(null)
|
|
1952
|
+
setBrowserOpen(false)
|
|
1953
|
+
setTestResult(null)
|
|
1954
|
+
await loadState()
|
|
1955
|
+
}
|
|
1956
|
+
} catch (err) {
|
|
1957
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
1958
|
+
}
|
|
1959
|
+
setBusy(null)
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
const handleDelete = async (id, name) => {
|
|
1963
|
+
if (!confirm(t('delConfirm', { name: name || id }))) return
|
|
1964
|
+
try {
|
|
1965
|
+
const res = await fetch('/dsh-remote-workspace/profiles/delete', {
|
|
1966
|
+
method: 'POST',
|
|
1967
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1968
|
+
body: JSON.stringify({ id })
|
|
1969
|
+
})
|
|
1970
|
+
const data = await reportAction(res)
|
|
1971
|
+
if (data) {
|
|
1972
|
+
if (editing && editing.id === id) setEditing(null)
|
|
1973
|
+
await loadState()
|
|
1974
|
+
}
|
|
1975
|
+
} catch (err) {
|
|
1976
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
const handleSetActive = async (id) => {
|
|
1981
|
+
try {
|
|
1982
|
+
const res = await fetch('/dsh-remote-workspace/profiles/active', {
|
|
1983
|
+
method: 'POST',
|
|
1984
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1985
|
+
body: JSON.stringify({ id })
|
|
1986
|
+
})
|
|
1987
|
+
const data = await reportAction(res)
|
|
1988
|
+
if (data) await loadState()
|
|
1989
|
+
} catch (err) {
|
|
1990
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
const handleTest = async (prof) => {
|
|
1995
|
+
setBusy('testing')
|
|
1996
|
+
setTestResult(null)
|
|
1997
|
+
try {
|
|
1998
|
+
const res = await fetch('/dsh-remote-workspace/test', {
|
|
1999
|
+
method: 'POST',
|
|
2000
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2001
|
+
body: JSON.stringify(prof)
|
|
2002
|
+
})
|
|
2003
|
+
const data = await res.json()
|
|
2004
|
+
setTestResult(data)
|
|
2005
|
+
} catch (err) {
|
|
2006
|
+
setTestResult({ ok: false, error: err.message })
|
|
2007
|
+
}
|
|
2008
|
+
setBusy(null)
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
const openDirectoryBrowser = async (initialPath) => {
|
|
2012
|
+
setBrowserOpen(true)
|
|
2013
|
+
const target = initialPath && initialPath.startsWith('/') ? initialPath : '/'
|
|
2014
|
+
await fetchRemoteDir(target)
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
const fetchRemoteDir = async (dirPath) => {
|
|
2018
|
+
setBrowserLoading(true)
|
|
2019
|
+
try {
|
|
2020
|
+
const res = await fetch('/dsh-remote-workspace/browse', {
|
|
2021
|
+
method: 'POST',
|
|
2022
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2023
|
+
body: JSON.stringify({ profile: editing, remotePath: dirPath })
|
|
2024
|
+
})
|
|
2025
|
+
const data = await reportAction(res)
|
|
2026
|
+
if (data && data.ok) {
|
|
2027
|
+
setBrowserPath(data.currentPath || dirPath)
|
|
2028
|
+
setBrowserEntries(data.entries || [])
|
|
2029
|
+
}
|
|
2030
|
+
} catch (err) {
|
|
2031
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
2032
|
+
}
|
|
2033
|
+
setBrowserLoading(false)
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
const handleSync = async (direction) => {
|
|
2037
|
+
setBusy(`sync_${direction}`)
|
|
2038
|
+
setSyncMsg(null)
|
|
2039
|
+
try {
|
|
2040
|
+
const res = await fetch('/dsh-remote-workspace/sync', {
|
|
2041
|
+
method: 'POST',
|
|
2042
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2043
|
+
body: JSON.stringify({ direction })
|
|
2044
|
+
})
|
|
2045
|
+
const data = await res.json()
|
|
2046
|
+
if (data.ok) {
|
|
2047
|
+
const count = (direction === 'push' ? data.pushed?.length : data.pulled?.length) || 0
|
|
2048
|
+
if (data.conflicts && data.conflicts.length > 0) {
|
|
2049
|
+
setSyncMsg({ ok: false, text: t('syncConflict', { count: data.conflicts.length }) })
|
|
2050
|
+
} else {
|
|
2051
|
+
setSyncMsg({ ok: true, text: t('syncOk', { count }) })
|
|
2052
|
+
}
|
|
2053
|
+
} else {
|
|
2054
|
+
setSyncMsg({ ok: false, text: t('syncErr', { err: data.error || 'Unknown error' }) })
|
|
2055
|
+
}
|
|
2056
|
+
} catch (err) {
|
|
2057
|
+
setSyncMsg({ ok: false, text: t('syncErr', { err: err.message }) })
|
|
2058
|
+
}
|
|
2059
|
+
setBusy(null)
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
const handleStartTunnel = async () => {
|
|
2063
|
+
if (!activeId) return
|
|
2064
|
+
setBusy('tunnel_open')
|
|
2065
|
+
setTunnelMsg(null)
|
|
2066
|
+
try {
|
|
2067
|
+
const res = await fetch('/dsh-remote-workspace/tunnels/start', {
|
|
2068
|
+
method: 'POST',
|
|
2069
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2070
|
+
body: JSON.stringify({
|
|
2071
|
+
profileId: activeId,
|
|
2072
|
+
localPort: newTunnelLocal,
|
|
2073
|
+
remotePort: newTunnelRemote
|
|
2074
|
+
})
|
|
2075
|
+
})
|
|
2076
|
+
const data = await res.json()
|
|
2077
|
+
if (data.ok) {
|
|
2078
|
+
setTunnelMsg({ ok: true, text: t('tOpenOk', { localPort: newTunnelLocal, remotePort: newTunnelRemote }) })
|
|
2079
|
+
await loadState()
|
|
2080
|
+
} else {
|
|
2081
|
+
setTunnelMsg({ ok: false, text: t('tOpenErr', { err: data.error || 'Failed' }) })
|
|
2082
|
+
}
|
|
2083
|
+
} catch (err) {
|
|
2084
|
+
setTunnelMsg({ ok: false, text: t('tOpenErr', { err: err.message }) })
|
|
2085
|
+
}
|
|
2086
|
+
setBusy(null)
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
const handleStopTunnel = async (tunnelId) => {
|
|
2090
|
+
try {
|
|
2091
|
+
const res = await fetch('/dsh-remote-workspace/tunnels/stop', {
|
|
2092
|
+
method: 'POST',
|
|
2093
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2094
|
+
body: JSON.stringify({ tunnelId })
|
|
2095
|
+
})
|
|
2096
|
+
const data = await reportAction(res)
|
|
2097
|
+
if (data) await loadState()
|
|
2098
|
+
} catch (err) {
|
|
2099
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
const activeProfile = profiles.find((p) => p.id === activeId)
|
|
2104
|
+
|
|
2105
|
+
return React.createElement(
|
|
2106
|
+
'div',
|
|
2107
|
+
{ className: 'drw-page' },
|
|
2108
|
+
|
|
2109
|
+
// Header with Live Badges (ClineBot Style)
|
|
2110
|
+
React.createElement(
|
|
2111
|
+
'div',
|
|
2112
|
+
{ className: 'drw-header' },
|
|
2113
|
+
React.createElement(
|
|
2114
|
+
'div',
|
|
2115
|
+
{ className: 'drw-header-top' },
|
|
2116
|
+
React.createElement('div', { className: 'drw-page-title' }, '🌐 ' + t('title')),
|
|
2117
|
+
React.createElement(
|
|
2118
|
+
'div',
|
|
2119
|
+
{ className: 'drw-header-badges' },
|
|
2120
|
+
activeProfile
|
|
2121
|
+
? React.createElement('span', { className: 'drw-badge drw-badge-ok' }, t('badgeOnline', { name: activeProfile.name || activeProfile.host, latency: testResult?.latencyMs ?? 15 }))
|
|
2122
|
+
: React.createElement('span', { className: 'drw-badge drw-badge-warn' }, t('badgeOffline')),
|
|
2123
|
+
activeProfile
|
|
2124
|
+
? React.createElement('span', { className: 'drw-badge' }, activeProfile.authType === 'password' ? t('badgePassword') : t('badgeKey'))
|
|
2125
|
+
: null,
|
|
2126
|
+
React.createElement('span', { className: `drw-badge ${tunnels.length > 0 ? 'drw-badge-ok' : ''}` }, t('badgeTunnels', { count: tunnels.length }))
|
|
2127
|
+
)
|
|
2128
|
+
),
|
|
2129
|
+
React.createElement('div', { className: 'drw-page-sub' }, t('subtitle')),
|
|
2130
|
+
actionError
|
|
2131
|
+
? React.createElement('div', { className: 'drw-badge drw-badge-err', role: 'alert' }, actionError)
|
|
2132
|
+
: null
|
|
2133
|
+
),
|
|
2134
|
+
|
|
2135
|
+
|
|
2136
|
+
React.createElement(ProfilesPanel, { t, editing, setEditing, setTestResult, profiles, activeId, busy, handleSave, testResult, handleTest, handleSetActive, handleDelete, browserOpen, setBrowserOpen, browserPath, browserEntries, browserLoading, openDirectoryBrowser, fetchRemoteDir, showPassword, setShowPassword }),
|
|
2137
|
+
|
|
2138
|
+
|
|
2139
|
+
// Section 2: Active Connection Diagnostics (ClineBot Card 4 style)
|
|
2140
|
+
activeProfile
|
|
2141
|
+
? React.createElement(
|
|
2142
|
+
'div',
|
|
2143
|
+
{ className: 'drw-card' },
|
|
2144
|
+
React.createElement(
|
|
2145
|
+
'div',
|
|
2146
|
+
{ className: 'drw-card-title' },
|
|
2147
|
+
React.createElement('span', null, t('diagTitle')),
|
|
2148
|
+
React.createElement(
|
|
2149
|
+
'button',
|
|
2150
|
+
{
|
|
2151
|
+
type: 'button',
|
|
2152
|
+
className: 'drw-btn',
|
|
2153
|
+
style: { fontSize: '11px', padding: '3px 9px' },
|
|
2154
|
+
disabled: busy === 'testing',
|
|
2155
|
+
onClick: () => handleTest(activeProfile)
|
|
2156
|
+
},
|
|
2157
|
+
busy === 'testing' ? t('testing') : t('diagProbeBtn')
|
|
2158
|
+
)
|
|
2159
|
+
),
|
|
2160
|
+
React.createElement('div', { className: 'drw-card-desc' }, t('diagDesc')),
|
|
2161
|
+
React.createElement(
|
|
2162
|
+
'div',
|
|
2163
|
+
{ className: 'drw-grid-4' },
|
|
2164
|
+
React.createElement(
|
|
2165
|
+
'div',
|
|
2166
|
+
{ className: 'drw-stat-box' },
|
|
2167
|
+
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.name || activeProfile.host),
|
|
2168
|
+
React.createElement('div', { className: 'drw-stat-lbl' }, `SSH: ${activeProfile.username}@${activeProfile.host}:${activeProfile.port || 22}`)
|
|
2169
|
+
),
|
|
2170
|
+
React.createElement(
|
|
2171
|
+
'div',
|
|
2172
|
+
{ className: 'drw-stat-box' },
|
|
2173
|
+
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.remoteWorkspace || '—'),
|
|
2174
|
+
React.createElement('div', { className: 'drw-stat-lbl' }, 'Remote Workspace')
|
|
2175
|
+
),
|
|
2176
|
+
React.createElement(
|
|
2177
|
+
'div',
|
|
2178
|
+
{ className: 'drw-stat-box' },
|
|
2179
|
+
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.localMirrorPath || '—'),
|
|
2180
|
+
React.createElement('div', { className: 'drw-stat-lbl' }, 'Local Mirror Path')
|
|
2181
|
+
),
|
|
2182
|
+
React.createElement(
|
|
2183
|
+
'div',
|
|
2184
|
+
{ className: 'drw-stat-box' },
|
|
2185
|
+
React.createElement('div', { className: 'drw-stat-val', style: { color: 'var(--dsw-alias-state-success-primary)' } }, 'Ready ✓'),
|
|
2186
|
+
React.createElement('div', { className: 'drw-stat-lbl' }, activeProfile.authType === 'password' ? 'Password Auth' : 'SSH Key Auth')
|
|
2187
|
+
)
|
|
2188
|
+
)
|
|
2189
|
+
)
|
|
2190
|
+
: null,
|
|
2191
|
+
|
|
2192
|
+
|
|
2193
|
+
React.createElement(SyncPanel, { t, busy, activeProfile, handleSync, syncMsg, autoSync, setAutoSync, reportAction, setActionError }),
|
|
2194
|
+
|
|
2195
|
+
React.createElement(TunnelsPanel, { t, busy, activeProfile, newTunnelLocal, setNewTunnelLocal, newTunnelRemote, setNewTunnelRemote, handleStartTunnel, tunnelMsg, tunnels, handleStopTunnel }),
|
|
2196
|
+
|
|
2197
|
+
)
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2080
2200
|
// Accordion item for Settings -> Plugins section
|
|
2081
2201
|
|
|
2082
2202
|
function UpdaterSection(props) {
|
|
@@ -2154,8 +2274,8 @@ window.__ModuleLoader__.load({
|
|
|
2154
2274
|
React.createElement(
|
|
2155
2275
|
'div',
|
|
2156
2276
|
null,
|
|
2157
|
-
React.createElement('span', { style: { fontWeight: 500 } }, 'v' +
|
|
2158
|
-
updateStatus?.
|
|
2277
|
+
React.createElement('span', { style: { fontWeight: 500 } }, (updateStatus?.currentVersion ? 'v' + updateStatus.currentVersion : t('versionUnknown'))),
|
|
2278
|
+
updateStatus?.updateAvailable
|
|
2159
2279
|
? React.createElement('span', { style: { marginLeft: '8px', color: 'var(--dsw-alias-state-warning-text)' } }, '→ v' + updateStatus.latestVersion)
|
|
2160
2280
|
: null,
|
|
2161
2281
|
msg
|
|
@@ -2198,10 +2318,34 @@ window.__ModuleLoader__.load({
|
|
|
2198
2318
|
const [open, setOpen] = React.useState(!!page)
|
|
2199
2319
|
const t = props.t || (props.locale === 'zh' ? makeT(zh, en) : makeT(en, zh))
|
|
2200
2320
|
|
|
2321
|
+
const [conn, setConn] = React.useState('loading')
|
|
2322
|
+
|
|
2201
2323
|
React.useEffect(() => {
|
|
2202
2324
|
ensureCss()
|
|
2203
2325
|
}, [])
|
|
2204
2326
|
|
|
2327
|
+
React.useEffect(() => {
|
|
2328
|
+
let cancelled = false
|
|
2329
|
+
fetch('/dsh-remote-workspace/state')
|
|
2330
|
+
.then((res) => {
|
|
2331
|
+
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
2332
|
+
return res.json()
|
|
2333
|
+
})
|
|
2334
|
+
.then((data) => {
|
|
2335
|
+
if (cancelled) return
|
|
2336
|
+
if (!data || data.ok === false) {
|
|
2337
|
+
setConn('error')
|
|
2338
|
+
return
|
|
2339
|
+
}
|
|
2340
|
+
const active = (data.profiles || []).find((item) => item.id === data.activeId)
|
|
2341
|
+
setConn(active ? 'ready' : 'empty')
|
|
2342
|
+
})
|
|
2343
|
+
.catch(() => {
|
|
2344
|
+
if (!cancelled) setConn('error')
|
|
2345
|
+
})
|
|
2346
|
+
return () => { cancelled = true }
|
|
2347
|
+
}, [])
|
|
2348
|
+
|
|
2205
2349
|
// Row seat (plugins.row.config): the host page draws title/icon/crumb and the
|
|
2206
2350
|
// padding, so the summary is a one-liner and the page drops our card chrome.
|
|
2207
2351
|
if (props && props.view === 'summary') {
|
|
@@ -2235,11 +2379,22 @@ window.__ModuleLoader__.load({
|
|
|
2235
2379
|
React.createElement('div', { style: { fontWeight: 600, fontSize: '15px', color: 'var(--dsw-alias-label-primary)' } }, t('title')),
|
|
2236
2380
|
React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('subtitle'))
|
|
2237
2381
|
),
|
|
2238
|
-
React.createElement('span', {
|
|
2382
|
+
React.createElement('span', {
|
|
2383
|
+
className: 'drw-badge ' + (
|
|
2384
|
+
conn === 'ready' ? 'drw-badge-ok'
|
|
2385
|
+
: conn === 'error' ? 'drw-badge-err'
|
|
2386
|
+
: conn === 'empty' ? 'drw-badge-warn' : ''
|
|
2387
|
+
),
|
|
2388
|
+
style: { marginRight: '14px' }
|
|
2389
|
+
}, t(
|
|
2390
|
+
conn === 'ready' ? 'statusReady'
|
|
2391
|
+
: conn === 'error' ? 'statusError'
|
|
2392
|
+
: conn === 'empty' ? 'statusEmpty' : 'statusLoading'
|
|
2393
|
+
)),
|
|
2239
2394
|
React.createElement(
|
|
2240
2395
|
'span',
|
|
2241
2396
|
{ style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
|
|
2242
|
-
React.createElement(FallbackChevron)
|
|
2397
|
+
React.createElement(ChevronIcon || FallbackChevron)
|
|
2243
2398
|
)
|
|
2244
2399
|
),
|
|
2245
2400
|
(page || open)
|
|
@@ -2301,16 +2456,28 @@ window.__ModuleLoader__.load({
|
|
|
2301
2456
|
|
|
2302
2457
|
if (ctx.slots && typeof ctx.slots.inject === 'function') {
|
|
2303
2458
|
// 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
|
-
//
|
|
2459
|
+
// (0.1.6-alpha.2) renders as the plugin's own page with its configuration.
|
|
2460
|
+
// The label getter runs during page render. A locale-service lookup there
|
|
2461
|
+
// crashes the client batch, so the title is read from the dictionaries
|
|
2462
|
+
// already loaded in this bundle. English is the result when the active
|
|
2463
|
+
// language cannot be read.
|
|
2307
2464
|
ctx.slots.inject('plugins.item', () =>
|
|
2308
2465
|
ctx.slots.register(
|
|
2309
2466
|
{
|
|
2310
2467
|
name: 'plugins.item',
|
|
2311
2468
|
id: ROW_ID,
|
|
2312
2469
|
order: 60,
|
|
2313
|
-
label: () =>
|
|
2470
|
+
label: () => {
|
|
2471
|
+
try {
|
|
2472
|
+
const lang = ctx.locale && typeof ctx.locale.getLocale === 'function'
|
|
2473
|
+
? ctx.locale.getLocale()
|
|
2474
|
+
: ''
|
|
2475
|
+
if (typeof lang === 'string' && (lang === 'zh' || lang.indexOf('zh') === 0)) return zh.title
|
|
2476
|
+
} catch (err) {
|
|
2477
|
+
return en.title
|
|
2478
|
+
}
|
|
2479
|
+
return en.title
|
|
2480
|
+
},
|
|
2314
2481
|
locale: NS,
|
|
2315
2482
|
inject: () => ({ ctx })
|
|
2316
2483
|
},
|