feat(scenarios): link scenarios to a default environment

- add optional ManyToOne relation from scenario to environment entity
- expose environmentId in create/update DTOs, service, and MCP tools
- pre-select linked environment in run modals on detail and list pages
- add environment selector to create/edit scenario forms
- show linked environment as a navigable link on scenario detail page
This commit is contained in:
2026-04-15 00:05:55 +03:00
parent e6aacc899d
commit a779ab4667
12 changed files with 109 additions and 19 deletions
@@ -1,4 +1,4 @@
import { useEffect, useState, SubmitEvent } from 'react';
import { useEffect, useRef, useState, SubmitEvent } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
@@ -68,6 +68,7 @@ export function ScenarioDetailPage() {
const [saveSessionFlag, setSaveSessionFlag] = useState(false);
const [envs, setEnvs] = useState<Environment[]>([]);
const [selectedEnvId, setSelectedEnvId] = useState('');
const envInitRef = useRef(false);
const [pendingDelete, setPendingDelete] = useState<
{ type: 'scenario' } | { type: 'step'; id: string } | { type: 'credential'; id: string } | null
>(null);
@@ -90,11 +91,19 @@ export function ScenarioDetailPage() {
.list(1, 200)
.then((r) => {
setEnvs(r.data);
setSelectedEnvId((prev) => prev || r.data[0]?.id || '');
})
.catch(() => {});
}, [id]);
// Default selected env to scenario's linked environment (or first env) once both are loaded
useEffect(() => {
if (envInitRef.current || envs.length === 0) return;
envInitRef.current = true;
const preferred = scenario?.environmentId ?? null;
const exists = preferred ? envs.some((e) => e.id === preferred) : false;
setSelectedEnvId(exists ? preferred! : envs[0].id);
}, [scenario, envs]);
const reloadScenario = async () => {
if (!id) return;
const s = await scenarios.get(id);
@@ -389,6 +398,19 @@ export function ScenarioDetailPage() {
items={[
{ term: t('scenarios.field_id'), detail: <UuidBadge id={scenario.id} /> },
{ term: t('scenarios.field_name'), detail: scenario.name },
...(scenario.environment
? [{
term: t('scenarios.field_environment'),
detail: (
<a
href={`/environments/${scenario.environment.id}`}
onClick={(e) => { e.preventDefault(); navigate(`/environments/${scenario.environment!.id}`); }}
>
{scenario.environment.name}
</a>
),
}]
: []),
{
term: t('scenarios.field_created'),
detail: <Timestamp value={scenario.createdAt} />,