feat: 修改前端显示
This commit is contained in:
@@ -56,6 +56,27 @@ test('manages a task and reads a real run log', async ({ page, request }) => {
|
|||||||
path: 'test-results/dashboard-task-editor.png',
|
path: 'test-results/dashboard-task-editor.png',
|
||||||
fullPage: true,
|
fullPage: true,
|
||||||
})
|
})
|
||||||
|
const addArgumentButton = newTaskDialog.getByRole('button', {
|
||||||
|
name: '+ 添加参数',
|
||||||
|
})
|
||||||
|
await addArgumentButton.focus()
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
await newTaskDialog
|
||||||
|
.getByPlaceholder('参数名 (如 --mode 或 -v)')
|
||||||
|
.fill('--mode')
|
||||||
|
await newTaskDialog
|
||||||
|
.getByPlaceholder('参数值 (可选,无值可留空)')
|
||||||
|
.fill('full')
|
||||||
|
await newTaskDialog
|
||||||
|
.getByText('调用预览 (Runtime Preview)')
|
||||||
|
.scrollIntoViewIfNeeded()
|
||||||
|
await expect(
|
||||||
|
newTaskDialog.getByText('--mode full', { exact: false }),
|
||||||
|
).toBeVisible()
|
||||||
|
await page.screenshot({
|
||||||
|
path: 'test-results/dashboard-task-editor-runtime.png',
|
||||||
|
fullPage: true,
|
||||||
|
})
|
||||||
await page.keyboard.press('Escape')
|
await page.keyboard.press('Escape')
|
||||||
await expect(newTaskDialog).toBeHidden()
|
await expect(newTaskDialog).toBeHidden()
|
||||||
|
|
||||||
@@ -75,7 +96,7 @@ test('manages a task and reads a real run log', async ({ page, request }) => {
|
|||||||
name: '编辑任务 [e2e_echo]',
|
name: '编辑任务 [e2e_echo]',
|
||||||
})
|
})
|
||||||
await expect(editor).toBeVisible()
|
await expect(editor).toBeVisible()
|
||||||
const nameInput = editor.getByLabel('任务名称')
|
const nameInput = editor.getByLabel('名称')
|
||||||
await nameInput.fill('E2E updated')
|
await nameInput.fill('E2E updated')
|
||||||
await editor.getByRole('button', { name: '保存修改' }).click()
|
await editor.getByRole('button', { name: '保存修改' }).click()
|
||||||
await expect(page.getByText('E2E updated')).toBeVisible()
|
await expect(page.getByText('E2E updated')).toBeVisible()
|
||||||
|
|||||||
+52
-16
@@ -648,6 +648,12 @@ code {
|
|||||||
max-height: 75vh;
|
max-height: 75vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-editor .modal-body {
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
.form-grid {
|
.form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -659,6 +665,10 @@ code {
|
|||||||
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 0.7fr);
|
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 0.7fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-grid-single {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.cron-header-row {
|
.cron-header-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -804,7 +814,7 @@ code {
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-panel {
|
.dynamic-fields {
|
||||||
margin: 22px 0 0;
|
margin: 22px 0 0;
|
||||||
border: 1px solid #eeeff2;
|
border: 1px solid #eeeff2;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@@ -812,15 +822,6 @@ code {
|
|||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-panel legend {
|
|
||||||
padding: 0 8px;
|
|
||||||
color: #374151;
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collection-header {
|
.collection-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@@ -853,7 +854,7 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.argument-row {
|
.argument-row {
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr) auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.key-value-row {
|
.key-value-row {
|
||||||
@@ -866,11 +867,46 @@ code {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-actions {
|
.cli-preview {
|
||||||
|
margin-top: 18px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
background: #18191c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-preview-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #282a30;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
color: #71767f;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-preview-body {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-line-env {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #9da3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-line-cmd {
|
||||||
|
color: #34d399;
|
||||||
|
font-weight: 500;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin: 24px -28px -24px;
|
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
padding: 16px 28px;
|
padding: 16px 28px;
|
||||||
background: #fafafb;
|
background: #fafafb;
|
||||||
@@ -1205,8 +1241,7 @@ code {
|
|||||||
padding: 18px;
|
padding: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-actions {
|
.modal-footer {
|
||||||
margin: 24px -18px -18px;
|
|
||||||
padding: 16px 18px;
|
padding: 16px 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1215,7 +1250,8 @@ code {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.key-value-row {
|
.key-value-row,
|
||||||
|
.argument-row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,9 +126,16 @@ describe('TaskDashboard', () => {
|
|||||||
renderApp()
|
renderApp()
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: '+ 新建任务' }))
|
await user.click(screen.getByRole('button', { name: '+ 新建任务' }))
|
||||||
await user.clear(screen.getByLabelText('任务标识'))
|
await user.click(screen.getByRole('button', { name: '+ 添加参数' }))
|
||||||
await user.type(screen.getByLabelText('任务标识'), 'new_task')
|
expect(
|
||||||
await user.type(screen.getByLabelText('任务名称'), 'New task')
|
screen.getByPlaceholderText('参数名 (如 --mode 或 -v)'),
|
||||||
|
).toBeInTheDocument()
|
||||||
|
expect(
|
||||||
|
screen.getByPlaceholderText('参数值 (可选,无值可留空)'),
|
||||||
|
).toBeInTheDocument()
|
||||||
|
await user.clear(screen.getByLabelText(/任务标识/))
|
||||||
|
await user.type(screen.getByLabelText(/任务标识/), 'new_task')
|
||||||
|
await user.type(screen.getByLabelText('名称'), 'New task')
|
||||||
await user.type(screen.getByLabelText('工作目录'), 'D:\\Projects\\new')
|
await user.type(screen.getByLabelText('工作目录'), 'D:\\Projects\\new')
|
||||||
|
|
||||||
await user.click(
|
await user.click(
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
mapServerFieldToFormField,
|
mapServerFieldToFormField,
|
||||||
taskFormSchema,
|
taskFormSchema,
|
||||||
taskToFormValues,
|
taskToFormValues,
|
||||||
|
type KeyValueFormValue,
|
||||||
type TaskFormValues,
|
type TaskFormValues,
|
||||||
} from '../lib/taskForm'
|
} from '../lib/taskForm'
|
||||||
import { useFocusTrap } from '../lib/useFocusTrap'
|
import { useFocusTrap } from '../lib/useFocusTrap'
|
||||||
@@ -53,6 +54,7 @@ export function TaskEditorModal({
|
|||||||
const actionType = watch('actionType')
|
const actionType = watch('actionType')
|
||||||
const cronValue = watch('cron')
|
const cronValue = watch('cron')
|
||||||
const timezoneValue = watch('timezone')
|
const timezoneValue = watch('timezone')
|
||||||
|
const scriptPreview = watch('script')
|
||||||
const debouncedCron = useDebouncedValue(cronValue, 300)
|
const debouncedCron = useDebouncedValue(cronValue, 300)
|
||||||
const debouncedTimezone = useDebouncedValue(timezoneValue, 300)
|
const debouncedTimezone = useDebouncedValue(timezoneValue, 300)
|
||||||
const cronPreview = useQuery({
|
const cronPreview = useQuery({
|
||||||
@@ -169,7 +171,12 @@ export function TaskEditorModal({
|
|||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<form className="modal-body" onSubmit={submit} noValidate>
|
<form
|
||||||
|
id="task-editor-form"
|
||||||
|
className="modal-body"
|
||||||
|
onSubmit={submit}
|
||||||
|
noValidate
|
||||||
|
>
|
||||||
{submitError ? (
|
{submitError ? (
|
||||||
<div className="inline-alert inline-alert-error" role="alert">
|
<div className="inline-alert inline-alert-error" role="alert">
|
||||||
{submitError}
|
{submitError}
|
||||||
@@ -177,7 +184,10 @@ export function TaskEditorModal({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="form-grid">
|
<div className="form-grid">
|
||||||
<Field label="任务标识" error={errors.id?.message}>
|
<Field
|
||||||
|
label="任务标识 (Task ID)"
|
||||||
|
error={errors.id?.message}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
{...register('id')}
|
{...register('id')}
|
||||||
className="field-input mono"
|
className="field-input mono"
|
||||||
@@ -187,7 +197,7 @@ export function TaskEditorModal({
|
|||||||
aria-invalid={Boolean(errors.id)}
|
aria-invalid={Boolean(errors.id)}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="任务名称" error={errors.name?.message}>
|
<Field label="名称" error={errors.name?.message}>
|
||||||
<input
|
<input
|
||||||
{...register('name')}
|
{...register('name')}
|
||||||
className="field-input"
|
className="field-input"
|
||||||
@@ -251,7 +261,7 @@ export function TaskEditorModal({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Field label="超时时间(秒)" error={errors.timeout_secs?.message}>
|
<Field label="超时时间 (秒)" error={errors.timeout_secs?.message}>
|
||||||
<input
|
<input
|
||||||
{...register('timeout_secs', { valueAsNumber: true })}
|
{...register('timeout_secs', { valueAsNumber: true })}
|
||||||
type="number"
|
type="number"
|
||||||
@@ -266,7 +276,7 @@ export function TaskEditorModal({
|
|||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-grid">
|
<div className="form-grid form-grid-single">
|
||||||
<Field label="IANA 时区" error={errors.timezone?.message}>
|
<Field label="IANA 时区" error={errors.timezone?.message}>
|
||||||
<input
|
<input
|
||||||
{...register('timezone')}
|
{...register('timezone')}
|
||||||
@@ -283,26 +293,19 @@ export function TaskEditorModal({
|
|||||||
<option value="America/New_York" />
|
<option value="America/New_York" />
|
||||||
</datalist>
|
</datalist>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="任务类型">
|
<Field label="任务类型 (Action Type)">
|
||||||
<select {...register('actionType')} className="field-input">
|
<select {...register('actionType')} className="field-input">
|
||||||
<option value="script">执行脚本</option>
|
<option value="script">
|
||||||
<option value="http">HTTP 请求</option>
|
执行脚本 (Python / Shell / Node / Lua)
|
||||||
<option value="process">二进制进程</option>
|
</option>
|
||||||
|
<option value="http">发起 HTTP 请求</option>
|
||||||
|
<option value="process">管理常驻/独立二进制</option>
|
||||||
</select>
|
</select>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="checkbox-field">
|
|
||||||
<input type="checkbox" {...register('enabled')} />
|
|
||||||
<span>
|
|
||||||
<strong>立即激活此任务调度</strong>
|
|
||||||
<small>保存后立即注册到 Cron 调度器</small>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{actionType === 'script' ? (
|
{actionType === 'script' ? (
|
||||||
<fieldset className="action-panel">
|
<div className="dynamic-fields">
|
||||||
<legend>脚本配置</legend>
|
|
||||||
<div className="form-grid">
|
<div className="form-grid">
|
||||||
<Field
|
<Field
|
||||||
label="工作目录"
|
label="工作目录"
|
||||||
@@ -344,7 +347,9 @@ export function TaskEditorModal({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="button button-secondary button-small"
|
className="button button-secondary button-small"
|
||||||
onClick={() => scriptArgs.append({ value: '' })}
|
onClick={() =>
|
||||||
|
scriptArgs.append({ key: '', value: '' })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
+ 添加参数
|
+ 添加参数
|
||||||
</button>
|
</button>
|
||||||
@@ -352,10 +357,15 @@ export function TaskEditorModal({
|
|||||||
<div className="collection-list">
|
<div className="collection-list">
|
||||||
{scriptArgs.fields.map((field, index) => (
|
{scriptArgs.fields.map((field, index) => (
|
||||||
<div className="argument-row" key={field.id}>
|
<div className="argument-row" key={field.id}>
|
||||||
|
<input
|
||||||
|
{...register(`script.args.${index}.key`)}
|
||||||
|
className="field-input mono"
|
||||||
|
placeholder="参数名 (如 --mode 或 -v)"
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
{...register(`script.args.${index}.value`)}
|
{...register(`script.args.${index}.value`)}
|
||||||
className="field-input mono"
|
className="field-input mono"
|
||||||
placeholder="例如:--mode"
|
placeholder="参数值 (可选,无值可留空)"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -385,12 +395,38 @@ export function TaskEditorModal({
|
|||||||
onAdd={() => scriptEnv.append({ key: '', value: '' })}
|
onAdd={() => scriptEnv.append({ key: '', value: '' })}
|
||||||
onRemove={scriptEnv.remove}
|
onRemove={scriptEnv.remove}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
|
||||||
|
<div className="cli-preview">
|
||||||
|
<div className="cli-preview-header">
|
||||||
|
<span>调用预览 (Runtime Preview)</span>
|
||||||
|
<span>Windows 原生环境</span>
|
||||||
|
</div>
|
||||||
|
<div className="cli-preview-body">
|
||||||
|
{scriptPreview.env
|
||||||
|
.filter((item) => item.key.trim())
|
||||||
|
.map((item, index) => (
|
||||||
|
<div
|
||||||
|
className="cli-line-env"
|
||||||
|
key={`${item.key}-${index}`}
|
||||||
|
>
|
||||||
|
set {item.key.trim()}=
|
||||||
|
{isSensitiveName(item.key)
|
||||||
|
? '[REDACTED]'
|
||||||
|
: item.value || '""'}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="cli-line-cmd">
|
||||||
|
{scriptPreview.interpreter.trim() || 'python.exe'}{' '}
|
||||||
|
{scriptPreview.script.trim() || 'main.py'}
|
||||||
|
{formatArgumentPreview(scriptPreview.args)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{actionType === 'http' ? (
|
{actionType === 'http' ? (
|
||||||
<fieldset className="action-panel">
|
<div className="dynamic-fields">
|
||||||
<legend>HTTP 配置</legend>
|
|
||||||
<div className="form-grid">
|
<div className="form-grid">
|
||||||
<Field label="请求 URL" error={errors.http?.url?.message}>
|
<Field label="请求 URL" error={errors.http?.url?.message}>
|
||||||
<input
|
<input
|
||||||
@@ -436,12 +472,11 @@ export function TaskEditorModal({
|
|||||||
onAdd={() => httpHeaders.append({ key: '', value: '' })}
|
onAdd={() => httpHeaders.append({ key: '', value: '' })}
|
||||||
onRemove={httpHeaders.remove}
|
onRemove={httpHeaders.remove}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{actionType === 'process' ? (
|
{actionType === 'process' ? (
|
||||||
<fieldset className="action-panel">
|
<div className="dynamic-fields">
|
||||||
<legend>进程配置</legend>
|
|
||||||
<div className="form-grid">
|
<div className="form-grid">
|
||||||
<Field
|
<Field
|
||||||
label="可执行程序"
|
label="可执行程序"
|
||||||
@@ -469,7 +504,9 @@ export function TaskEditorModal({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="button button-secondary button-small"
|
className="button button-secondary button-small"
|
||||||
onClick={() => processArgs.append({ value: '' })}
|
onClick={() =>
|
||||||
|
processArgs.append({ key: '', value: '' })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
+ 添加参数
|
+ 添加参数
|
||||||
</button>
|
</button>
|
||||||
@@ -477,10 +514,15 @@ export function TaskEditorModal({
|
|||||||
<div className="collection-list">
|
<div className="collection-list">
|
||||||
{processArgs.fields.map((field, index) => (
|
{processArgs.fields.map((field, index) => (
|
||||||
<div className="argument-row" key={field.id}>
|
<div className="argument-row" key={field.id}>
|
||||||
|
<input
|
||||||
|
{...register(`process.args.${index}.key`)}
|
||||||
|
className="field-input mono"
|
||||||
|
placeholder="参数名 (如 --port)"
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
{...register(`process.args.${index}.value`)}
|
{...register(`process.args.${index}.value`)}
|
||||||
className="field-input mono"
|
className="field-input mono"
|
||||||
placeholder="例如:--port"
|
placeholder="参数值 (可选)"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -509,32 +551,68 @@ export function TaskEditorModal({
|
|||||||
onAdd={() => processEnv.append({ key: '', value: '' })}
|
onAdd={() => processEnv.append({ key: '', value: '' })}
|
||||||
onRemove={processEnv.remove}
|
onRemove={processEnv.remove}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<footer className="form-actions">
|
<label className="checkbox-field">
|
||||||
<button
|
<input type="checkbox" {...register('enabled')} />
|
||||||
type="button"
|
<span>
|
||||||
className="button button-secondary"
|
<strong>立即激活此任务调度</strong>
|
||||||
onClick={onClose}
|
<small>保存后立即注册到 Cron 调度器</small>
|
||||||
disabled={saving}
|
</span>
|
||||||
>
|
</label>
|
||||||
取消
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="button button-primary"
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
{saving ? '保存中...' : task ? '保存修改' : '保存并热刷新'}
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<footer className="modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button button-secondary"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
form="task-editor-form"
|
||||||
|
className="button button-primary"
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
{saving ? '保存中...' : task ? '保存修改' : '保存并热刷新'}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatArgumentPreview(argumentsList: KeyValueFormValue[]): string {
|
||||||
|
const values = argumentsList.flatMap(({ key, value }) => {
|
||||||
|
const argument = key.trim()
|
||||||
|
const argumentValue = value.trim()
|
||||||
|
return [
|
||||||
|
argument,
|
||||||
|
argumentValue.includes(' ') ? `"${argumentValue}"` : argumentValue,
|
||||||
|
].filter(Boolean)
|
||||||
|
})
|
||||||
|
return values.length ? ` ${values.join(' ')}` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSensitiveName(name: string): boolean {
|
||||||
|
const normalized = name.toUpperCase()
|
||||||
|
return [
|
||||||
|
'SECRET',
|
||||||
|
'TOKEN',
|
||||||
|
'PASSWORD',
|
||||||
|
'PASS',
|
||||||
|
'API_KEY',
|
||||||
|
'PRIVATE_KEY',
|
||||||
|
'CREDENTIAL',
|
||||||
|
'AUTHORIZATION',
|
||||||
|
'AUTH',
|
||||||
|
].some((marker) => normalized.includes(marker))
|
||||||
|
}
|
||||||
|
|
||||||
interface FieldProps {
|
interface FieldProps {
|
||||||
label: string
|
label: string
|
||||||
error?: string
|
error?: string
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ describe('task form payload', () => {
|
|||||||
values.name = 'Worker'
|
values.name = 'Worker'
|
||||||
values.actionType = 'process'
|
values.actionType = 'process'
|
||||||
values.process.executable = 'C:\\Tools\\worker.exe'
|
values.process.executable = 'C:\\Tools\\worker.exe'
|
||||||
values.process.args = [{ value: '--port' }, { value: '8080' }]
|
values.process.args = [{ key: '--port', value: '8080' }]
|
||||||
values.process.env = [{ key: 'WORKER_MODE', value: 'local' }]
|
values.process.env = [{ key: 'WORKER_MODE', value: 'local' }]
|
||||||
|
|
||||||
const payload = valuesToCreateRequest(values)
|
const payload = valuesToCreateRequest(values)
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ export interface KeyValueFormValue {
|
|||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArgumentFormValue {
|
|
||||||
value: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TaskFormValues {
|
export interface TaskFormValues {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -28,7 +24,7 @@ export interface TaskFormValues {
|
|||||||
working_dir: string
|
working_dir: string
|
||||||
interpreter: string
|
interpreter: string
|
||||||
script: string
|
script: string
|
||||||
args: ArgumentFormValue[]
|
args: KeyValueFormValue[]
|
||||||
env: KeyValueFormValue[]
|
env: KeyValueFormValue[]
|
||||||
}
|
}
|
||||||
http: {
|
http: {
|
||||||
@@ -41,7 +37,7 @@ export interface TaskFormValues {
|
|||||||
process: {
|
process: {
|
||||||
executable: string
|
executable: string
|
||||||
working_dir: string
|
working_dir: string
|
||||||
args: ArgumentFormValue[]
|
args: KeyValueFormValue[]
|
||||||
env: KeyValueFormValue[]
|
env: KeyValueFormValue[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,7 +88,7 @@ export const taskFormSchema = z
|
|||||||
working_dir: z.string(),
|
working_dir: z.string(),
|
||||||
interpreter: z.string(),
|
interpreter: z.string(),
|
||||||
script: z.string(),
|
script: z.string(),
|
||||||
args: z.array(z.object({ value: z.string() })),
|
args: z.array(keyValueSchema),
|
||||||
env: z.array(keyValueSchema),
|
env: z.array(keyValueSchema),
|
||||||
}),
|
}),
|
||||||
http: z.object({
|
http: z.object({
|
||||||
@@ -105,7 +101,7 @@ export const taskFormSchema = z
|
|||||||
process: z.object({
|
process: z.object({
|
||||||
executable: z.string(),
|
executable: z.string(),
|
||||||
working_dir: z.string(),
|
working_dir: z.string(),
|
||||||
args: z.array(z.object({ value: z.string() })),
|
args: z.array(keyValueSchema),
|
||||||
env: z.array(keyValueSchema),
|
env: z.array(keyValueSchema),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -208,7 +204,7 @@ export function taskToFormValues(task: Task): TaskFormValues {
|
|||||||
working_dir: task.action.working_dir,
|
working_dir: task.action.working_dir,
|
||||||
interpreter: task.action.interpreter,
|
interpreter: task.action.interpreter,
|
||||||
script: task.action.script,
|
script: task.action.script,
|
||||||
args: (task.action.args ?? []).map((value) => ({ value })),
|
args: argsToRows(task.action.args ?? []),
|
||||||
env: Object.entries(task.action.env ?? {}).map(([key, value]) => ({
|
env: Object.entries(task.action.env ?? {}).map(([key, value]) => ({
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
@@ -228,7 +224,7 @@ export function taskToFormValues(task: Task): TaskFormValues {
|
|||||||
values.process = {
|
values.process = {
|
||||||
executable: task.action.executable,
|
executable: task.action.executable,
|
||||||
working_dir: task.action.working_dir ?? '',
|
working_dir: task.action.working_dir ?? '',
|
||||||
args: (task.action.args ?? []).map((value) => ({ value })),
|
args: argsToRows(task.action.args ?? []),
|
||||||
env: Object.entries(task.action.env ?? {}).map(([key, value]) => ({
|
env: Object.entries(task.action.env ?? {}).map(([key, value]) => ({
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
@@ -246,7 +242,7 @@ export function valuesToAction(values: TaskFormValues): TaskAction {
|
|||||||
working_dir: values.script.working_dir.trim(),
|
working_dir: values.script.working_dir.trim(),
|
||||||
interpreter: values.script.interpreter.trim(),
|
interpreter: values.script.interpreter.trim(),
|
||||||
script: values.script.script.trim(),
|
script: values.script.script.trim(),
|
||||||
args: cleanList(values.script.args.map((argument) => argument.value)),
|
args: argumentsToArray(values.script.args),
|
||||||
env: keyValuesToObject(values.script.env),
|
env: keyValuesToObject(values.script.env),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,7 +267,7 @@ export function valuesToAction(values: TaskFormValues): TaskAction {
|
|||||||
type: 'process',
|
type: 'process',
|
||||||
executable: values.process.executable.trim(),
|
executable: values.process.executable.trim(),
|
||||||
working_dir: values.process.working_dir.trim() || null,
|
working_dir: values.process.working_dir.trim() || null,
|
||||||
args: cleanList(values.process.args.map((argument) => argument.value)),
|
args: argumentsToArray(values.process.args),
|
||||||
env: keyValuesToObject(values.process.env),
|
env: keyValuesToObject(values.process.env),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,8 +314,23 @@ export function mapServerFieldToFormField(
|
|||||||
return `${actionType}.${actionField}`
|
return `${actionType}.${actionField}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanList(values: string[]): string[] {
|
function argumentsToArray(values: KeyValueFormValue[]): string[] {
|
||||||
return values.map((value) => value.trim()).filter(Boolean)
|
return values.flatMap(({ key, value }) => {
|
||||||
|
const argument = key.trim()
|
||||||
|
const argumentValue = value.trim()
|
||||||
|
return [argument, argumentValue].filter(Boolean)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function argsToRows(values: string[]): KeyValueFormValue[] {
|
||||||
|
const rows: KeyValueFormValue[] = []
|
||||||
|
for (let index = 0; index < values.length; index += 2) {
|
||||||
|
rows.push({
|
||||||
|
key: values[index] ?? '',
|
||||||
|
value: values[index + 1] ?? '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
function keyValuesToObject(
|
function keyValuesToObject(
|
||||||
|
|||||||
Reference in New Issue
Block a user