feat: polish protocol-aware operator UI

This commit is contained in:
a.tolmachev
2026-03-25 21:52:53 +03:00
parent f8b79906ed
commit c1447c8365
12 changed files with 1246 additions and 255 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ export function OperationCreatePage() {
<div className="page-stack">
<PageSection
title="Create Operation"
subtitle="REST-first creation flow for the first production UI slice. GraphQL and gRPC will reuse the same operator surface later."
subtitle="Create one MCP tool from one fixed REST, GraphQL or unary gRPC contract."
>
<OperationForm />
</PageSection>
+178 -93
View File
@@ -6,15 +6,51 @@ import {
generateDraft,
getOperation,
getOperationVersion,
listAuthProfiles,
importOperationYaml,
listAuthProfiles,
} from "../../entities/operation/api";
import type { OperationTarget } from "../../entities/operation/types";
import { GrpcDescriptorPanel } from "../../features/grpc-descriptor/panel";
import { PublishOperationPanel } from "../../features/publish-operation/panel";
import { SampleUploadPanel } from "../../features/sample-upload/panel";
import { SchemaViewerPanel } from "../../features/schema-viewer/panel";
import { TestRunPanel } from "../../features/test-run/panel";
import { PageSection } from "../../shared/ui/page-section";
function describeTarget(target: OperationTarget) {
switch (target.kind) {
case "rest":
return [
["Base URL", target.base_url],
["Method", target.method],
["Path", target.path_template],
] as const;
case "graphql":
return [
["Endpoint", target.endpoint],
["Operation", `${target.operation_type} ${target.operation_name}`],
["Response path", target.response_path],
] as const;
case "grpc":
return [
["Server", target.server_addr],
["Service", `${target.package}.${target.service}/${target.method}`],
["Descriptor ref", target.descriptor_ref],
] as const;
}
}
function draftSubtitle(target: OperationTarget) {
switch (target.kind) {
case "rest":
return "Upload request and response JSON samples, then infer schema and mapping drafts.";
case "graphql":
return "Upload stable request and response samples for the fixed GraphQL operation contract.";
case "grpc":
return "Upload JSON samples or inspect descriptor-driven schemas before refining mappings.";
}
}
export function OperationDetailPage() {
const { operationId = "" } = useParams();
const summaryQuery = useQuery({
@@ -22,18 +58,11 @@ export function OperationDetailPage() {
queryFn: () => getOperation(operationId),
enabled: operationId.length > 0,
});
const currentVersion = summaryQuery.data?.current_draft_version ?? 0;
const versionQuery = useQuery({
queryKey: [
"operation-version",
operationId,
summaryQuery.data?.current_draft_version,
],
queryFn: () =>
getOperationVersion(operationId, summaryQuery.data!.current_draft_version),
enabled:
operationId.length > 0 &&
summaryQuery.data !== undefined &&
summaryQuery.data.current_draft_version > 0,
queryKey: ["operation-version", operationId, currentVersion],
queryFn: () => getOperationVersion(operationId, currentVersion),
enabled: operationId.length > 0 && currentVersion > 0,
});
const authProfilesQuery = useQuery({
queryKey: ["auth-profiles"],
@@ -43,15 +72,15 @@ export function OperationDetailPage() {
mutationFn: async () => generateDraft(operationId),
});
const exportMutation = useMutation({
mutationFn: async () =>
exportOperationYaml(operationId, summaryQuery.data?.current_draft_version),
mutationFn: async () => exportOperationYaml(operationId, currentVersion),
});
const importMutation = useMutation({
mutationFn: async (yamlText: string) => importOperationYaml(yamlText, "upsert"),
});
const currentVersion = summaryQuery.data?.current_draft_version ?? 0;
const snapshot = versionQuery.data?.snapshot;
const exportedYaml = exportMutation.data;
const targetRows = snapshot ? describeTarget(snapshot.target) : [];
return (
<div className="page-stack">
@@ -64,26 +93,76 @@ export function OperationDetailPage() {
}
>
{summaryQuery.data ? (
<div className="summary-grid">
<div className="summary-card">
<span>Status</span>
<strong>{summaryQuery.data.status}</strong>
</div>
<div className="summary-card">
<span>Published version</span>
<strong>{summaryQuery.data.latest_published_version ?? "none"}</strong>
</div>
<div className="summary-card">
<span>Updated</span>
<strong>{new Date(summaryQuery.data.updated_at).toLocaleString()}</strong>
<div className="detail-hero">
<div className="summary-grid">
<div className="summary-card">
<span>Status</span>
<strong>{summaryQuery.data.status}</strong>
</div>
<div className="summary-card">
<span>Published version</span>
<strong>{summaryQuery.data.latest_published_version ?? "none"}</strong>
</div>
<div className="summary-card">
<span>Updated</span>
<strong>{new Date(summaryQuery.data.updated_at).toLocaleString()}</strong>
</div>
</div>
{snapshot ? (
<div className="target-grid">
{targetRows.map(([label, value]) => (
<div className="target-card" key={label}>
<span>{label}</span>
<strong>{value}</strong>
</div>
))}
</div>
) : null}
</div>
) : null}
</PageSection>
{snapshot ? (
<PageSection
title="Target Contract"
subtitle="The runtime transport details below are the fixed contract exposed as one MCP tool."
>
<div className="metadata-grid">
<div className="result-panel">
<h3>Tool description</h3>
<dl className="key-value-list">
<div>
<dt>Title</dt>
<dd>{snapshot.tool_description.title}</dd>
</div>
<div>
<dt>Tags</dt>
<dd>{snapshot.tool_description.tags.join(", ") || "none"}</dd>
</div>
</dl>
<p className="body-copy">{snapshot.tool_description.description}</p>
</div>
<div className="result-panel">
<h3>Execution config</h3>
<pre>{JSON.stringify(snapshot.execution_config, null, 2)}</pre>
</div>
</div>
</PageSection>
) : null}
{snapshot?.target.kind === "grpc" ? (
<PageSection
title="gRPC Discovery"
subtitle="Descriptor artifacts and discovered unary methods for the current draft version."
>
<GrpcDescriptorPanel operationId={operationId} version={currentVersion} />
</PageSection>
) : null}
<PageSection
title="Samples And Draft"
subtitle="Upload input/output samples first, then generate a draft schema and mapping set."
subtitle={snapshot ? draftSubtitle(snapshot.target) : "Prepare schema and mapping drafts."}
actions={
<button
className="button-primary"
@@ -101,76 +180,82 @@ export function OperationDetailPage() {
/>
</PageSection>
<PageSection
title="Test Run"
subtitle="Run the current draft version through the backend runtime before publishing."
>
<TestRunPanel operationId={operationId} version={currentVersion} />
</PageSection>
<div className="detail-columns">
<PageSection
title="Test Run"
subtitle="Run the current draft version through the runtime before publishing."
>
<TestRunPanel operationId={operationId} version={currentVersion} />
</PageSection>
<PageSection
title="Publish"
subtitle="Move the current draft version into the published tool set exposed by MCP."
>
<PublishOperationPanel operationId={operationId} version={currentVersion} />
</PageSection>
<PageSection
title="Publish"
subtitle="Promote the current draft version into the active MCP tool catalog."
>
<PublishOperationPanel operationId={operationId} version={currentVersion} />
</PageSection>
</div>
<PageSection
title="YAML Export And Reimport"
subtitle="Export the current version, inspect the YAML, then upsert it back through the same backend contract."
actions={
<button
className="button-secondary"
type="button"
onClick={() => exportMutation.mutate()}
>
{exportMutation.isPending ? "Exporting..." : "Export YAML"}
</button>
}
>
<div className="stack-layout">
{exportedYaml ? (
<>
<pre className="yaml-panel">{exportedYaml}</pre>
<div className="button-row">
<button
className="button-primary"
type="button"
onClick={() => importMutation.mutate(exportedYaml)}
>
{importMutation.isPending ? "Importing..." : "Upsert from YAML"}
</button>
<div className="detail-columns">
<PageSection
title="YAML Export And Reimport"
subtitle="Export the current version, inspect the YAML, then upsert it through the same backend contract."
actions={
<button
className="button-secondary"
type="button"
onClick={() => exportMutation.mutate()}
>
{exportMutation.isPending ? "Exporting..." : "Export YAML"}
</button>
}
>
<div className="stack-layout">
{exportedYaml ? (
<>
<pre className="yaml-panel">{exportedYaml}</pre>
<div className="button-row">
<button
className="button-primary"
type="button"
onClick={() => importMutation.mutate(exportedYaml)}
>
{importMutation.isPending ? "Importing..." : "Upsert from YAML"}
</button>
</div>
</>
) : (
<p className="body-copy">
Export YAML to inspect the current operation snapshot and transport contract.
</p>
)}
{importMutation.data ? (
<div className="feedback-card feedback-success">
Imported version {String(importMutation.data.version)} in{" "}
{String(importMutation.data.import_mode)} mode.
</div>
</>
) : (
<p>Export YAML to inspect the current operation snapshot.</p>
)}
{importMutation.data ? (
<div className="feedback-card feedback-success">
Imported version {String(importMutation.data.version)} in{" "}
{String(importMutation.data.import_mode)} mode.
</div>
) : null}
</div>
</PageSection>
) : null}
</div>
</PageSection>
<PageSection
title="Auth Profiles"
subtitle="Currently available auth profiles exposed by the admin backend."
>
<div className="operation-grid">
{(authProfilesQuery.data?.items ?? []).map((profile) => (
<div className="operation-card" key={profile.id}>
<div className="operation-card-top">
<span className="protocol-pill protocol-rest">{profile.kind}</span>
<PageSection
title="Auth Profiles"
subtitle="Profiles currently available to the admin backend and execution config."
>
<div className="operation-grid">
{(authProfilesQuery.data?.items ?? []).map((profile) => (
<div className="operation-card" key={profile.id}>
<div className="operation-card-top">
<span className="status-pill status-testing">{profile.kind}</span>
</div>
<h3>{profile.name}</h3>
<p className="operation-name">{profile.id}</p>
<pre>{JSON.stringify(profile.config, null, 2)}</pre>
</div>
<h3>{profile.name}</h3>
<p className="operation-name">{profile.id}</p>
<pre>{JSON.stringify(profile.config, null, 2)}</pre>
</div>
))}
</div>
</PageSection>
))}
</div>
</PageSection>
</div>
</div>
);
}