refactor(environment): rename urls payload to data

- replace hardcoded URL keys with a generic key-value data map

- align backend DTOs, MCP schemas, and service import/export mapping

- update environment UI forms to edit JSON data instead of fixed fields
This commit is contained in:
2026-04-10 21:22:46 +03:00
parent b45f95d8cf
commit fd515ee459
15 changed files with 141 additions and 143 deletions
+3 -3
View File
@@ -118,13 +118,13 @@ export const environments = {
get(id: string): Promise<Environment> {
return request(`/environments/${id}`);
},
create(name: string, urls: Environment['urls']): Promise<Environment> {
create(name: string, data: Environment['data']): Promise<Environment> {
return request('/environments', {
method: 'POST',
body: JSON.stringify({ name, urls }),
body: JSON.stringify({ name, data }),
});
},
update(id: string, patch: Partial<Pick<Environment, 'name' | 'urls'>>): Promise<Environment> {
update(id: string, patch: Partial<Pick<Environment, 'name' | 'data'>>): Promise<Environment> {
return request(`/environments/${id}`, {
method: 'PATCH',
body: JSON.stringify(patch),
+2 -6
View File
@@ -9,17 +9,14 @@ export interface PaginatedResponse<T> {
// ── Environments ──────────────────────────────────────────────────────────────
export interface EnvironmentUrls {
id_url?: string;
cabinet_url?: string;
admin_url?: string;
export interface EnvironmentData {
[key: string]: string | undefined;
}
export interface Environment {
id: string;
name: string;
urls: EnvironmentUrls;
data: EnvironmentData;
createdAt: string;
updatedAt: string;
}
@@ -67,7 +64,6 @@ export interface ScenarioStep {
scenarioId: string;
order: number;
title: string | null;
sessionName: string | null;
execCode: string | null;
validateCode: string | null;
createdAt: string;
+5 -6
View File
@@ -21,11 +21,11 @@
"title": "Environments",
"col_id": "ID",
"col_name": "Name",
"col_urls": "URLs",
"col_data": "Data",
"col_updated": "Updated",
"empty": "No environments yet.",
"loading": "Loading…",
"no_urls": "No URLs configured.",
"no_data": "No data configured.",
"menu_label": "Environment options",
"action_view": "View details",
"action_delete": "Delete",
@@ -41,14 +41,13 @@
"form_name": "Name",
"form_name_placeholder": "e.g. staging, production",
"form_name_required": "Name is required",
"form_id_url": "ID URL",
"form_cabinet_url": "Cabinet URL",
"form_admin_url": "Admin URL",
"form_data": "Data (JSON)",
"form_data_invalid_json": "Must be a valid JSON object",
"back": "Environments",
"field_id": "ID",
"field_created": "Created",
"field_updated": "Updated",
"section_urls": "URLs"
"section_data": "Data"
},
"credentials": {
"title": "Credentials",
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { X, Globe, Save } from 'lucide-react';
import { environments } from '../../api';
import { Breadcrumbs, Button, Card, Input } from '../../ui';
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
import styles from '../Page.module.css';
export function CreateEnvironmentPage() {
@@ -11,10 +11,9 @@ export function CreateEnvironmentPage() {
const navigate = useNavigate();
const [name, setName] = useState('');
const [idUrl, setIdUrl] = useState('');
const [cabinetUrl, setCabinetUrl] = useState('');
const [adminUrl, setAdminUrl] = useState('');
const [dataJson, setDataJson] = useState('{\n "id": "https://id.example.com"\n}');
const [nameError, setNameError] = useState('');
const [dataError, setDataError] = useState('');
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -24,14 +23,27 @@ export function CreateEnvironmentPage() {
setNameError(t('environments.form_name_required'));
return;
}
let parsedData: Record<string, string | undefined>;
try {
const raw = dataJson.trim() ? JSON.parse(dataJson.trim()) : {};
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
throw new Error('invalid');
}
parsedData = Object.fromEntries(
Object.entries(raw as Record<string, unknown>).map(([k, v]) => [
k,
v == null ? undefined : String(v),
]),
);
setDataError('');
} catch {
setDataError(t('environments.form_data_invalid_json'));
return;
}
setSaving(true);
setError(null);
try {
const env = await environments.create(name.trim(), {
id_url: idUrl.trim() || undefined,
cabinet_url: cabinetUrl.trim() || undefined,
admin_url: adminUrl.trim() || undefined,
});
const env = await environments.create(name.trim(), parsedData);
navigate(`/environments/${env.id}`);
} catch (err) {
setError((err as Error).message);
@@ -68,27 +80,20 @@ export function CreateEnvironmentPage() {
required
autoFocus
/>
<Input
label={t('environments.form_id_url')}
type="url"
placeholder="https://…"
value={idUrl}
onChange={(e) => setIdUrl(e.target.value)}
/>
<Input
label={t('environments.form_cabinet_url')}
type="url"
placeholder="https://…"
value={cabinetUrl}
onChange={(e) => setCabinetUrl(e.target.value)}
/>
<Input
label={t('environments.form_admin_url')}
type="url"
placeholder="https://…"
value={adminUrl}
onChange={(e) => setAdminUrl(e.target.value)}
/>
<div className={styles.formField}>
<label className={styles.fieldLabel}>{t('environments.form_data')}</label>
<CodeEditor
language="json"
value={dataJson}
onChange={(v) => {
setDataJson(v);
setDataError('');
}}
rows={8}
error={!!dataError}
/>
{dataError && <span className={styles.fieldError}>{dataError}</span>}
</div>
</div>
<div className={styles.formActions}>
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { X, Globe, Save } from 'lucide-react';
import { environments } from '../../api';
import type { Environment } from '../../api';
import { Breadcrumbs, Button, Card, Input } from '../../ui';
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
import styles from '../Page.module.css';
export function EditEnvironmentPage() {
@@ -14,10 +14,9 @@ export function EditEnvironmentPage() {
const [env, setEnv] = useState<Environment | null>(null);
const [name, setName] = useState('');
const [idUrl, setIdUrl] = useState('');
const [cabinetUrl, setCabinetUrl] = useState('');
const [adminUrl, setAdminUrl] = useState('');
const [dataJson, setDataJson] = useState('{}');
const [nameError, setNameError] = useState('');
const [dataError, setDataError] = useState('');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -29,9 +28,7 @@ export function EditEnvironmentPage() {
.then((data) => {
setEnv(data);
setName(data.name);
setIdUrl(data.urls.id_url ?? '');
setCabinetUrl(data.urls.cabinet_url ?? '');
setAdminUrl(data.urls.admin_url ?? '');
setDataJson(JSON.stringify(data.data ?? {}, null, 2));
})
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
@@ -43,16 +40,29 @@ export function EditEnvironmentPage() {
setNameError(t('environments.form_name_required'));
return;
}
let parsedData: Record<string, string | undefined>;
try {
const raw = dataJson.trim() ? JSON.parse(dataJson.trim()) : {};
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
throw new Error('invalid');
}
parsedData = Object.fromEntries(
Object.entries(raw as Record<string, unknown>).map(([k, v]) => [
k,
v == null ? undefined : String(v),
]),
);
setDataError('');
} catch {
setDataError(t('environments.form_data_invalid_json'));
return;
}
setSaving(true);
setError(null);
try {
await environments.update(id!, {
name: name.trim(),
urls: {
id_url: idUrl.trim() || undefined,
cabinet_url: cabinetUrl.trim() || undefined,
admin_url: adminUrl.trim() || undefined,
},
data: parsedData,
});
navigate(`/environments/${id}`);
} catch (err) {
@@ -96,27 +106,20 @@ export function EditEnvironmentPage() {
required
autoFocus
/>
<Input
label={t('environments.form_id_url')}
type="url"
placeholder="https://…"
value={idUrl}
onChange={(e) => setIdUrl(e.target.value)}
/>
<Input
label={t('environments.form_cabinet_url')}
type="url"
placeholder="https://…"
value={cabinetUrl}
onChange={(e) => setCabinetUrl(e.target.value)}
/>
<Input
label={t('environments.form_admin_url')}
type="url"
placeholder="https://…"
value={adminUrl}
onChange={(e) => setAdminUrl(e.target.value)}
/>
<div className={styles.formField}>
<label className={styles.fieldLabel}>{t('environments.form_data')}</label>
<CodeEditor
language="json"
value={dataJson}
onChange={(v) => {
setDataJson(v);
setDataError('');
}}
rows={8}
error={!!dataError}
/>
{dataError && <span className={styles.fieldError}>{dataError}</span>}
</div>
</div>
<div className={styles.formActions}>
@@ -105,14 +105,14 @@ export function EnvironmentDetailPage() {
</Card>
<div className={styles.stepsSection}>
<h2 className={styles.sectionHeading}>{t('environments.section_urls')}</h2>
<h2 className={styles.sectionHeading}>{t('environments.section_data')}</h2>
<Card>
{Object.entries(env.urls).filter(([, v]) => v).length === 0 ? (
<p className={styles.muted}>{t('environments.no_urls')}</p>
{Object.entries(env.data).filter(([, v]) => v).length === 0 ? (
<p className={styles.muted}>{t('environments.no_data')}</p>
) : (
<DescriptionList
layout="grid"
items={Object.entries(env.urls)
items={Object.entries(env.data)
.filter(([, v]) => v)
.map(([key, value]) => ({
term: key,
@@ -20,7 +20,7 @@ import styles from '../Page.module.css';
function EnvironmentCard({ env, onDelete }: { env: Environment; onDelete: (id: string) => void }) {
const { t } = useTranslation();
const navigate = useNavigate();
const urlEntries = Object.entries(env.urls).filter(([, v]) => v);
const dataEntries = Object.entries(env.data).filter(([, v]) => v);
const handleExport = async () => {
const data = await environments.exportEnvironment(env.id);
@@ -87,15 +87,15 @@ function EnvironmentCard({ env, onDelete }: { env: Environment; onDelete: (id: s
footer={footer}
onClick={() => navigate(`/environments/${env.id}`)}
>
{urlEntries.length > 0 && (
{dataEntries.length > 0 && (
<DescriptionList
layout="compact"
truncate
items={urlEntries.map(([key, value]) => ({ term: key, detail: value }))}
items={dataEntries.map(([key, value]) => ({ term: key, detail: value }))}
/>
)}
{urlEntries.length === 0 && (
<p className={styles.envCardEmpty}>{t('environments.no_urls')}</p>
{dataEntries.length === 0 && (
<p className={styles.envCardEmpty}>{t('environments.no_data')}</p>
)}
</Card>
);