- add scenario upload and download UI with typed file API helpers - show run artifacts on run detail pages after polling completes - handle multipart uploads and absolute file paths for downloads
119 lines
3.7 KiB
TypeScript
119 lines
3.7 KiB
TypeScript
import { useEffect, useState, SubmitEvent } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { usePageTitle } from '../../hooks/usePageTitle';
|
|
import { X, Save, ClipboardList } from 'lucide-react';
|
|
import { scenarios, environments } from '../../api';
|
|
import type { Environment } from '../../api';
|
|
import { Breadcrumbs, Button, Card, Input, CodeEditor, Select, useToast } from '../../ui';
|
|
import styles from '../Page.module.css';
|
|
|
|
export function CreateScenarioPage() {
|
|
const { t } = useTranslation();
|
|
usePageTitle(t('scenarios.create_title'));
|
|
const navigate = useNavigate();
|
|
const toast = useToast();
|
|
|
|
const [name, setName] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [environmentId, setEnvironmentId] = useState('');
|
|
const [envs, setEnvs] = useState<Environment[]>([]);
|
|
const [nameError, setNameError] = useState('');
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
useEffect(() => {
|
|
environments
|
|
.list(1, 200)
|
|
.then((r) => setEnvs(r?.data ?? []))
|
|
.catch(() => {});
|
|
}, []);
|
|
|
|
const handleSubmit = async (e: SubmitEvent<HTMLFormElement>) => {
|
|
e.preventDefault();
|
|
if (!name.trim()) {
|
|
setNameError(t('scenarios.form_name_required'));
|
|
return;
|
|
}
|
|
setSaving(true);
|
|
try {
|
|
const scenario = await scenarios.create(
|
|
name.trim(),
|
|
description.trim() || undefined,
|
|
environmentId || undefined,
|
|
);
|
|
toast.success(t('scenarios.created'));
|
|
navigate(`/scenarios/${scenario.id}`);
|
|
} catch (err) {
|
|
const message = (err as Error).message;
|
|
toast.error(message);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div className={styles.pageToolbar}>
|
|
<Breadcrumbs
|
|
items={[
|
|
{
|
|
label: t('scenarios.title'),
|
|
icon: <ClipboardList size={14} />,
|
|
onClick: () => navigate('/scenarios'),
|
|
},
|
|
{ label: t('scenarios.create_title') },
|
|
]}
|
|
/>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} noValidate>
|
|
<Card className={styles.formCard}>
|
|
<div className={styles.formFields}>
|
|
<Input
|
|
label={t('scenarios.form_name')}
|
|
placeholder={t('scenarios.form_name_placeholder')}
|
|
value={name}
|
|
onChange={(e) => {
|
|
setName(e.target.value);
|
|
setNameError('');
|
|
}}
|
|
error={nameError || undefined}
|
|
required
|
|
autoFocus
|
|
/>
|
|
<div className={styles.formField}>
|
|
<label className={styles.fieldLabel}>{t('scenarios.form_description')}</label>
|
|
<CodeEditor
|
|
language="markdown"
|
|
value={description}
|
|
onChange={setDescription}
|
|
rows={8}
|
|
/>
|
|
</div>
|
|
<Select
|
|
label={t('scenarios.form_environment')}
|
|
value={environmentId}
|
|
onChange={(e) => setEnvironmentId(e.target.value)}
|
|
options={[
|
|
{ value: '', label: t('scenarios.form_environment_none') },
|
|
...envs.map((env) => ({ value: env.id, label: env.name })),
|
|
]}
|
|
/>
|
|
</div>
|
|
|
|
<div className={styles.formActions}>
|
|
<Button type="button" variant="secondary" onClick={() => navigate('/scenarios')}>
|
|
<X size={14} />
|
|
{t('scenarios.action_cancel')}
|
|
</Button>
|
|
<Button type="submit" loading={saving}>
|
|
<Save size={14} />
|
|
{t('scenarios.action_save')}
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|