From 9d1f5347c2bc290885f77034d8c215b416cbba1a Mon Sep 17 00:00:00 2001 From: "a.tolmachev" Date: Thu, 26 Mar 2026 01:18:53 +0300 Subject: [PATCH] feat: align operator UI with design concept --- TASKS.md | 12 +- .../operation-form/operation-form.tsx | 422 ++++-- apps/ui/src/pages/operation-create/page.tsx | 46 +- apps/ui/src/shared/ui/app-shell.tsx | 147 +- apps/ui/src/styles.css | 1244 +++++++++++------ 5 files changed, 1201 insertions(+), 670 deletions(-) diff --git a/TASKS.md b/TASKS.md index e699058..f56d12d 100644 --- a/TASKS.md +++ b/TASKS.md @@ -2,17 +2,17 @@ ## Current -### `feat/header-fields` +### `feat/ui-concept-alignment` Status: completed DoD: -- форма создания явно поддерживает настройку headers, а не прячет их только в сыром JSON -- `execution headers` доступны для всех протоколов -- `static headers` доступны для REST target -- detail view показывает сохраненные headers -- UI build и tests остаются зелеными +- shell и create flow визуально опираются на утвержденный concept +- sidebar, topbar и page header выглядят как единая продуктовая система +- create page больше не выглядит как длинная сырая форма, а как step-based contract builder +- кодовые JSON и mapping поля имеют отдельную визуальную подачу +- UI build и tests остаются зелеными после выравнивания под concept ## Next diff --git a/apps/ui/src/features/operation-form/operation-form.tsx b/apps/ui/src/features/operation-form/operation-form.tsx index d654928..3ef4902 100644 --- a/apps/ui/src/features/operation-form/operation-form.tsx +++ b/apps/ui/src/features/operation-form/operation-form.tsx @@ -22,103 +22,155 @@ type FieldConfig = { description?: string; wide?: boolean; rows?: number; + code?: boolean; }; +const protocolCards = [ + { + protocol: "rest", + title: "REST / HTTP", + description: + "One HTTP method and one path template, with body, query and header mapping.", + }, + { + protocol: "graphql", + title: "GraphQL", + description: + "One fixed query or mutation with stable variables and a typed response shape.", + }, + { + protocol: "grpc", + title: "gRPC (unary)", + description: + "One unary method backed by a descriptor-set contract and typed request payload.", + }, +] as const satisfies Array<{ + protocol: OperationFormValues["protocol"]; + title: string; + description: string; +}>; + const commonFields: FieldConfig[] = [ { name: "name", label: "Tool name" }, { name: "displayName", label: "Display name" }, { name: "toolTitle", label: "Tool title" }, { name: "toolDescription", - label: "Tool description", - description: "LLM-facing description for the MCP tool.", + label: "Description", + description: "LLM-facing description for the tool runtime contract.", rows: 4, wide: true, }, ]; const restFields: FieldConfig[] = [ - { name: "restBaseUrl", label: "Base URL" }, - { name: "restMethod", label: "HTTP method" }, + { name: "restBaseUrl", label: "Base URL", wide: true }, { name: "restPathTemplate", label: "Path template" }, - { - name: "restStaticHeadersText", - label: "Static headers", - description: "Always sent for this REST target before dynamic request headers are merged.", - rows: 8, - wide: true, - }, + { name: "restMethod", label: "HTTP method" }, ]; const graphqlFields: FieldConfig[] = [ - { name: "graphqlEndpoint", label: "GraphQL endpoint" }, + { name: "graphqlEndpoint", label: "GraphQL endpoint", wide: true }, { name: "graphqlOperationType", label: "Operation type" }, { name: "graphqlOperationName", label: "Operation name" }, { - name: "graphqlQueryTemplate", - label: "Query template", - description: "A fixed query or mutation document exposed as a single MCP tool.", - rows: 10, + name: "graphqlResponsePath", + label: "Response path", + description: "Stable extraction root inside the response payload.", wide: true, }, { - name: "graphqlResponsePath", - label: "Response path", - description: "JSONPath-like extraction root inside response preview.", + name: "graphqlQueryTemplate", + label: "Query template", + description: "Fixed GraphQL document exposed as one MCP tool.", + rows: 12, wide: true, + code: true, }, ]; const grpcFields: FieldConfig[] = [ - { name: "grpcServerAddr", label: "Server address" }, + { name: "grpcServerAddr", label: "Server address", wide: true }, { name: "grpcPackage", label: "Package" }, { name: "grpcService", label: "Service" }, { name: "grpcMethod", label: "Method" }, { name: "grpcDescriptorRef", label: "Descriptor reference", - description: "Stable descriptor identifier stored together with the operation.", + description: "Stable descriptor identifier stored with the operation.", + wide: true, }, { name: "grpcDescriptorSetB64", label: "Descriptor set base64", - description: "Use a descriptor-set file to prefill this field before creation.", - rows: 8, + description: "Compiled descriptor-set contents used for runtime invocation.", + rows: 10, wide: true, + code: true, }, ]; const schemaFields: FieldConfig[] = [ - { name: "inputSchemaText", label: "Input schema", rows: 12, wide: true }, - { name: "outputSchemaText", label: "Output schema", rows: 12, wide: true }, + { + name: "inputSchemaText", + label: "Input schema", + rows: 14, + wide: true, + code: true, + }, + { + name: "outputSchemaText", + label: "Output schema", + rows: 14, + wide: true, + code: true, + }, ]; const mappingFields: FieldConfig[] = [ - { name: "inputMappingText", label: "Input mapping", rows: 12, wide: true }, - { name: "outputMappingText", label: "Output mapping", rows: 12, wide: true }, + { + name: "inputMappingText", + label: "Input → Request mapping", + rows: 12, + wide: true, + code: true, + }, + { + name: "outputMappingText", + label: "Response → Output mapping", + rows: 12, + wide: true, + code: true, + }, +]; + +const headerFields: FieldConfig[] = [ { name: "executionHeadersText", label: "Execution headers", - description: "Shared transport headers available for REST, GraphQL and gRPC requests.", + description: "Shared transport headers for REST, GraphQL and gRPC requests.", rows: 8, wide: true, + code: true, }, { name: "executionConfigText", label: "Execution config", + description: "Timeouts, auth profile reference and protocol options.", rows: 10, wide: true, + code: true, }, ]; function protocolSummary(protocol: OperationFormValues["protocol"]) { switch (protocol) { case "rest": - return "One MCP tool maps to one HTTP method and one path template."; + return "One tool maps to one HTTP method and one path template."; case "graphql": - return "One MCP tool maps to one fixed GraphQL query or mutation with stable variables and response shape."; + return "One tool maps to one fixed GraphQL query or mutation."; case "grpc": - return "One MCP tool maps to one unary gRPC method backed by a descriptor set embedded at creation time."; + return "One tool maps to one unary gRPC method backed by a descriptor set."; } } @@ -202,53 +254,13 @@ export function OperationForm() { } } - function renderField(field: FieldConfig) { - const fieldError = form.formState.errors[field.name]; - const message = - typeof fieldError?.message === "string" ? fieldError.message : undefined; + function fieldError(name: OperationFieldName) { + const error = form.formState.errors[name]; + return typeof error?.message === "string" ? error.message : undefined; + } - if ( - field.name === "restMethod" || - field.name === "graphqlOperationType" || - field.name === "protocol" - ) { - return ( - - ); - } - - const isTextarea = field.rows !== undefined; + function renderSelect(field: FieldConfig) { + const message = fieldError(field.name); return (