dsh-app-deploy 0.1.0 → 0.3.0
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 +13 -4
- package/client.js +352 -121
- package/package.json +3 -2
- package/server/config.js +92 -31
- package/server/deployment.js +179 -46
- package/server/local-tools.js +105 -10
- package/server/rpc.js +18 -6
package/client.js
CHANGED
|
@@ -48,17 +48,19 @@ window.__ModuleLoader__.load({
|
|
|
48
48
|
}
|
|
49
49
|
function clone(v) { return JSON.parse(JSON.stringify(v)) }
|
|
50
50
|
function normPath(p) { return (p || '').replace(/\/+$/, '') }
|
|
51
|
+
// 绝对路径:posix 以 / 开头,Windows 为盘符或 \ 开头(如 C:\proj、D:/proj)
|
|
52
|
+
function isAbsPath(p) { return /^([a-zA-Z]:[\\/]|[\\/])/.test(p || '') }
|
|
51
53
|
function joinPath(base, name) {
|
|
52
54
|
if (!name) return base || ''
|
|
53
|
-
if (name
|
|
55
|
+
if (isAbsPath(name)) return name
|
|
54
56
|
const b = base || ''
|
|
55
57
|
if (!b) return name
|
|
56
|
-
return b === '/' ? '/' + name : b.replace(
|
|
58
|
+
return b === '/' ? '/' + name : b.replace(/[\\/]+$/, '') + '/' + name
|
|
57
59
|
}
|
|
58
60
|
function parentPath(p) {
|
|
59
61
|
const base = normPath(p || '')
|
|
60
|
-
if (!base || base === '/') return '/'
|
|
61
|
-
const i = base.lastIndexOf('/')
|
|
62
|
+
if (!base || base === '/' || /^[a-zA-Z]:[\\/]?$/.test(base)) return base || '/'
|
|
63
|
+
const i = Math.max(base.lastIndexOf('/'), base.lastIndexOf('\\'))
|
|
62
64
|
return i <= 0 ? '/' : base.slice(0, i)
|
|
63
65
|
}
|
|
64
66
|
function newEnvId(envs) {
|
|
@@ -72,7 +74,7 @@ window.__ModuleLoader__.load({
|
|
|
72
74
|
while (modules.some((m) => m.id === 'mod' + n)) n++
|
|
73
75
|
return {
|
|
74
76
|
id: 'mod' + n, name: '新模块', kind: 'front', enabled: true,
|
|
75
|
-
|
|
77
|
+
buildCmd: '', sshId: '', localPath: '', remotePath: '',
|
|
76
78
|
postCommand: '', healthCheckUrl: '', frontEnv: '',
|
|
77
79
|
}
|
|
78
80
|
}
|
|
@@ -80,14 +82,21 @@ window.__ModuleLoader__.load({
|
|
|
80
82
|
return {
|
|
81
83
|
id: m.id || '', name: m.name || '', kind: m.kind === 'server' ? 'server' : 'front',
|
|
82
84
|
enabled: m.enabled !== false,
|
|
83
|
-
|
|
85
|
+
buildCmd: m.buildCmd || '', sshId: m.sshId || '',
|
|
84
86
|
localPath: m.localPath || '', remotePath: m.remotePath || '', postCommand: m.postCommand || '',
|
|
85
87
|
healthCheckUrl: m.healthCheckUrl || '', frontEnv: m.frontEnv || '',
|
|
86
88
|
}
|
|
87
89
|
}
|
|
90
|
+
// 模块的发布目标(与服务端 config.js targetFor 同规则:sshId 落空→第一个目标)
|
|
91
|
+
function targetOf(env, m) {
|
|
92
|
+
const list = (env && env.sshs) || []
|
|
93
|
+
return list.find((s) => s.id === (m && m.sshId)) || list[0] || {}
|
|
94
|
+
}
|
|
95
|
+
|
|
88
96
|
function normalizeEnv(env) {
|
|
89
97
|
const e = clone(env || {})
|
|
90
|
-
|
|
98
|
+
// 旧格式 env.ssh 单对象自动迁移为 sshs 数组
|
|
99
|
+
const rawSshs = Array.isArray(e.sshs) && e.sshs.length ? e.sshs : (e.ssh ? [e.ssh] : [])
|
|
91
100
|
return {
|
|
92
101
|
id: e.id || '',
|
|
93
102
|
name: e.name || '',
|
|
@@ -95,11 +104,17 @@ window.__ModuleLoader__.load({
|
|
|
95
104
|
group: e.group || '',
|
|
96
105
|
projectPath: e.projectPath || '',
|
|
97
106
|
jdkHome: e.jdkHome || '',
|
|
107
|
+
nodeHome: e.nodeHome || '',
|
|
108
|
+
nodeOptions: e.nodeOptions || '',
|
|
109
|
+
pmPath: e.pmPath || '',
|
|
110
|
+
buildEnv: e.buildEnv || '',
|
|
98
111
|
expTime: e.expTime || '',
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
112
|
+
sshs: rawSshs.map((s, i) => ({
|
|
113
|
+
id: s.id || (i === 0 ? 'default' : 'ssh' + (i + 1)),
|
|
114
|
+
name: s.name || (i === 0 ? '默认' : 'SSH ' + (i + 1)),
|
|
115
|
+
host: s.host || '', port: Number(s.port) || 22, user: s.user || 'root',
|
|
116
|
+
password: s.password || '', keyFile: s.keyFile || '', options: s.options || '',
|
|
117
|
+
})),
|
|
103
118
|
modules: (e.modules || []).map(normalizeModule),
|
|
104
119
|
}
|
|
105
120
|
}
|
|
@@ -125,9 +140,40 @@ window.__ModuleLoader__.load({
|
|
|
125
140
|
children)
|
|
126
141
|
}
|
|
127
142
|
|
|
143
|
+
// 路径下拉选择器(JDK / Node 共用):输入框 + 候选项弹层
|
|
144
|
+
function PathPicker({ value, aria, items, open, wrapRef, onFocus, onChange, onPick, emptyText }) {
|
|
145
|
+
return h('div', { className: 'dad-jdkwrap', ref: wrapRef },
|
|
146
|
+
h('input', {
|
|
147
|
+
className: 'dad-input', type: 'text', value, 'aria-label': aria, onFocus,
|
|
148
|
+
onChange: (ev) => onChange(ev.target.value),
|
|
149
|
+
}),
|
|
150
|
+
open ? h('div', { className: 'dad-jdkdd' },
|
|
151
|
+
items.length
|
|
152
|
+
? items.map((j) => h('button', {
|
|
153
|
+
key: j.path, type: 'button', className: 'dad-jitem',
|
|
154
|
+
onClick: () => onPick(j.path),
|
|
155
|
+
},
|
|
156
|
+
h('span', { className: 'dad-jlbl' }, j.label),
|
|
157
|
+
h('span', { className: 'dad-jfull' }, j.path)))
|
|
158
|
+
: h('div', { className: 'dad-jempty' }, emptyText)) : null)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 构建工具预设:选中即填充构建命令(可再手动编辑)
|
|
162
|
+
const BUILD_PRESETS = [
|
|
163
|
+
{ tool: 'Gradle', cmd: './gradlew' },
|
|
164
|
+
{ tool: 'Maven', cmd: 'mvn -DskipTests package' },
|
|
165
|
+
{ tool: 'npm', cmd: 'npm run build' },
|
|
166
|
+
{ tool: 'pnpm', cmd: 'pnpm run build' },
|
|
167
|
+
{ tool: 'yarn', cmd: 'yarn build' },
|
|
168
|
+
]
|
|
169
|
+
|
|
128
170
|
function EnvCard({ env, selected, lastBuild, onSelect, onCopy, onDelete }) {
|
|
129
171
|
const lb = lastBuild[env.id]
|
|
130
|
-
const
|
|
172
|
+
const sshList = env.sshs || []
|
|
173
|
+
const ssh = sshList[0] || {}
|
|
174
|
+
const sshText = ssh.host
|
|
175
|
+
? (ssh.user || 'root') + '@' + ssh.host + ':' + (ssh.port || 22) + (sshList.length > 1 ? ' 等 ' + sshList.length + ' 个目标' : '')
|
|
176
|
+
: 'SSH 未配置'
|
|
131
177
|
return h('div', {
|
|
132
178
|
className: 'dad-env',
|
|
133
179
|
'data-sel': selected ? '1' : '0',
|
|
@@ -140,7 +186,7 @@ window.__ModuleLoader__.load({
|
|
|
140
186
|
h('i', { className: 'dad-dot' + (lb ? ' st-' + lb.status : ''), 'aria-hidden': true }),
|
|
141
187
|
env.name || env.id),
|
|
142
188
|
h('span', { className: 'dad-sub' }, env.id + (env.note ? ' · ' + env.note : '')),
|
|
143
|
-
h('span', { className: 'dad-sub' },
|
|
189
|
+
h('span', { className: 'dad-sub' }, sshText),
|
|
144
190
|
h('span', { className: 'dad-sub dad-build' + (lb ? ' ' + lb.status : '') },
|
|
145
191
|
h('i', { className: 'dad-dot', 'aria-hidden': true }),
|
|
146
192
|
lb ? '最后构建: ' + fmtTime(lb.time) + '(' + (HSTATUS[lb.status] || lb.status) + ')' : '最后构建: 从未')),
|
|
@@ -203,11 +249,11 @@ window.__ModuleLoader__.load({
|
|
|
203
249
|
nodes))
|
|
204
250
|
}
|
|
205
251
|
|
|
206
|
-
function ModuleCard({ mod, onChange, onDelete }) {
|
|
252
|
+
function ModuleCard({ mod, onChange, onDelete, sshs }) {
|
|
207
253
|
const envFile = mod.id === 'bsFront' ? 'client/client-front/.env'
|
|
208
254
|
: mod.id === 'adminFront' ? 'admin/admin-front/.env' : ''
|
|
209
255
|
const feLabel = 'frontEnv(KEY=VALUE 多行,构建时注入'
|
|
210
|
-
+ (envFile ? ' ' + envFile
|
|
256
|
+
+ (envFile ? ' ' + envFile : '') + ')'
|
|
211
257
|
return h('div', { className: 'dad-mcard', 'data-off': mod.enabled ? '0' : '1' },
|
|
212
258
|
h('button', {
|
|
213
259
|
type: 'button', className: 'dad-btndel', title: '删除模块', 'aria-label': '删除模块',
|
|
@@ -237,21 +283,19 @@ window.__ModuleLoader__.load({
|
|
|
237
283
|
onChange: (ev) => onChange('enabled', ev.target.checked),
|
|
238
284
|
}), '启用')),
|
|
239
285
|
h('div', { className: 'dad-row' },
|
|
240
|
-
h(Field, { label: '
|
|
241
|
-
h('
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
onChange: (ev) => onChange('extraArgs', ev.target.value),
|
|
254
|
-
}))),
|
|
286
|
+
h(Field, { label: '构建命令(可左侧选工具自动填充,再手动补任务/参数)', wide: true },
|
|
287
|
+
h('div', { className: 'dad-inline' },
|
|
288
|
+
h('select', {
|
|
289
|
+
className: 'dad-select', 'aria-label': '构建工具',
|
|
290
|
+
value: (BUILD_PRESETS.find((p) => p.cmd === (mod.buildCmd || '').trim()) || {}).cmd || '',
|
|
291
|
+
onChange: (ev) => { if (ev.target.value) onChange('buildCmd', ev.target.value) },
|
|
292
|
+
},
|
|
293
|
+
h('option', { value: '' }, '构建工具…'),
|
|
294
|
+
BUILD_PRESETS.map((p) => h('option', { key: p.cmd, value: p.cmd }, p.tool))),
|
|
295
|
+
h('input', {
|
|
296
|
+
className: 'dad-input', type: 'text', value: mod.buildCmd, 'aria-label': '构建命令',
|
|
297
|
+
onChange: (ev) => onChange('buildCmd', ev.target.value),
|
|
298
|
+
})))),
|
|
255
299
|
h('div', { className: 'dad-row' },
|
|
256
300
|
h(Field, { label: '本地产物路径' },
|
|
257
301
|
h('input', {
|
|
@@ -262,7 +306,13 @@ window.__ModuleLoader__.load({
|
|
|
262
306
|
h('input', {
|
|
263
307
|
className: 'dad-input', type: 'text', value: mod.remotePath, 'aria-label': '远程目标路径',
|
|
264
308
|
onChange: (ev) => onChange('remotePath', ev.target.value),
|
|
265
|
-
}))
|
|
309
|
+
})),
|
|
310
|
+
h(Field, { label: 'SSH 目标' },
|
|
311
|
+
h('select', {
|
|
312
|
+
className: 'dad-select', value: mod.sshId || '', 'aria-label': 'SSH 目标',
|
|
313
|
+
onChange: (ev) => onChange('sshId', ev.target.value),
|
|
314
|
+
},
|
|
315
|
+
(sshs || []).map((s) => h('option', { key: s.id, value: s.id }, s.name || s.id))))),
|
|
266
316
|
h('div', { className: 'dad-row' },
|
|
267
317
|
h(Field, { label: '部署后远程命令(可空)' },
|
|
268
318
|
h('input', {
|
|
@@ -290,12 +340,15 @@ window.__ModuleLoader__.load({
|
|
|
290
340
|
function EnvForm({
|
|
291
341
|
env, isNew, groups, message, open, onToggle,
|
|
292
342
|
onPatch, onPatchSsh, onPatchModule, onAddModule, onDeleteModule,
|
|
293
|
-
onSave, onDelete, onBrowse,
|
|
343
|
+
onSave, onDelete, onBrowse, onBrowseKey,
|
|
344
|
+
onAddSsh, onDeleteSsh, onTestSsh, sshTest,
|
|
294
345
|
jdks, jdkOpen, jdkRef, onJdkFocus, onJdkPick,
|
|
346
|
+
nodes, nodeOpen, nodeRef, nodeErr, onNodeFocus, onNodePick,
|
|
347
|
+
pms, pmOpen, pmRef, pmErr, onPmFocus, onPmPick,
|
|
295
348
|
}) {
|
|
296
349
|
const title = env ? '环境配置' : '选择左侧环境进行编辑'
|
|
297
350
|
const groupOptions = groups.map((g) => h('option', { key: g, value: g }))
|
|
298
|
-
const
|
|
351
|
+
const sshList = env ? (env.sshs || []) : []
|
|
299
352
|
return h('div', { className: 'dad-card' },
|
|
300
353
|
h('div', { className: 'dad-card-head' },
|
|
301
354
|
h('span', { className: 'dad-card-title', title }, title),
|
|
@@ -343,20 +396,37 @@ window.__ModuleLoader__.load({
|
|
|
343
396
|
}),
|
|
344
397
|
h('button', { type: 'button', className: 'dad-btn', onClick: onBrowse }, '浏览…'))),
|
|
345
398
|
h(Field, { label: 'JDK 路径(JAVA_HOME,可留空)', wide: true },
|
|
346
|
-
h(
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
399
|
+
h(PathPicker, {
|
|
400
|
+
value: env.jdkHome, aria: 'JDK 路径', items: jdks, open: jdkOpen, wrapRef: jdkRef,
|
|
401
|
+
onFocus: onJdkFocus, onChange: (v) => onPatch({ jdkHome: v }), onPick: onJdkPick,
|
|
402
|
+
emptyText: '未发现可用 JDK',
|
|
403
|
+
}))),
|
|
404
|
+
h('div', { className: 'dad-row' },
|
|
405
|
+
h(Field, { label: 'Node 路径(构建时加入 PATH,可留空)', wide: true },
|
|
406
|
+
h(PathPicker, {
|
|
407
|
+
value: env.nodeHome || '', aria: 'Node 路径', items: nodes, open: nodeOpen, wrapRef: nodeRef,
|
|
408
|
+
onFocus: onNodeFocus, onChange: (v) => onPatch({ nodeHome: v }), onPick: onNodePick,
|
|
409
|
+
emptyText: nodeErr || '未发现已装 Node(支持 nvm / n / fnm / volta)',
|
|
410
|
+
})),
|
|
411
|
+
h(Field, { label: 'Node options(NODE_OPTIONS,可留空)' },
|
|
412
|
+
h('input', {
|
|
413
|
+
className: 'dad-input', type: 'text', value: env.nodeOptions || '', 'aria-label': 'Node options',
|
|
414
|
+
onChange: (ev) => onPatch({ nodeOptions: ev.target.value }),
|
|
415
|
+
}))),
|
|
416
|
+
h('div', { className: 'dad-row' },
|
|
417
|
+
h(Field, { label: '包管理器路径(如 …/pnpm,构建时加入 PATH,可留空)', wide: true },
|
|
418
|
+
h(PathPicker, {
|
|
419
|
+
value: env.pmPath || '', aria: '包管理器路径', items: pms, open: pmOpen, wrapRef: pmRef,
|
|
420
|
+
onFocus: onPmFocus, onChange: (v) => onPatch({ pmPath: v }), onPick: onPmPick,
|
|
421
|
+
emptyText: pmErr || '未发现 pnpm / yarn(npm 随 Node 自动可用)',
|
|
422
|
+
}))),
|
|
423
|
+
h('div', { className: 'dad-row' },
|
|
424
|
+
h(Field, { label: '构建环境变量(KEY=VALUE 多行,构建时注入所有模块,可留空)', wide: true },
|
|
425
|
+
h('textarea', {
|
|
426
|
+
className: 'dad-textarea', rows: 3, spellCheck: false, value: env.buildEnv || '',
|
|
427
|
+
'aria-label': '构建环境变量',
|
|
428
|
+
onChange: (ev) => onPatch({ buildEnv: ev.target.value }),
|
|
429
|
+
}))),
|
|
360
430
|
h('div', { className: 'dad-row' },
|
|
361
431
|
h(Field, { label: '过期时间 expTime(YYYY-MM-DD,可留空)' },
|
|
362
432
|
h('input', {
|
|
@@ -364,44 +434,81 @@ window.__ModuleLoader__.load({
|
|
|
364
434
|
onChange: (ev) => onPatch({ expTime: ev.target.value }),
|
|
365
435
|
})))),
|
|
366
436
|
h('div', { className: 'dad-sec' },
|
|
367
|
-
h('div', { className: 'dad-sec-title' }, 'SSH'),
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
437
|
+
h('div', { className: 'dad-sec-title' }, 'SSH 目标(模块可选发布到任一目标)'),
|
|
438
|
+
sshList.map((s, i) => {
|
|
439
|
+
const t = (sshTest || {})[i]
|
|
440
|
+
return h('div', { key: s.id || i, className: 'dad-sshtarget' },
|
|
441
|
+
h('div', { className: 'dad-row' },
|
|
442
|
+
h(Field, { label: '目标名称' },
|
|
443
|
+
h('input', {
|
|
444
|
+
className: 'dad-input', type: 'text', value: s.name, 'aria-label': 'SSH 目标名称',
|
|
445
|
+
onChange: (ev) => onPatchSsh(i, { name: ev.target.value }),
|
|
446
|
+
})),
|
|
447
|
+
h(Field, { label: 'ID(字母/数字/连字符)' },
|
|
448
|
+
h('input', {
|
|
449
|
+
className: 'dad-input', type: 'text', value: s.id, 'aria-label': 'SSH 目标 ID',
|
|
450
|
+
onChange: (ev) => onPatchSsh(i, { id: ev.target.value }),
|
|
451
|
+
})),
|
|
452
|
+
sshList.length > 1
|
|
453
|
+
? h('button', {
|
|
454
|
+
type: 'button', className: 'dad-btndel', title: '删除目标', 'aria-label': '删除 SSH 目标',
|
|
455
|
+
onClick: () => onDeleteSsh(i),
|
|
456
|
+
}, '×')
|
|
457
|
+
: null),
|
|
458
|
+
h('div', { className: 'dad-row' },
|
|
459
|
+
h(Field, { label: 'Host' },
|
|
460
|
+
h('input', {
|
|
461
|
+
className: 'dad-input', type: 'text', value: s.host, 'aria-label': 'SSH Host',
|
|
462
|
+
onChange: (ev) => onPatchSsh(i, { host: ev.target.value }),
|
|
463
|
+
})),
|
|
464
|
+
h(Field, { label: 'Port' },
|
|
465
|
+
h('input', {
|
|
466
|
+
className: 'dad-input', type: 'number', value: s.port, 'aria-label': 'SSH Port',
|
|
467
|
+
onChange: (ev) => onPatchSsh(i, { port: ev.target.value }),
|
|
468
|
+
})),
|
|
469
|
+
h(Field, { label: 'User' },
|
|
470
|
+
h('input', {
|
|
471
|
+
className: 'dad-input', type: 'text', value: s.user, 'aria-label': 'SSH User',
|
|
472
|
+
onChange: (ev) => onPatchSsh(i, { user: ev.target.value }),
|
|
473
|
+
}))),
|
|
474
|
+
h('div', { className: 'dad-row' },
|
|
475
|
+
h(Field, { label: '密码(留空=ssh-agent/密钥)' },
|
|
476
|
+
h('input', {
|
|
477
|
+
className: 'dad-input', type: 'password', value: s.password, 'aria-label': 'SSH 密码',
|
|
478
|
+
autoComplete: 'new-password',
|
|
479
|
+
onChange: (ev) => onPatchSsh(i, { password: ev.target.value }),
|
|
480
|
+
})),
|
|
481
|
+
h(Field, { label: '私钥路径(留空=默认密钥/agent,优先于选项里的 -i)', wide: true },
|
|
482
|
+
h('div', { className: 'dad-inline' },
|
|
483
|
+
h('input', {
|
|
484
|
+
className: 'dad-input', type: 'text', value: s.keyFile || '', 'aria-label': '私钥路径',
|
|
485
|
+
onChange: (ev) => onPatchSsh(i, { keyFile: ev.target.value }),
|
|
486
|
+
}),
|
|
487
|
+
h('button', { type: 'button', className: 'dad-btn', onClick: () => onBrowseKey(i) }, '浏览…')))),
|
|
488
|
+
h('div', { className: 'dad-row' },
|
|
489
|
+
h(Field, { label: '自定义 ssh 选项(如 -o StrictHostKeyChecking=no)', wide: true },
|
|
490
|
+
h('input', {
|
|
491
|
+
className: 'dad-input', type: 'text', value: s.options, 'aria-label': '自定义 ssh 选项',
|
|
492
|
+
onChange: (ev) => onPatchSsh(i, { options: ev.target.value }),
|
|
493
|
+
}))),
|
|
494
|
+
h('div', { className: 'dad-inline' },
|
|
495
|
+
h('button', {
|
|
496
|
+
type: 'button', className: 'dad-btn', onClick: () => onTestSsh(i),
|
|
497
|
+
disabled: t && t.state === 'run',
|
|
498
|
+
}, t && t.state === 'run' ? '测试中…' : '测试连接'),
|
|
499
|
+
t && t.state !== 'run'
|
|
500
|
+
? h('span', { className: 'dad-msg ' + (t.state === 'ok' ? 'ok' : 'err') }, t.text)
|
|
501
|
+
: null))
|
|
502
|
+
}),
|
|
384
503
|
h('div', { className: 'dad-row' },
|
|
385
|
-
h(
|
|
386
|
-
h('input', {
|
|
387
|
-
className: 'dad-input', type: 'password', value: ssh.password, 'aria-label': 'SSH 密码',
|
|
388
|
-
autoComplete: 'new-password',
|
|
389
|
-
onChange: (ev) => onPatchSsh({ password: ev.target.value }),
|
|
390
|
-
})),
|
|
391
|
-
h(Field, { label: '自定义 ssh 选项(如 -o StrictHostKeyChecking=no)', wide: true },
|
|
392
|
-
h('input', {
|
|
393
|
-
className: 'dad-input', type: 'text', value: ssh.options, 'aria-label': '自定义 ssh 选项',
|
|
394
|
-
onChange: (ev) => onPatchSsh({ options: ev.target.value }),
|
|
395
|
-
}))),
|
|
504
|
+
h('button', { type: 'button', className: 'dad-btn', onClick: onAddSsh }, '+ 添加 SSH 目标')),
|
|
396
505
|
h('div', { className: 'dad-hint' },
|
|
397
|
-
ssh
|
|
398
|
-
? '已配置密码,部署时将使用 sshpass(密码不会写入日志)'
|
|
399
|
-
: '密码留空时使用本机 ssh-agent / 密钥认证')),
|
|
506
|
+
'密码留空时使用本机 ssh-agent / 密钥认证;密码只经 sshpass 环境变量传递,不会写入日志')),
|
|
400
507
|
h('div', { className: 'dad-sec' },
|
|
401
508
|
h('div', { className: 'dad-sec-title' }, '构建 / 发布模块'),
|
|
402
509
|
env.modules.length === 0 ? h('div', { className: 'dad-hint' }, '(暂无模块)') : null,
|
|
403
510
|
env.modules.map((m, i) => h(ModuleCard, {
|
|
404
|
-
key: i, mod: m,
|
|
511
|
+
key: i, mod: m, sshs: sshList,
|
|
405
512
|
onChange: (field, value) => onPatchModule(i, field, value),
|
|
406
513
|
onDelete: () => onDeleteModule(i),
|
|
407
514
|
})),
|
|
@@ -468,14 +575,14 @@ window.__ModuleLoader__.load({
|
|
|
468
575
|
}, state.confirmLabel || '确定'))))
|
|
469
576
|
}
|
|
470
577
|
|
|
471
|
-
function BrowseModal({ state, onClose, onNavigate, onUp, onPick, onToggleHidden }) {
|
|
578
|
+
function BrowseModal({ state, onClose, onNavigate, onUp, onPick, onSelectFile, onToggleHidden }) {
|
|
472
579
|
return h('div', { className: 'dad-mask', role: 'presentation', onClick: onClose },
|
|
473
580
|
h('div', {
|
|
474
|
-
className: 'dad-modal', role: 'dialog', 'aria-modal': 'true', 'aria-label': '选择目录',
|
|
581
|
+
className: 'dad-modal', role: 'dialog', 'aria-modal': 'true', 'aria-label': state.file ? '选择文件' : '选择目录',
|
|
475
582
|
onClick: (ev) => ev.stopPropagation(),
|
|
476
583
|
},
|
|
477
584
|
h('div', { className: 'dad-modal-head' },
|
|
478
|
-
h('strong', null, '选择目录'),
|
|
585
|
+
h('strong', null, state.file ? '选择文件' : '选择目录'),
|
|
479
586
|
h('label', { className: 'dad-chk dad-autoflex' },
|
|
480
587
|
h('input', {
|
|
481
588
|
type: 'checkbox', checked: !!state.hidden,
|
|
@@ -504,16 +611,29 @@ window.__ModuleLoader__.load({
|
|
|
504
611
|
(state.dirs || []).map((dir) => h('button', {
|
|
505
612
|
key: dir, type: 'button', className: 'dad-dir',
|
|
506
613
|
onClick: () => onNavigate(dir),
|
|
507
|
-
}, dir)))
|
|
614
|
+
}, dir))),
|
|
615
|
+
state.file
|
|
616
|
+
? h('div', { className: 'dad-dirs' },
|
|
617
|
+
(state.files || []).length ? null : h('div', { className: 'dad-hint' }, '(无文件)'),
|
|
618
|
+
(state.files || []).map((f) => h('button', {
|
|
619
|
+
key: f, type: 'button', className: 'dad-dir', 'data-sel': state.sel === f ? '1' : '0',
|
|
620
|
+
onClick: () => onSelectFile(f),
|
|
621
|
+
}, f)))
|
|
622
|
+
: null),
|
|
508
623
|
h('div', { className: 'dad-modal-foot' },
|
|
509
624
|
h('button', {
|
|
510
625
|
type: 'button', className: 'dad-btn', onClick: onUp,
|
|
511
626
|
disabled: state.loading || !state.path || state.path === '/',
|
|
512
627
|
}, '上级'),
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
628
|
+
state.file
|
|
629
|
+
? h('button', {
|
|
630
|
+
type: 'button', className: 'dad-btn dad-btn-primary', onClick: onPick,
|
|
631
|
+
disabled: state.loading || !state.path || !state.sel,
|
|
632
|
+
}, '选择此文件')
|
|
633
|
+
: h('button', {
|
|
634
|
+
type: 'button', className: 'dad-btn dad-btn-primary', onClick: onPick,
|
|
635
|
+
disabled: state.loading || !state.path,
|
|
636
|
+
}, '选择当前目录'))))
|
|
517
637
|
}
|
|
518
638
|
|
|
519
639
|
// ---------- 主面板 ----------
|
|
@@ -533,6 +653,17 @@ window.__ModuleLoader__.load({
|
|
|
533
653
|
const [jdkOpen, setJdkOpen] = useState(false)
|
|
534
654
|
const jdkRef = useRef(null)
|
|
535
655
|
|
|
656
|
+
const [nodes, setNodes] = useState([])
|
|
657
|
+
const [nodeErr, setNodeErr] = useState('')
|
|
658
|
+
|
|
659
|
+
const [pms, setPms] = useState([])
|
|
660
|
+
const [pmErr, setPmErr] = useState('')
|
|
661
|
+
const [pmOpen, setPmOpen] = useState(false)
|
|
662
|
+
const pmRef = useRef(null)
|
|
663
|
+
const [sshTest, setSshTest] = useState({}) // { [目标index]: { state: 'run'|'ok'|'err', text } }
|
|
664
|
+
const [nodeOpen, setNodeOpen] = useState(false)
|
|
665
|
+
const nodeRef = useRef(null)
|
|
666
|
+
|
|
536
667
|
const [browse, setBrowse] = useState(null)
|
|
537
668
|
|
|
538
669
|
const [selected, setSelected] = useState([])
|
|
@@ -550,6 +681,7 @@ window.__ModuleLoader__.load({
|
|
|
550
681
|
const logRef = useRef(null)
|
|
551
682
|
const esRef = useRef(null)
|
|
552
683
|
const importRef = useRef(null)
|
|
684
|
+
const keyPickRef = useRef(null) // 文件浏览弹窗的选中回调(私钥选择用)
|
|
553
685
|
const dragIdRef = useRef(null)
|
|
554
686
|
const [draggingId, setDraggingId] = useState('')
|
|
555
687
|
const [dragOverId, setDragOverId] = useState('')
|
|
@@ -679,16 +811,17 @@ window.__ModuleLoader__.load({
|
|
|
679
811
|
if (el) el.scrollTop = el.scrollHeight
|
|
680
812
|
}, [logText])
|
|
681
813
|
|
|
682
|
-
// JDK 下拉:点击空白处关闭
|
|
814
|
+
// JDK/Node/包管理器 下拉:点击空白处关闭
|
|
683
815
|
useEffect(() => {
|
|
684
|
-
if (!jdkOpen) return undefined
|
|
816
|
+
if (!jdkOpen && !nodeOpen && !pmOpen) return undefined
|
|
685
817
|
function onDocDown(ev) {
|
|
686
|
-
|
|
687
|
-
if (
|
|
818
|
+
if (jdkOpen && jdkRef.current && !jdkRef.current.contains(ev.target)) setJdkOpen(false)
|
|
819
|
+
if (nodeOpen && nodeRef.current && !nodeRef.current.contains(ev.target)) setNodeOpen(false)
|
|
820
|
+
if (pmOpen && pmRef.current && !pmRef.current.contains(ev.target)) setPmOpen(false)
|
|
688
821
|
}
|
|
689
822
|
document.addEventListener('mousedown', onDocDown)
|
|
690
823
|
return () => document.removeEventListener('mousedown', onDocDown)
|
|
691
|
-
}, [jdkOpen])
|
|
824
|
+
}, [jdkOpen, nodeOpen, pmOpen])
|
|
692
825
|
|
|
693
826
|
// ---------- 环境操作 ----------
|
|
694
827
|
|
|
@@ -700,8 +833,8 @@ window.__ModuleLoader__.load({
|
|
|
700
833
|
function addEnv() {
|
|
701
834
|
const env = {
|
|
702
835
|
id: newEnvId(envs), name: '新环境', note: '', group: '',
|
|
703
|
-
projectPath: defProject, jdkHome: '', expTime: '',
|
|
704
|
-
|
|
836
|
+
projectPath: defProject, jdkHome: '', nodeHome: '', nodeOptions: '', pmPath: '', buildEnv: '', expTime: '',
|
|
837
|
+
sshs: [{ id: 'default', name: '默认', host: '', port: 22, user: 'root', password: '', keyFile: '', options: '' }],
|
|
705
838
|
modules: [],
|
|
706
839
|
}
|
|
707
840
|
setDraft(env)
|
|
@@ -834,8 +967,43 @@ window.__ModuleLoader__.load({
|
|
|
834
967
|
function patchEnv(patch) {
|
|
835
968
|
setDraft((prev) => (prev ? { ...prev, ...patch } : prev))
|
|
836
969
|
}
|
|
837
|
-
function patchSsh(patch) {
|
|
838
|
-
setDraft((prev) => (prev
|
|
970
|
+
function patchSsh(index, patch) {
|
|
971
|
+
setDraft((prev) => (prev
|
|
972
|
+
? { ...prev, sshs: (prev.sshs || []).map((s, i) => (i === index ? { ...s, ...patch } : s)) }
|
|
973
|
+
: prev))
|
|
974
|
+
}
|
|
975
|
+
function addSsh() {
|
|
976
|
+
setDraft((prev) => {
|
|
977
|
+
if (!prev) return prev
|
|
978
|
+
const n = (prev.sshs || []).length + 1
|
|
979
|
+
return {
|
|
980
|
+
...prev,
|
|
981
|
+
sshs: [...(prev.sshs || []), { id: 'ssh-' + Date.now().toString(36), name: 'SSH ' + n, host: '', port: 22, user: 'root', password: '', keyFile: '', options: '' }],
|
|
982
|
+
}
|
|
983
|
+
})
|
|
984
|
+
}
|
|
985
|
+
function deleteSsh(index) {
|
|
986
|
+
setDraft((prev) => (prev && (prev.sshs || []).length > 1
|
|
987
|
+
? { ...prev, sshs: prev.sshs.filter((_, i) => i !== index) }
|
|
988
|
+
: prev))
|
|
989
|
+
}
|
|
990
|
+
// 测试连接:直接用表单当前值(无需先保存环境)
|
|
991
|
+
function runSshTest(index) {
|
|
992
|
+
const t = draft && draft.sshs && draft.sshs[index]
|
|
993
|
+
if (!t) return
|
|
994
|
+
setSshTest((p) => ({ ...p, [index]: { state: 'run', text: '测试中…' } }))
|
|
995
|
+
rpc('sshTest', {
|
|
996
|
+
target: {
|
|
997
|
+
host: (t.host || '').trim(), port: Number(t.port) || 22,
|
|
998
|
+
user: (t.user || '').trim() || 'root', password: t.password || '',
|
|
999
|
+
keyFile: (t.keyFile || '').trim(), options: (t.options || '').trim(),
|
|
1000
|
+
},
|
|
1001
|
+
})
|
|
1002
|
+
.then((r) => setSshTest((p) => ({
|
|
1003
|
+
...p,
|
|
1004
|
+
[index]: r && r.ok ? { state: 'ok', text: '连接成功(' + r.ms + ' ms)' } : { state: 'err', text: '失败:' + ((r && r.error) || '未知错误') },
|
|
1005
|
+
})))
|
|
1006
|
+
.catch((e) => setSshTest((p) => ({ ...p, [index]: { state: 'err', text: '失败:' + String(e?.message || e) } })))
|
|
839
1007
|
}
|
|
840
1008
|
function patchModule(index, field, value) {
|
|
841
1009
|
setDraft((prev) => {
|
|
@@ -877,22 +1045,28 @@ window.__ModuleLoader__.load({
|
|
|
877
1045
|
group: (draft.group || '').trim(),
|
|
878
1046
|
projectPath: (draft.projectPath || '').trim() || defProject,
|
|
879
1047
|
jdkHome: (draft.jdkHome || '').trim(),
|
|
1048
|
+
nodeHome: (draft.nodeHome || '').trim(),
|
|
1049
|
+
nodeOptions: (draft.nodeOptions || '').trim(),
|
|
1050
|
+
pmPath: (draft.pmPath || '').trim(),
|
|
1051
|
+
buildEnv: draft.buildEnv || '',
|
|
880
1052
|
expTime: draft.expTime || '',
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1053
|
+
sshs: (draft.sshs || []).map((s) => ({
|
|
1054
|
+
id: (s.id || '').trim(),
|
|
1055
|
+
name: (s.name || '').trim(),
|
|
1056
|
+
host: (s.host || '').trim(),
|
|
1057
|
+
port: Number(s.port) || 22,
|
|
1058
|
+
user: (s.user || '').trim() || 'root',
|
|
1059
|
+
password: s.password || '',
|
|
1060
|
+
keyFile: (s.keyFile || '').trim(),
|
|
1061
|
+
options: (s.options || '').trim(),
|
|
1062
|
+
})),
|
|
888
1063
|
modules: draft.modules.map((m) => ({
|
|
889
1064
|
id: (m.id || '').trim(),
|
|
890
1065
|
name: (m.name || '').trim() || '新模块',
|
|
891
1066
|
kind: m.kind === 'server' ? 'server' : 'front',
|
|
892
1067
|
enabled: !!m.enabled,
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
extraArgs: (m.extraArgs || '').trim(),
|
|
1068
|
+
buildCmd: (m.buildCmd || '').trim(),
|
|
1069
|
+
sshId: (m.sshId || '').trim(),
|
|
896
1070
|
localPath: (m.localPath || '').trim(),
|
|
897
1071
|
remotePath: (m.remotePath || '').trim(),
|
|
898
1072
|
postCommand: (m.postCommand || '').trim(),
|
|
@@ -914,30 +1088,36 @@ window.__ModuleLoader__.load({
|
|
|
914
1088
|
|
|
915
1089
|
// ---------- 路径浏览 ----------
|
|
916
1090
|
|
|
917
|
-
function loadBrowse(path, hidden) {
|
|
1091
|
+
function loadBrowse(path, hidden, file) {
|
|
918
1092
|
setBrowse((prev) => ({
|
|
919
1093
|
path: prev ? prev.path : '',
|
|
920
1094
|
parent: prev ? prev.parent : '',
|
|
921
1095
|
dirs: prev ? prev.dirs : [],
|
|
922
1096
|
worktrees: prev ? prev.worktrees : [],
|
|
1097
|
+
files: prev && prev.file === !!file ? prev.files : [],
|
|
1098
|
+
file: !!file, sel: '',
|
|
923
1099
|
hidden: !!hidden, loading: true, error: '',
|
|
924
1100
|
}))
|
|
925
1101
|
const args = {}
|
|
926
1102
|
if (path) args.path = path
|
|
927
1103
|
if (hidden) args.hidden = true
|
|
1104
|
+
if (file) args.file = true
|
|
928
1105
|
rpc('browse', args).then((d) => {
|
|
929
1106
|
setBrowse({
|
|
930
1107
|
path: (d && d.path) || '',
|
|
931
1108
|
parent: (d && d.parent) || parentPath((d && d.path) || ''),
|
|
932
1109
|
dirs: Array.isArray(d && d.dirs) ? d.dirs : [],
|
|
1110
|
+
files: Array.isArray(d && d.files) ? d.files : [],
|
|
933
1111
|
worktrees: Array.isArray(d && d.worktrees) ? d.worktrees : [],
|
|
1112
|
+
file: !!file, sel: '',
|
|
934
1113
|
hidden: !!hidden, loading: false, error: '',
|
|
935
1114
|
})
|
|
936
1115
|
}).catch((error) => {
|
|
937
1116
|
setBrowse((prev) => ({
|
|
938
1117
|
path: prev ? prev.path : '',
|
|
939
1118
|
parent: prev ? prev.parent : '',
|
|
940
|
-
dirs: [], worktrees: [],
|
|
1119
|
+
dirs: [], worktrees: [], files: [],
|
|
1120
|
+
file: !!file, sel: '',
|
|
941
1121
|
hidden: !!hidden, loading: false,
|
|
942
1122
|
error: error.message || String(error),
|
|
943
1123
|
}))
|
|
@@ -945,24 +1125,39 @@ window.__ModuleLoader__.load({
|
|
|
945
1125
|
}
|
|
946
1126
|
function openBrowse() {
|
|
947
1127
|
if (!draft) return
|
|
948
|
-
|
|
1128
|
+
keyPickRef.current = null
|
|
1129
|
+
loadBrowse((draft.projectPath || '').trim(), false, false)
|
|
1130
|
+
}
|
|
1131
|
+
// 选择私钥文件:从当前 keyFile 所在目录(或 ~/.ssh)起步,默认显示隐藏文件(密钥多为点开头无,但 .ssh 是隐藏目录)
|
|
1132
|
+
function openKeyBrowse(index) {
|
|
1133
|
+
const t = draft && draft.sshs && draft.sshs[index]
|
|
1134
|
+
keyPickRef.current = (p) => patchSsh(index, { keyFile: p })
|
|
1135
|
+
loadBrowse(t && t.keyFile ? parentPath(t.keyFile) : '~/.ssh', true, true)
|
|
949
1136
|
}
|
|
950
1137
|
function navigateBrowse(target) {
|
|
951
1138
|
if (!browse) return
|
|
952
|
-
loadBrowse(joinPath(browse.path, target), browse.hidden)
|
|
1139
|
+
loadBrowse(joinPath(browse.path, target), browse.hidden, browse.file)
|
|
953
1140
|
}
|
|
954
1141
|
function browseUp() {
|
|
955
1142
|
if (!browse) return
|
|
956
|
-
loadBrowse(browse.parent || parentPath(browse.path), browse.hidden)
|
|
1143
|
+
loadBrowse(browse.parent || parentPath(browse.path), browse.hidden, browse.file)
|
|
957
1144
|
}
|
|
958
1145
|
function browsePick() {
|
|
959
1146
|
if (!browse || !browse.path) return
|
|
960
|
-
|
|
1147
|
+
if (browse.file) {
|
|
1148
|
+
if (!browse.sel) return
|
|
1149
|
+
if (keyPickRef.current) keyPickRef.current(joinPath(browse.path, browse.sel))
|
|
1150
|
+
} else {
|
|
1151
|
+
patchEnv({ projectPath: browse.path })
|
|
1152
|
+
}
|
|
961
1153
|
setBrowse(null)
|
|
962
1154
|
}
|
|
1155
|
+
function browseSelectFile(name) {
|
|
1156
|
+
setBrowse((prev) => (prev ? { ...prev, sel: name } : prev))
|
|
1157
|
+
}
|
|
963
1158
|
function browseToggleHidden(hidden) {
|
|
964
1159
|
if (!browse) return
|
|
965
|
-
loadBrowse(browse.path, hidden)
|
|
1160
|
+
loadBrowse(browse.path, hidden, browse.file)
|
|
966
1161
|
}
|
|
967
1162
|
|
|
968
1163
|
// ---------- JDK ----------
|
|
@@ -978,6 +1173,32 @@ window.__ModuleLoader__.load({
|
|
|
978
1173
|
setJdkOpen(false)
|
|
979
1174
|
}
|
|
980
1175
|
|
|
1176
|
+
// ---------- Node ----------
|
|
1177
|
+
|
|
1178
|
+
function openNode() {
|
|
1179
|
+
setNodeOpen(true)
|
|
1180
|
+
if (!nodes.length) {
|
|
1181
|
+
rpc('nodes').then((list) => setNodes(Array.isArray(list) ? list : [])).catch((e) => setNodeErr(String(e?.message || e)))
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
function pickNode(path) {
|
|
1185
|
+
patchEnv({ nodeHome: path })
|
|
1186
|
+
setNodeOpen(false)
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// ---------- 包管理器 ----------
|
|
1190
|
+
|
|
1191
|
+
function openPm() {
|
|
1192
|
+
setPmOpen(true)
|
|
1193
|
+
if (!pms.length) {
|
|
1194
|
+
rpc('packageManagers').then((list) => setPms(Array.isArray(list) ? list : [])).catch((e) => setPmErr(String(e?.message || e)))
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
function pickPm(path) {
|
|
1198
|
+
patchEnv({ pmPath: path })
|
|
1199
|
+
setPmOpen(false)
|
|
1200
|
+
}
|
|
1201
|
+
|
|
981
1202
|
// ---------- 部署 ----------
|
|
982
1203
|
|
|
983
1204
|
function toggleSel(id) {
|
|
@@ -1013,11 +1234,13 @@ window.__ModuleLoader__.load({
|
|
|
1013
1234
|
lines.push('')
|
|
1014
1235
|
lines.push('将依次执行:')
|
|
1015
1236
|
for (const m of mods) {
|
|
1016
|
-
|
|
1237
|
+
// expTime 追加规则与服务端一致:仅首 token 是 gradle 的 server 模块
|
|
1238
|
+
const exp = m.kind === 'server' && env.expTime && ((m.buildCmd || '').trim().split(/\s+/)[0] || '').includes('gradle') ? ' -PexpTime=' + env.expTime : ''
|
|
1017
1239
|
lines.push('')
|
|
1018
1240
|
lines.push('[' + (m.name || m.id) + ']')
|
|
1019
|
-
|
|
1020
|
-
lines.push('
|
|
1241
|
+
const target = targetOf(env, m)
|
|
1242
|
+
lines.push(' 构建: ' + m.buildCmd + exp)
|
|
1243
|
+
lines.push(' rsync: ' + m.localPath + ' → ' + (target.user || 'root') + '@' + (target.host || '?') + ':' + m.remotePath + (env.sshs && env.sshs.length > 1 ? '(目标: ' + (target.name || target.id) + ')' : ''))
|
|
1021
1244
|
if (m.postCommand) lines.push(' ssh: ' + m.postCommand)
|
|
1022
1245
|
}
|
|
1023
1246
|
return lines.join('\n')
|
|
@@ -1026,8 +1249,12 @@ window.__ModuleLoader__.load({
|
|
|
1026
1249
|
function requestDeploy() {
|
|
1027
1250
|
if (!draft || !selected.length) return
|
|
1028
1251
|
if (isNew) { setDeployState({ running: false, text: '请先保存环境再执行部署', kind: 'err' }); return }
|
|
1029
|
-
|
|
1030
|
-
|
|
1252
|
+
const deployTargets = [...new Set(selected.map((id) => {
|
|
1253
|
+
const m = moduleById[id]
|
|
1254
|
+
return m && targetOf(draft, m)
|
|
1255
|
+
}).filter(Boolean))]
|
|
1256
|
+
if (!deployTargets.length || deployTargets.some((t) => !t.host || !t.user)) {
|
|
1257
|
+
setDeployState({ running: false, text: '所选模块的 SSH 目标配置不完整,请先保存 SSH 配置', kind: 'err' })
|
|
1031
1258
|
return
|
|
1032
1259
|
}
|
|
1033
1260
|
const mods = selected.map((id) => moduleById[id]).filter((m) => m && m.enabled)
|
|
@@ -1124,7 +1351,7 @@ window.__ModuleLoader__.load({
|
|
|
1124
1351
|
const canRun = !!draft && !isNew && selected.length > 0 && !deployState.running
|
|
1125
1352
|
|
|
1126
1353
|
// 标题行:对象名 + 一句「现状」(ID / 最近部署结果 / SSH 目标),全部来自已有数据
|
|
1127
|
-
const selSsh = draft && draft.
|
|
1354
|
+
const selSsh = draft && draft.sshs && draft.sshs[0] ? draft.sshs[0] : {}
|
|
1128
1355
|
const selSshText = selSsh.host ? (selSsh.user || 'root') + '@' + selSsh.host + ':' + (selSsh.port || 22) : ''
|
|
1129
1356
|
const selBuild = draft ? lastBuild[draft.id] : null
|
|
1130
1357
|
let titleSub = '选择左侧环境,或新增一个环境'
|
|
@@ -1283,13 +1510,16 @@ window.__ModuleLoader__.load({
|
|
|
1283
1510
|
onToggle: () => setFormOpen((v) => !v),
|
|
1284
1511
|
onPatch: patchEnv, onPatchSsh: patchSsh, onPatchModule: patchModule,
|
|
1285
1512
|
onAddModule: addModule, onDeleteModule: askDeleteModule,
|
|
1286
|
-
|
|
1513
|
+
onAddSsh: addSsh, onDeleteSsh: deleteSsh, onTestSsh: runSshTest, sshTest,
|
|
1514
|
+
onSave: saveEnv, onDelete: deleteCurrentEnv, onBrowse: openBrowse, onBrowseKey: openKeyBrowse,
|
|
1287
1515
|
jdks, jdkOpen, jdkRef, onJdkFocus: openJdk, onJdkPick: pickJdk,
|
|
1516
|
+
nodes, nodeOpen, nodeRef, nodeErr, onNodeFocus: openNode, onNodePick: pickNode,
|
|
1517
|
+
pms, pmOpen, pmRef, pmErr, onPmFocus: openPm, onPmPick: pickPm,
|
|
1288
1518
|
})),
|
|
1289
1519
|
confirmState ? h(ConfirmDialog, { state: confirmState, onClose: () => setConfirmState(null) }) : null,
|
|
1290
1520
|
browse ? h(BrowseModal, {
|
|
1291
1521
|
state: browse, onClose: () => setBrowse(null),
|
|
1292
|
-
onNavigate: navigateBrowse, onUp: browseUp, onPick: browsePick,
|
|
1522
|
+
onNavigate: navigateBrowse, onUp: browseUp, onPick: browsePick, onSelectFile: browseSelectFile,
|
|
1293
1523
|
onToggleHidden: browseToggleHidden,
|
|
1294
1524
|
}) : null)
|
|
1295
1525
|
}
|
|
@@ -1376,6 +1606,7 @@ window.__ModuleLoader__.load({
|
|
|
1376
1606
|
'.dad-btn-danger-solid:hover:not(:disabled){background:var(--dsw-alias-state-error-primary)}',
|
|
1377
1607
|
'.dad-formfoot{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:4px}',
|
|
1378
1608
|
'.dad-mcard{position:relative;min-width:0;padding:10px 12px;margin-bottom:10px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-layer-2)}',
|
|
1609
|
+
'.dad-sshtarget{position:relative;min-width:0;padding:8px 10px 10px;margin-bottom:8px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-layer-1)}',
|
|
1379
1610
|
'.dad-mcard[data-off="1"]{opacity:.6}',
|
|
1380
1611
|
'.dad-btndel{position:absolute;top:4px;right:6px;border:0;background:none;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:1;padding:2px 4px;cursor:pointer}',
|
|
1381
1612
|
'.dad-btndel:hover{color:var(--dsw-alias-state-error-primary)}',
|