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:
+1
-1
@@ -35,7 +35,7 @@ RUN npm ci --omit=dev -w server
|
||||
COPY --from=builder /app/server/dist ./dist
|
||||
|
||||
# Runtime directories (keys and SQLite DB mounted via volumes)
|
||||
RUN mkdir -p data keys
|
||||
RUN mkdir -p data
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsObject, IsString } from "class-validator";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsObject, IsOptional, IsString } from "class-validator";
|
||||
import { EnvironmentData } from "../environment.entity";
|
||||
|
||||
export class CreateEnvironmentDto {
|
||||
@@ -8,6 +8,11 @@ export class CreateEnvironmentDto {
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "Optional markdown description" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: "Generic key-value map for environment metadata",
|
||||
example: {
|
||||
|
||||
@@ -25,6 +25,11 @@ export class EnvironmentExportDto {
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsObject()
|
||||
data: EnvironmentData;
|
||||
|
||||
@@ -18,6 +18,9 @@ export class EnvironmentEntity {
|
||||
@Column({ unique: true })
|
||||
name: string;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
description: string | null;
|
||||
|
||||
@Column("simple-json")
|
||||
data: EnvironmentData;
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ export class EnvironmentService {
|
||||
kind: "environment",
|
||||
id: env.id,
|
||||
name: env.name,
|
||||
description: env.description ?? undefined,
|
||||
data: env.data,
|
||||
};
|
||||
}
|
||||
@@ -81,18 +82,36 @@ export class EnvironmentService {
|
||||
if (dto.id) {
|
||||
const existing = await this.repo.findOneBy({ id: dto.id });
|
||||
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(
|
||||
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 });
|
||||
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(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
|
||||
.string()
|
||||
.describe("Unique environment name, e.g. liquio-diia-stg"),
|
||||
description: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional markdown description"),
|
||||
data: z
|
||||
.record(z.string(), z.string())
|
||||
.describe("Map of string keys to string values"),
|
||||
},
|
||||
},
|
||||
async ({ name, data }) => {
|
||||
async ({ name, description, data }) => {
|
||||
try {
|
||||
const env = await this.environmentService.create({
|
||||
name,
|
||||
description,
|
||||
data: data as EnvironmentData,
|
||||
});
|
||||
return {
|
||||
@@ -207,20 +212,26 @@ export class McpService {
|
||||
server.registerTool(
|
||||
"update_environment",
|
||||
{
|
||||
description: "Update an existing environment (name and/or data)",
|
||||
description:
|
||||
"Update an existing environment (name, description, and/or data)",
|
||||
inputSchema: {
|
||||
id: z.uuid().describe("Environment ID to update"),
|
||||
name: z.string().optional().describe("New name"),
|
||||
description: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("New markdown description"),
|
||||
data: z
|
||||
.record(z.string(), z.string())
|
||||
.optional()
|
||||
.describe("New data map"),
|
||||
},
|
||||
},
|
||||
async ({ id, name, data }) => {
|
||||
async ({ id, name, description, data }) => {
|
||||
try {
|
||||
const env = await this.environmentService.update(id, {
|
||||
name,
|
||||
description,
|
||||
data: data as EnvironmentData | undefined,
|
||||
});
|
||||
return {
|
||||
@@ -457,11 +468,18 @@ export class McpService {
|
||||
description: "Create a new scenario",
|
||||
inputSchema: {
|
||||
name: z.string().describe("Scenario name"),
|
||||
description: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional markdown description"),
|
||||
},
|
||||
},
|
||||
async ({ name }) => {
|
||||
async ({ name, description }) => {
|
||||
try {
|
||||
const scenario = await this.scenarioService.create({ name });
|
||||
const scenario = await this.scenarioService.create({
|
||||
name,
|
||||
description,
|
||||
});
|
||||
return {
|
||||
content: [
|
||||
{ type: "text" as const, text: JSON.stringify(scenario) },
|
||||
@@ -479,15 +497,22 @@ export class McpService {
|
||||
server.registerTool(
|
||||
"update_scenario",
|
||||
{
|
||||
description: "Update a scenario name",
|
||||
description: "Update a scenario name and/or description",
|
||||
inputSchema: {
|
||||
id: z.uuid().describe("Scenario ID"),
|
||||
name: z.string().optional().describe("New name"),
|
||||
description: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("New markdown description"),
|
||||
},
|
||||
},
|
||||
async ({ id, name }) => {
|
||||
async ({ id, name, description }) => {
|
||||
try {
|
||||
const scenario = await this.scenarioService.update(id, { name });
|
||||
const scenario = await this.scenarioService.update(id, {
|
||||
name,
|
||||
description,
|
||||
});
|
||||
return {
|
||||
content: [
|
||||
{ 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",
|
||||
inputSchema: {
|
||||
name: z.string().describe("Scenario name"),
|
||||
description: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional markdown description"),
|
||||
steps: z
|
||||
.array(
|
||||
z.object({
|
||||
@@ -967,10 +996,11 @@ export class McpService {
|
||||
.describe("Ordered list of steps"),
|
||||
},
|
||||
},
|
||||
async ({ name, steps }) => {
|
||||
async ({ name, description, steps }) => {
|
||||
try {
|
||||
const scenario = await this.scenarioService.importScenario({
|
||||
name,
|
||||
description,
|
||||
steps: steps as Parameters<
|
||||
typeof this.scenarioService.importScenario
|
||||
>[0]["steps"],
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsString } from "class-validator";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsOptional, IsString } from "class-validator";
|
||||
|
||||
export class CreateScenarioDto {
|
||||
@ApiProperty({ example: "Login and verify cabinet" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "Optional markdown description" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,11 @@ export class ScenarioExportDto {
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ type: [ScenarioStepExportDto] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
|
||||
@@ -7,4 +7,9 @@ export class UpdateScenarioDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "Optional markdown description" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ export class ScenarioEntity {
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
description: string | null;
|
||||
|
||||
@OneToMany(() => ScenarioStepEntity, (step) => step.scenario, {
|
||||
cascade: true,
|
||||
eager: false,
|
||||
|
||||
@@ -410,6 +410,7 @@ export class ScenarioService {
|
||||
kind: "scenario",
|
||||
id: scenario.id,
|
||||
name: scenario.name,
|
||||
description: scenario.description ?? undefined,
|
||||
steps: scenario.steps.map((s) => ({
|
||||
title: s.title,
|
||||
execCode: s.execCode,
|
||||
@@ -424,17 +425,25 @@ export class ScenarioService {
|
||||
const existing = await this.scenarioRepo.findOneBy({ id: dto.id });
|
||||
if (existing) {
|
||||
existing.name = dto.name;
|
||||
existing.description = dto.description ?? null;
|
||||
scenario = await this.scenarioRepo.save(existing);
|
||||
// Delete old steps and recreate
|
||||
await this.stepRepo.delete({ scenarioId: scenario.id });
|
||||
} else {
|
||||
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 {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user