feat(scenarios,environments): add markdown description field
- add optional description to scenario and environment entities - expose description in create/update DTOs, MCP tools, and export DTOs - render description as markdown on detail pages - add description editor (CodeEditor) to create/edit forms for both resources
This commit is contained in:
@@ -133,13 +133,13 @@ export const environments = {
|
||||
get(id: string): Promise<Environment> {
|
||||
return request(`/environments/${id}`);
|
||||
},
|
||||
create(name: string, data: Environment['data']): Promise<Environment> {
|
||||
create(name: string, data: Environment['data'], description?: string): Promise<Environment> {
|
||||
return request('/environments', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name, data }),
|
||||
body: JSON.stringify({ name, description, data }),
|
||||
});
|
||||
},
|
||||
update(id: string, patch: Partial<Pick<Environment, 'name' | 'data'>>): Promise<Environment> {
|
||||
update(id: string, patch: Partial<Pick<Environment, 'name' | 'description' | 'data'>>): Promise<Environment> {
|
||||
return request(`/environments/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(patch),
|
||||
@@ -182,13 +182,13 @@ export const scenarios = {
|
||||
get(id: string): Promise<Scenario & { steps: ScenarioStep[] }> {
|
||||
return request(`/scenarios/${id}`);
|
||||
},
|
||||
create(name: string): Promise<Scenario> {
|
||||
create(name: string, description?: string): Promise<Scenario> {
|
||||
return request('/scenarios', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name }),
|
||||
body: JSON.stringify({ name, description }),
|
||||
});
|
||||
},
|
||||
update(id: string, patch: Partial<Pick<Scenario, 'name'>>): Promise<Scenario> {
|
||||
update(id: string, patch: Partial<Pick<Scenario, 'name' | 'description'>>): Promise<Scenario> {
|
||||
return request(`/scenarios/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(patch),
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface EnvironmentData {
|
||||
export interface Environment {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
data: EnvironmentData;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -82,6 +83,7 @@ export interface ScenarioCredential {
|
||||
export interface Scenario {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
steps?: ScenarioStep[];
|
||||
scenarioCredentials?: ScenarioCredential[];
|
||||
createdAt: string;
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"updated": "Environment updated",
|
||||
"col_id": "ID",
|
||||
"col_name": "Name",
|
||||
"col_description": "Description",
|
||||
"col_data": "Data",
|
||||
"col_updated": "Updated",
|
||||
"empty": "No environments yet.",
|
||||
@@ -56,10 +57,13 @@
|
||||
"form_name": "Name",
|
||||
"form_name_placeholder": "e.g. staging, production",
|
||||
"form_name_required": "Name is required",
|
||||
"form_description": "Description",
|
||||
"form_description_placeholder": "Describe this environment",
|
||||
"form_data": "Data (JSON)",
|
||||
"form_data_invalid_json": "Must be a valid JSON object",
|
||||
"back": "Environments",
|
||||
"field_id": "ID",
|
||||
"field_description": "Description",
|
||||
"field_created": "Created",
|
||||
"field_updated": "Updated",
|
||||
"section_data": "Data"
|
||||
@@ -115,12 +119,14 @@
|
||||
"field_updated": "Updated",
|
||||
"field_lastUsed": "Last Used"
|
||||
},
|
||||
"scenarios": { "title": "Scenarios",
|
||||
"scenarios": {
|
||||
"title": "Scenarios",
|
||||
"created": "Scenario created",
|
||||
"updated": "Scenario updated",
|
||||
"run_started": "Scenario run started",
|
||||
"col_id": "ID",
|
||||
"col_name": "Name",
|
||||
"col_description": "Description",
|
||||
"col_updated": "Updated",
|
||||
"empty": "No scenarios yet.",
|
||||
"loading": "Loading…",
|
||||
@@ -128,6 +134,7 @@
|
||||
"action_delete": "Delete",
|
||||
"field_id": "ID",
|
||||
"field_name": "Name",
|
||||
"field_description": "Description",
|
||||
"field_steps": "Steps",
|
||||
"field_created": "Created",
|
||||
"field_updated": "Updated",
|
||||
@@ -148,6 +155,8 @@
|
||||
"action_cancel": "Cancel",
|
||||
"create_title": "New Scenario",
|
||||
"edit_title": "Edit Scenario",
|
||||
"form_description": "Description",
|
||||
"form_description_placeholder": "Describe this scenario",
|
||||
"form_name": "Name",
|
||||
"form_name_placeholder": "e.g. Login flow",
|
||||
"form_name_required": "Name is required",
|
||||
|
||||
@@ -12,6 +12,7 @@ export function CreateEnvironmentPage() {
|
||||
const toast = useToast();
|
||||
|
||||
const [name, setName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [dataJson, setDataJson] = useState('{\n "id": "https://id.example.com"\n}');
|
||||
const [nameError, setNameError] = useState('');
|
||||
const [dataError, setDataError] = useState('');
|
||||
@@ -42,7 +43,11 @@ export function CreateEnvironmentPage() {
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
const env = await environments.create(name.trim(), parsedData);
|
||||
const env = await environments.create(
|
||||
name.trim(),
|
||||
parsedData,
|
||||
description.trim() || undefined,
|
||||
);
|
||||
toast.success(t('environments.created'));
|
||||
navigate(`/environments/${env.id}`);
|
||||
} catch (err) {
|
||||
@@ -83,6 +88,15 @@ export function CreateEnvironmentPage() {
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>{t('environments.form_description')}</label>
|
||||
<CodeEditor
|
||||
language="markdown"
|
||||
value={description}
|
||||
onChange={setDescription}
|
||||
rows={6}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>{t('environments.form_data')}</label>
|
||||
<CodeEditor
|
||||
|
||||
@@ -15,6 +15,7 @@ export function EditEnvironmentPage() {
|
||||
|
||||
const [env, setEnv] = useState<Environment | null>(null);
|
||||
const [name, setName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [dataJson, setDataJson] = useState('{}');
|
||||
const [nameError, setNameError] = useState('');
|
||||
const [dataError, setDataError] = useState('');
|
||||
@@ -28,6 +29,7 @@ export function EditEnvironmentPage() {
|
||||
.then((data) => {
|
||||
setEnv(data);
|
||||
setName(data.name);
|
||||
setDescription(data.description || '');
|
||||
setDataJson(JSON.stringify(data.data ?? {}, null, 2));
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
@@ -60,6 +62,7 @@ export function EditEnvironmentPage() {
|
||||
try {
|
||||
await environments.update(id!, {
|
||||
name: name.trim(),
|
||||
description: description.trim() || undefined,
|
||||
data: parsedData,
|
||||
});
|
||||
toast.success(t('environments.updated'));
|
||||
@@ -109,6 +112,15 @@ export function EditEnvironmentPage() {
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>{t('environments.form_description')}</label>
|
||||
<CodeEditor
|
||||
language="markdown"
|
||||
value={description}
|
||||
onChange={setDescription}
|
||||
rows={6}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>{t('environments.form_data')}</label>
|
||||
<CodeEditor
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
DescriptionList,
|
||||
Timestamp,
|
||||
UuidBadge,
|
||||
MarkdownContent,
|
||||
useToast,
|
||||
} from '../../ui';
|
||||
import styles from '../Page.module.css';
|
||||
@@ -117,6 +118,17 @@ export function EnvironmentDetailPage() {
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{env.description && (
|
||||
<div className={styles.stepsSection}>
|
||||
<div className={styles.sectionHeadingRow}>
|
||||
<h2 className={styles.sectionHeading}>{t('environments.field_description')}</h2>
|
||||
</div>
|
||||
<Card>
|
||||
<MarkdownContent content={env.description} />
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.stepsSection}>
|
||||
<h2 className={styles.sectionHeading}>{t('environments.section_data')}</h2>
|
||||
<Card>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { X, Save, ClipboardList } from 'lucide-react';
|
||||
import { scenarios } from '../../api';
|
||||
import { Breadcrumbs, Button, Card, Input, useToast } from '../../ui';
|
||||
import { Breadcrumbs, Button, Card, Input, CodeEditor, useToast } from '../../ui';
|
||||
import styles from '../Page.module.css';
|
||||
|
||||
export function CreateScenarioPage() {
|
||||
@@ -12,6 +12,7 @@ export function CreateScenarioPage() {
|
||||
const toast = useToast();
|
||||
|
||||
const [name, setName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [nameError, setNameError] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
@@ -23,7 +24,7 @@ export function CreateScenarioPage() {
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
const scenario = await scenarios.create(name.trim());
|
||||
const scenario = await scenarios.create(name.trim(), description.trim() || undefined);
|
||||
toast.success(t('scenarios.created'));
|
||||
navigate(`/scenarios/${scenario.id}`);
|
||||
} catch (err) {
|
||||
@@ -64,6 +65,15 @@ export function CreateScenarioPage() {
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div className={styles.formActions}>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { X, Save, ClipboardList } from 'lucide-react';
|
||||
import { scenarios } from '../../api';
|
||||
import type { Scenario } from '../../api';
|
||||
import { Breadcrumbs, Button, Card, Input, useToast } from '../../ui';
|
||||
import { Breadcrumbs, Button, Card, Input, CodeEditor, useToast } from '../../ui';
|
||||
import styles from '../Page.module.css';
|
||||
|
||||
export function EditScenarioPage() {
|
||||
@@ -15,6 +15,7 @@ export function EditScenarioPage() {
|
||||
|
||||
const [scenario, setScenario] = useState<Scenario | null>(null);
|
||||
const [name, setName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [nameError, setNameError] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -26,6 +27,7 @@ export function EditScenarioPage() {
|
||||
.then((data) => {
|
||||
setScenario(data);
|
||||
setName(data.name);
|
||||
setDescription(data.description || '');
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, [id]);
|
||||
@@ -38,7 +40,10 @@ export function EditScenarioPage() {
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
await scenarios.update(id!, { name: name.trim() });
|
||||
await scenarios.update(id!, {
|
||||
name: name.trim(),
|
||||
description: description.trim() || undefined,
|
||||
});
|
||||
toast.success(t('scenarios.updated'));
|
||||
navigate(`/scenarios/${id}`);
|
||||
} catch (err) {
|
||||
@@ -86,6 +91,15 @@ export function EditScenarioPage() {
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div className={styles.formActions}>
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
Table,
|
||||
Timestamp,
|
||||
UuidBadge,
|
||||
MarkdownContent,
|
||||
useToast,
|
||||
type TableColumn,
|
||||
} from '../../ui';
|
||||
@@ -400,6 +401,17 @@ export function ScenarioDetailPage() {
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{scenario.description && (
|
||||
<div className={styles.stepsSection}>
|
||||
<div className={styles.sectionHeadingRow}>
|
||||
<h2 className={styles.sectionHeading}>{t('scenarios.field_description')}</h2>
|
||||
</div>
|
||||
<Card>
|
||||
<MarkdownContent content={scenario.description} />
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{scenario.steps.length > 0 && (
|
||||
<div className={styles.stepsSection}>
|
||||
<div className={styles.sectionToolbar}>
|
||||
|
||||
Reference in New Issue
Block a user