291 lines
10 KiB
TypeScript
291 lines
10 KiB
TypeScript
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
import { useParams } from "react-router-dom";
|
|
|
|
import {
|
|
exportOperationYaml,
|
|
generateDraft,
|
|
getOperation,
|
|
getOperationVersion,
|
|
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 readObjectRecord(value: unknown) {
|
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
return {};
|
|
}
|
|
|
|
return value as Record<string, unknown>;
|
|
}
|
|
|
|
function readExecutionHeaders(value: unknown) {
|
|
const config = readObjectRecord(value);
|
|
const headers = config.headers;
|
|
|
|
if (headers === null || typeof headers !== "object" || Array.isArray(headers)) {
|
|
return {};
|
|
}
|
|
|
|
return headers as Record<string, unknown>;
|
|
}
|
|
|
|
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({
|
|
queryKey: ["operation-summary", operationId],
|
|
queryFn: () => getOperation(operationId),
|
|
enabled: operationId.length > 0,
|
|
});
|
|
const currentVersion = summaryQuery.data?.current_draft_version ?? 0;
|
|
const versionQuery = useQuery({
|
|
queryKey: ["operation-version", operationId, currentVersion],
|
|
queryFn: () => getOperationVersion(operationId, currentVersion),
|
|
enabled: operationId.length > 0 && currentVersion > 0,
|
|
});
|
|
const authProfilesQuery = useQuery({
|
|
queryKey: ["auth-profiles"],
|
|
queryFn: listAuthProfiles,
|
|
});
|
|
const draftMutation = useMutation({
|
|
mutationFn: async () => generateDraft(operationId),
|
|
});
|
|
const exportMutation = useMutation({
|
|
mutationFn: async () => exportOperationYaml(operationId, currentVersion),
|
|
});
|
|
const importMutation = useMutation({
|
|
mutationFn: async (yamlText: string) => importOperationYaml(yamlText, "upsert"),
|
|
});
|
|
|
|
const snapshot = versionQuery.data?.snapshot;
|
|
const exportedYaml = exportMutation.data;
|
|
const targetRows = snapshot ? describeTarget(snapshot.target) : [];
|
|
|
|
return (
|
|
<div className="page-stack">
|
|
<PageSection
|
|
title={summaryQuery.data?.display_name ?? "Operation"}
|
|
subtitle={
|
|
summaryQuery.data
|
|
? `${summaryQuery.data.name} · ${summaryQuery.data.protocol} · draft v${currentVersion}`
|
|
: "Loading operation metadata..."
|
|
}
|
|
>
|
|
{summaryQuery.data ? (
|
|
<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 className="result-panel">
|
|
<h3>Transport headers</h3>
|
|
<pre>{JSON.stringify(readExecutionHeaders(snapshot.execution_config), null, 2)}</pre>
|
|
</div>
|
|
{snapshot.target.kind === "rest" ? (
|
|
<div className="result-panel">
|
|
<h3>REST static headers</h3>
|
|
<pre>{JSON.stringify(snapshot.target.static_headers ?? {}, null, 2)}</pre>
|
|
</div>
|
|
) : null}
|
|
</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={snapshot ? draftSubtitle(snapshot.target) : "Prepare schema and mapping drafts."}
|
|
actions={
|
|
<button
|
|
className="button-primary"
|
|
onClick={() => draftMutation.mutate()}
|
|
type="button"
|
|
>
|
|
{draftMutation.isPending ? "Generating..." : "Generate draft"}
|
|
</button>
|
|
}
|
|
>
|
|
<SampleUploadPanel operationId={operationId} />
|
|
<SchemaViewerPanel
|
|
operationRecord={versionQuery.data}
|
|
draftResult={draftMutation.data}
|
|
/>
|
|
</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="Promote the current draft version into the active MCP tool catalog."
|
|
>
|
|
<PublishOperationPanel operationId={operationId} version={currentVersion} />
|
|
</PageSection>
|
|
</div>
|
|
|
|
<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>
|
|
) : null}
|
|
</div>
|
|
</PageSection>
|
|
|
|
<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>
|
|
))}
|
|
</div>
|
|
</PageSection>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|