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> {
|
get(id: string): Promise<Environment> {
|
||||||
return request(`/environments/${id}`);
|
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', {
|
return request('/environments', {
|
||||||
method: 'POST',
|
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}`, {
|
return request(`/environments/${id}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: JSON.stringify(patch),
|
body: JSON.stringify(patch),
|
||||||
@@ -182,13 +182,13 @@ export const scenarios = {
|
|||||||
get(id: string): Promise<Scenario & { steps: ScenarioStep[] }> {
|
get(id: string): Promise<Scenario & { steps: ScenarioStep[] }> {
|
||||||
return request(`/scenarios/${id}`);
|
return request(`/scenarios/${id}`);
|
||||||
},
|
},
|
||||||
create(name: string): Promise<Scenario> {
|
create(name: string, description?: string): Promise<Scenario> {
|
||||||
return request('/scenarios', {
|
return request('/scenarios', {
|
||||||
method: 'POST',
|
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}`, {
|
return request(`/scenarios/${id}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: JSON.stringify(patch),
|
body: JSON.stringify(patch),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export interface EnvironmentData {
|
|||||||
export interface Environment {
|
export interface Environment {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
description?: string;
|
||||||
data: EnvironmentData;
|
data: EnvironmentData;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -82,6 +83,7 @@ export interface ScenarioCredential {
|
|||||||
export interface Scenario {
|
export interface Scenario {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
description?: string;
|
||||||
steps?: ScenarioStep[];
|
steps?: ScenarioStep[];
|
||||||
scenarioCredentials?: ScenarioCredential[];
|
scenarioCredentials?: ScenarioCredential[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"updated": "Environment updated",
|
"updated": "Environment updated",
|
||||||
"col_id": "ID",
|
"col_id": "ID",
|
||||||
"col_name": "Name",
|
"col_name": "Name",
|
||||||
|
"col_description": "Description",
|
||||||
"col_data": "Data",
|
"col_data": "Data",
|
||||||
"col_updated": "Updated",
|
"col_updated": "Updated",
|
||||||
"empty": "No environments yet.",
|
"empty": "No environments yet.",
|
||||||
@@ -56,10 +57,13 @@
|
|||||||
"form_name": "Name",
|
"form_name": "Name",
|
||||||
"form_name_placeholder": "e.g. staging, production",
|
"form_name_placeholder": "e.g. staging, production",
|
||||||
"form_name_required": "Name is required",
|
"form_name_required": "Name is required",
|
||||||
|
"form_description": "Description",
|
||||||
|
"form_description_placeholder": "Describe this environment",
|
||||||
"form_data": "Data (JSON)",
|
"form_data": "Data (JSON)",
|
||||||
"form_data_invalid_json": "Must be a valid JSON object",
|
"form_data_invalid_json": "Must be a valid JSON object",
|
||||||
"back": "Environments",
|
"back": "Environments",
|
||||||
"field_id": "ID",
|
"field_id": "ID",
|
||||||
|
"field_description": "Description",
|
||||||
"field_created": "Created",
|
"field_created": "Created",
|
||||||
"field_updated": "Updated",
|
"field_updated": "Updated",
|
||||||
"section_data": "Data"
|
"section_data": "Data"
|
||||||
@@ -115,12 +119,14 @@
|
|||||||
"field_updated": "Updated",
|
"field_updated": "Updated",
|
||||||
"field_lastUsed": "Last Used"
|
"field_lastUsed": "Last Used"
|
||||||
},
|
},
|
||||||
"scenarios": { "title": "Scenarios",
|
"scenarios": {
|
||||||
|
"title": "Scenarios",
|
||||||
"created": "Scenario created",
|
"created": "Scenario created",
|
||||||
"updated": "Scenario updated",
|
"updated": "Scenario updated",
|
||||||
"run_started": "Scenario run started",
|
"run_started": "Scenario run started",
|
||||||
"col_id": "ID",
|
"col_id": "ID",
|
||||||
"col_name": "Name",
|
"col_name": "Name",
|
||||||
|
"col_description": "Description",
|
||||||
"col_updated": "Updated",
|
"col_updated": "Updated",
|
||||||
"empty": "No scenarios yet.",
|
"empty": "No scenarios yet.",
|
||||||
"loading": "Loading…",
|
"loading": "Loading…",
|
||||||
@@ -128,6 +134,7 @@
|
|||||||
"action_delete": "Delete",
|
"action_delete": "Delete",
|
||||||
"field_id": "ID",
|
"field_id": "ID",
|
||||||
"field_name": "Name",
|
"field_name": "Name",
|
||||||
|
"field_description": "Description",
|
||||||
"field_steps": "Steps",
|
"field_steps": "Steps",
|
||||||
"field_created": "Created",
|
"field_created": "Created",
|
||||||
"field_updated": "Updated",
|
"field_updated": "Updated",
|
||||||
@@ -148,6 +155,8 @@
|
|||||||
"action_cancel": "Cancel",
|
"action_cancel": "Cancel",
|
||||||
"create_title": "New Scenario",
|
"create_title": "New Scenario",
|
||||||
"edit_title": "Edit Scenario",
|
"edit_title": "Edit Scenario",
|
||||||
|
"form_description": "Description",
|
||||||
|
"form_description_placeholder": "Describe this scenario",
|
||||||
"form_name": "Name",
|
"form_name": "Name",
|
||||||
"form_name_placeholder": "e.g. Login flow",
|
"form_name_placeholder": "e.g. Login flow",
|
||||||
"form_name_required": "Name is required",
|
"form_name_required": "Name is required",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export function CreateEnvironmentPage() {
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
const [dataJson, setDataJson] = useState('{\n "id": "https://id.example.com"\n}');
|
const [dataJson, setDataJson] = useState('{\n "id": "https://id.example.com"\n}');
|
||||||
const [nameError, setNameError] = useState('');
|
const [nameError, setNameError] = useState('');
|
||||||
const [dataError, setDataError] = useState('');
|
const [dataError, setDataError] = useState('');
|
||||||
@@ -42,7 +43,11 @@ export function CreateEnvironmentPage() {
|
|||||||
}
|
}
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
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'));
|
toast.success(t('environments.created'));
|
||||||
navigate(`/environments/${env.id}`);
|
navigate(`/environments/${env.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -83,6 +88,15 @@ export function CreateEnvironmentPage() {
|
|||||||
required
|
required
|
||||||
autoFocus
|
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}>
|
<div className={styles.formField}>
|
||||||
<label className={styles.fieldLabel}>{t('environments.form_data')}</label>
|
<label className={styles.fieldLabel}>{t('environments.form_data')}</label>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export function EditEnvironmentPage() {
|
|||||||
|
|
||||||
const [env, setEnv] = useState<Environment | null>(null);
|
const [env, setEnv] = useState<Environment | null>(null);
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
const [dataJson, setDataJson] = useState('{}');
|
const [dataJson, setDataJson] = useState('{}');
|
||||||
const [nameError, setNameError] = useState('');
|
const [nameError, setNameError] = useState('');
|
||||||
const [dataError, setDataError] = useState('');
|
const [dataError, setDataError] = useState('');
|
||||||
@@ -28,6 +29,7 @@ export function EditEnvironmentPage() {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
setEnv(data);
|
setEnv(data);
|
||||||
setName(data.name);
|
setName(data.name);
|
||||||
|
setDescription(data.description || '');
|
||||||
setDataJson(JSON.stringify(data.data ?? {}, null, 2));
|
setDataJson(JSON.stringify(data.data ?? {}, null, 2));
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
@@ -60,6 +62,7 @@ export function EditEnvironmentPage() {
|
|||||||
try {
|
try {
|
||||||
await environments.update(id!, {
|
await environments.update(id!, {
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
|
description: description.trim() || undefined,
|
||||||
data: parsedData,
|
data: parsedData,
|
||||||
});
|
});
|
||||||
toast.success(t('environments.updated'));
|
toast.success(t('environments.updated'));
|
||||||
@@ -109,6 +112,15 @@ export function EditEnvironmentPage() {
|
|||||||
required
|
required
|
||||||
autoFocus
|
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}>
|
<div className={styles.formField}>
|
||||||
<label className={styles.fieldLabel}>{t('environments.form_data')}</label>
|
<label className={styles.fieldLabel}>{t('environments.form_data')}</label>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
DescriptionList,
|
DescriptionList,
|
||||||
Timestamp,
|
Timestamp,
|
||||||
UuidBadge,
|
UuidBadge,
|
||||||
|
MarkdownContent,
|
||||||
useToast,
|
useToast,
|
||||||
} from '../../ui';
|
} from '../../ui';
|
||||||
import styles from '../Page.module.css';
|
import styles from '../Page.module.css';
|
||||||
@@ -117,6 +118,17 @@ export function EnvironmentDetailPage() {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</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}>
|
<div className={styles.stepsSection}>
|
||||||
<h2 className={styles.sectionHeading}>{t('environments.section_data')}</h2>
|
<h2 className={styles.sectionHeading}>{t('environments.section_data')}</h2>
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { X, Save, ClipboardList } from 'lucide-react';
|
import { X, Save, ClipboardList } from 'lucide-react';
|
||||||
import { scenarios } from '../../api';
|
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';
|
import styles from '../Page.module.css';
|
||||||
|
|
||||||
export function CreateScenarioPage() {
|
export function CreateScenarioPage() {
|
||||||
@@ -12,6 +12,7 @@ export function CreateScenarioPage() {
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
const [nameError, setNameError] = useState('');
|
const [nameError, setNameError] = useState('');
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ export function CreateScenarioPage() {
|
|||||||
}
|
}
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const scenario = await scenarios.create(name.trim());
|
const scenario = await scenarios.create(name.trim(), description.trim() || undefined);
|
||||||
toast.success(t('scenarios.created'));
|
toast.success(t('scenarios.created'));
|
||||||
navigate(`/scenarios/${scenario.id}`);
|
navigate(`/scenarios/${scenario.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -64,6 +65,15 @@ export function CreateScenarioPage() {
|
|||||||
required
|
required
|
||||||
autoFocus
|
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>
|
||||||
|
|
||||||
<div className={styles.formActions}>
|
<div className={styles.formActions}>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { X, Save, ClipboardList } from 'lucide-react';
|
import { X, Save, ClipboardList } from 'lucide-react';
|
||||||
import { scenarios } from '../../api';
|
import { scenarios } from '../../api';
|
||||||
import type { Scenario } 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';
|
import styles from '../Page.module.css';
|
||||||
|
|
||||||
export function EditScenarioPage() {
|
export function EditScenarioPage() {
|
||||||
@@ -15,6 +15,7 @@ export function EditScenarioPage() {
|
|||||||
|
|
||||||
const [scenario, setScenario] = useState<Scenario | null>(null);
|
const [scenario, setScenario] = useState<Scenario | null>(null);
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
const [nameError, setNameError] = useState('');
|
const [nameError, setNameError] = useState('');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -26,6 +27,7 @@ export function EditScenarioPage() {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
setScenario(data);
|
setScenario(data);
|
||||||
setName(data.name);
|
setName(data.name);
|
||||||
|
setDescription(data.description || '');
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [id]);
|
}, [id]);
|
||||||
@@ -38,7 +40,10 @@ export function EditScenarioPage() {
|
|||||||
}
|
}
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
await scenarios.update(id!, { name: name.trim() });
|
await scenarios.update(id!, {
|
||||||
|
name: name.trim(),
|
||||||
|
description: description.trim() || undefined,
|
||||||
|
});
|
||||||
toast.success(t('scenarios.updated'));
|
toast.success(t('scenarios.updated'));
|
||||||
navigate(`/scenarios/${id}`);
|
navigate(`/scenarios/${id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -86,6 +91,15 @@ export function EditScenarioPage() {
|
|||||||
required
|
required
|
||||||
autoFocus
|
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>
|
||||||
|
|
||||||
<div className={styles.formActions}>
|
<div className={styles.formActions}>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
Timestamp,
|
Timestamp,
|
||||||
UuidBadge,
|
UuidBadge,
|
||||||
|
MarkdownContent,
|
||||||
useToast,
|
useToast,
|
||||||
type TableColumn,
|
type TableColumn,
|
||||||
} from '../../ui';
|
} from '../../ui';
|
||||||
@@ -400,6 +401,17 @@ export function ScenarioDetailPage() {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</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 && (
|
{scenario.steps.length > 0 && (
|
||||||
<div className={styles.stepsSection}>
|
<div className={styles.stepsSection}>
|
||||||
<div className={styles.sectionToolbar}>
|
<div className={styles.sectionToolbar}>
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@ RUN npm ci --omit=dev -w server
|
|||||||
COPY --from=builder /app/server/dist ./dist
|
COPY --from=builder /app/server/dist ./dist
|
||||||
|
|
||||||
# Runtime directories (keys and SQLite DB mounted via volumes)
|
# Runtime directories (keys and SQLite DB mounted via volumes)
|
||||||
RUN mkdir -p data keys
|
RUN mkdir -p data
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
import { IsNotEmpty, IsObject, IsString } from "class-validator";
|
import { IsNotEmpty, IsObject, IsOptional, IsString } from "class-validator";
|
||||||
import { EnvironmentData } from "../environment.entity";
|
import { EnvironmentData } from "../environment.entity";
|
||||||
|
|
||||||
export class CreateEnvironmentDto {
|
export class CreateEnvironmentDto {
|
||||||
@@ -8,6 +8,11 @@ export class CreateEnvironmentDto {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Optional markdown description" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: "Generic key-value map for environment metadata",
|
description: "Generic key-value map for environment metadata",
|
||||||
example: {
|
example: {
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ export class EnvironmentExportDto {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsObject()
|
@IsObject()
|
||||||
data: EnvironmentData;
|
data: EnvironmentData;
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ export class EnvironmentEntity {
|
|||||||
@Column({ unique: true })
|
@Column({ unique: true })
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@Column("text", { nullable: true })
|
||||||
|
description: string | null;
|
||||||
|
|
||||||
@Column("simple-json")
|
@Column("simple-json")
|
||||||
data: EnvironmentData;
|
data: EnvironmentData;
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export class EnvironmentService {
|
|||||||
kind: "environment",
|
kind: "environment",
|
||||||
id: env.id,
|
id: env.id,
|
||||||
name: env.name,
|
name: env.name,
|
||||||
|
description: env.description ?? undefined,
|
||||||
data: env.data,
|
data: env.data,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -81,18 +82,36 @@ export class EnvironmentService {
|
|||||||
if (dto.id) {
|
if (dto.id) {
|
||||||
const existing = await this.repo.findOneBy({ id: dto.id });
|
const existing = await this.repo.findOneBy({ id: dto.id });
|
||||||
if (existing) {
|
if (existing) {
|
||||||
Object.assign(existing, { name: dto.name, data: dto.data });
|
Object.assign(existing, {
|
||||||
|
name: dto.name,
|
||||||
|
description: dto.description ?? null,
|
||||||
|
data: dto.data,
|
||||||
|
});
|
||||||
return this.repo.save(existing);
|
return this.repo.save(existing);
|
||||||
}
|
}
|
||||||
return this.repo.save(
|
return this.repo.save(
|
||||||
this.repo.create({ id: dto.id, name: dto.name, data: dto.data }),
|
this.repo.create({
|
||||||
|
id: dto.id,
|
||||||
|
name: dto.name,
|
||||||
|
description: dto.description ?? null,
|
||||||
|
data: dto.data,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const byName = await this.repo.findOneBy({ name: dto.name });
|
const byName = await this.repo.findOneBy({ name: dto.name });
|
||||||
if (byName) {
|
if (byName) {
|
||||||
Object.assign(byName, { data: dto.data });
|
Object.assign(byName, {
|
||||||
|
description: dto.description ?? null,
|
||||||
|
data: dto.data,
|
||||||
|
});
|
||||||
return this.repo.save(byName);
|
return this.repo.save(byName);
|
||||||
}
|
}
|
||||||
return this.repo.save(this.repo.create({ name: dto.name, data: dto.data }));
|
return this.repo.save(
|
||||||
|
this.repo.create({
|
||||||
|
name: dto.name,
|
||||||
|
description: dto.description ?? null,
|
||||||
|
data: dto.data,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,15 +181,20 @@ export class McpService {
|
|||||||
name: z
|
name: z
|
||||||
.string()
|
.string()
|
||||||
.describe("Unique environment name, e.g. liquio-diia-stg"),
|
.describe("Unique environment name, e.g. liquio-diia-stg"),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Optional markdown description"),
|
||||||
data: z
|
data: z
|
||||||
.record(z.string(), z.string())
|
.record(z.string(), z.string())
|
||||||
.describe("Map of string keys to string values"),
|
.describe("Map of string keys to string values"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ name, data }) => {
|
async ({ name, description, data }) => {
|
||||||
try {
|
try {
|
||||||
const env = await this.environmentService.create({
|
const env = await this.environmentService.create({
|
||||||
name,
|
name,
|
||||||
|
description,
|
||||||
data: data as EnvironmentData,
|
data: data as EnvironmentData,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@@ -207,20 +212,26 @@ export class McpService {
|
|||||||
server.registerTool(
|
server.registerTool(
|
||||||
"update_environment",
|
"update_environment",
|
||||||
{
|
{
|
||||||
description: "Update an existing environment (name and/or data)",
|
description:
|
||||||
|
"Update an existing environment (name, description, and/or data)",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
id: z.uuid().describe("Environment ID to update"),
|
id: z.uuid().describe("Environment ID to update"),
|
||||||
name: z.string().optional().describe("New name"),
|
name: z.string().optional().describe("New name"),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("New markdown description"),
|
||||||
data: z
|
data: z
|
||||||
.record(z.string(), z.string())
|
.record(z.string(), z.string())
|
||||||
.optional()
|
.optional()
|
||||||
.describe("New data map"),
|
.describe("New data map"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ id, name, data }) => {
|
async ({ id, name, description, data }) => {
|
||||||
try {
|
try {
|
||||||
const env = await this.environmentService.update(id, {
|
const env = await this.environmentService.update(id, {
|
||||||
name,
|
name,
|
||||||
|
description,
|
||||||
data: data as EnvironmentData | undefined,
|
data: data as EnvironmentData | undefined,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@@ -457,11 +468,18 @@ export class McpService {
|
|||||||
description: "Create a new scenario",
|
description: "Create a new scenario",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
name: z.string().describe("Scenario name"),
|
name: z.string().describe("Scenario name"),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Optional markdown description"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ name }) => {
|
async ({ name, description }) => {
|
||||||
try {
|
try {
|
||||||
const scenario = await this.scenarioService.create({ name });
|
const scenario = await this.scenarioService.create({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{ type: "text" as const, text: JSON.stringify(scenario) },
|
{ type: "text" as const, text: JSON.stringify(scenario) },
|
||||||
@@ -479,15 +497,22 @@ export class McpService {
|
|||||||
server.registerTool(
|
server.registerTool(
|
||||||
"update_scenario",
|
"update_scenario",
|
||||||
{
|
{
|
||||||
description: "Update a scenario name",
|
description: "Update a scenario name and/or description",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
id: z.uuid().describe("Scenario ID"),
|
id: z.uuid().describe("Scenario ID"),
|
||||||
name: z.string().optional().describe("New name"),
|
name: z.string().optional().describe("New name"),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("New markdown description"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ id, name }) => {
|
async ({ id, name, description }) => {
|
||||||
try {
|
try {
|
||||||
const scenario = await this.scenarioService.update(id, { name });
|
const scenario = await this.scenarioService.update(id, {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{ type: "text" as const, text: JSON.stringify(scenario) },
|
{ type: "text" as const, text: JSON.stringify(scenario) },
|
||||||
@@ -951,6 +976,10 @@ export class McpService {
|
|||||||
"Import a scenario from an export payload, creating a new scenario with all its steps",
|
"Import a scenario from an export payload, creating a new scenario with all its steps",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
name: z.string().describe("Scenario name"),
|
name: z.string().describe("Scenario name"),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Optional markdown description"),
|
||||||
steps: z
|
steps: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -967,10 +996,11 @@ export class McpService {
|
|||||||
.describe("Ordered list of steps"),
|
.describe("Ordered list of steps"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ name, steps }) => {
|
async ({ name, description, steps }) => {
|
||||||
try {
|
try {
|
||||||
const scenario = await this.scenarioService.importScenario({
|
const scenario = await this.scenarioService.importScenario({
|
||||||
name,
|
name,
|
||||||
|
description,
|
||||||
steps: steps as Parameters<
|
steps: steps as Parameters<
|
||||||
typeof this.scenarioService.importScenario
|
typeof this.scenarioService.importScenario
|
||||||
>[0]["steps"],
|
>[0]["steps"],
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
import { IsNotEmpty, IsString } from "class-validator";
|
import { IsNotEmpty, IsOptional, IsString } from "class-validator";
|
||||||
|
|
||||||
export class CreateScenarioDto {
|
export class CreateScenarioDto {
|
||||||
@ApiProperty({ example: "Login and verify cabinet" })
|
@ApiProperty({ example: "Login and verify cabinet" })
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Optional markdown description" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ export class ScenarioExportDto {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
@ApiProperty({ type: [ScenarioStepExportDto] })
|
@ApiProperty({ type: [ScenarioStepExportDto] })
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
|
|||||||
@@ -7,4 +7,9 @@ export class UpdateScenarioDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Optional markdown description" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ export class ScenarioEntity {
|
|||||||
@Column()
|
@Column()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@Column("text", { nullable: true })
|
||||||
|
description: string | null;
|
||||||
|
|
||||||
@OneToMany(() => ScenarioStepEntity, (step) => step.scenario, {
|
@OneToMany(() => ScenarioStepEntity, (step) => step.scenario, {
|
||||||
cascade: true,
|
cascade: true,
|
||||||
eager: false,
|
eager: false,
|
||||||
|
|||||||
@@ -410,6 +410,7 @@ export class ScenarioService {
|
|||||||
kind: "scenario",
|
kind: "scenario",
|
||||||
id: scenario.id,
|
id: scenario.id,
|
||||||
name: scenario.name,
|
name: scenario.name,
|
||||||
|
description: scenario.description ?? undefined,
|
||||||
steps: scenario.steps.map((s) => ({
|
steps: scenario.steps.map((s) => ({
|
||||||
title: s.title,
|
title: s.title,
|
||||||
execCode: s.execCode,
|
execCode: s.execCode,
|
||||||
@@ -424,17 +425,25 @@ export class ScenarioService {
|
|||||||
const existing = await this.scenarioRepo.findOneBy({ id: dto.id });
|
const existing = await this.scenarioRepo.findOneBy({ id: dto.id });
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.name = dto.name;
|
existing.name = dto.name;
|
||||||
|
existing.description = dto.description ?? null;
|
||||||
scenario = await this.scenarioRepo.save(existing);
|
scenario = await this.scenarioRepo.save(existing);
|
||||||
// Delete old steps and recreate
|
// Delete old steps and recreate
|
||||||
await this.stepRepo.delete({ scenarioId: scenario.id });
|
await this.stepRepo.delete({ scenarioId: scenario.id });
|
||||||
} else {
|
} else {
|
||||||
scenario = await this.scenarioRepo.save(
|
scenario = await this.scenarioRepo.save(
|
||||||
this.scenarioRepo.create({ id: dto.id, name: dto.name }),
|
this.scenarioRepo.create({
|
||||||
|
id: dto.id,
|
||||||
|
name: dto.name,
|
||||||
|
description: dto.description ?? null,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
scenario = await this.scenarioRepo.save(
|
scenario = await this.scenarioRepo.save(
|
||||||
this.scenarioRepo.create({ name: dto.name }),
|
this.scenarioRepo.create({
|
||||||
|
name: dto.name,
|
||||||
|
description: dto.description ?? null,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (dto.steps.length > 0) {
|
if (dto.steps.length > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user