OpenAPI plugin now has three sub-path exports: - @executor/plugin-openapi (SDK plugin, unchanged) - @executor/plugin-openapi/api (HTTP routes + handlers) - OpenApiGroup: previewSpec, addSpec, listSpecs, removeSpec endpoints - OpenApiHandlers: Effect HttpApiBuilder handlers - OpenApiExtensionService: Context tag for wiring - @executor/plugin-openapi/react (React components + atoms) - OpenApiClient: typed AtomHttpApi client (core + openapi routes) - Atoms: openApiSpecsAtom, previewOpenApiSpec, addOpenApiSpec, removeOpenApiSpec - openApiSourcePlugin: implements SourcePlugin contract - Lazy-loaded components: AddOpenApiSource (multi-step), EditOpenApiSource, OpenApiSourceSummary Shared infrastructure: - @executor/api: addGroup() composes plugin HttpApiGroups with CoreExecutorApi - @executor/react: SourcePlugin contract type for shell↔plugin UI composition Shell owns layout/nav/common actions, plugin owns content within typed frames Pattern: plugins use addGroup() + AtomHttpApi.Tag to get typed query/mutation atoms for both core and plugin-specific routes. Peer deps with optional metadata so SDK-only consumers don't pull in React. 10 packages typecheck clean, 21 OpenAPI tests passing.
Showing the changes to packages/plugins/openapi/src/react (7 of 18 files). Show all
Unified · Split
| @@ -0,0 +1,55 @@ | ||
| 1 | +import { useState } from "react"; | |
| 2 | + | |
| 3 | +export default function AddOpenApiSource(props: { | |
| 4 | + onComplete: () => void; | |
| 5 | + onCancel: () => void; | |
| 6 | +}) { | |
| 7 | + const [step, setStep] = useState<"url" | "auth" | "confirm">("url"); | |
| 8 | + const [specUrl, setSpecUrl] = useState(""); | |
| 9 | + | |
| 10 | + return ( | |
| 11 | + <div> | |
| 12 | + <h3>Add OpenAPI Source</h3> | |
| 13 | + | |
| 14 | + {step === "url" && ( | |
| 15 | + <div> | |
| 16 | + <label> | |
| 17 | + Spec URL or paste content: | |
| 18 | + <input | |
| 19 | + type="text" | |
| 20 | + value={specUrl} | |
| 21 | + onChange={(e) => setSpecUrl((e.target as HTMLInputElement).value)} | |
| 22 | + placeholder="https://api.example.com/openapi.json" | |
| 23 | + /> | |
| 24 | + </label> | |
| 25 | + <div> | |
| 26 | + <button onClick={props.onCancel}>Cancel</button> | |
| 27 | + <button onClick={() => setStep("auth")} disabled={!specUrl}> | |
| 28 | + Next | |
| 29 | + </button> | |
| 30 | + </div> | |
| 31 | + </div> | |
| 32 | + )} | |
| 33 | + | |
| 34 | + {step === "auth" && ( | |
| 35 | + <div> | |
| 36 | + <p>Configure authentication headers…</p> | |
| 37 | + <div> | |
| 38 | + <button onClick={() => setStep("url")}>Back</button> | |
| 39 | + <button onClick={() => setStep("confirm")}>Next</button> | |
| 40 | + </div> | |
| 41 | + </div> | |
| 42 | + )} | |
| 43 | + | |
| 44 | + {step === "confirm" && ( | |
| 45 | + <div> | |
| 46 | + <p>Ready to add spec from: {specUrl}</p> | |
| 47 | + <div> | |
| 48 | + <button onClick={() => setStep("auth")}>Back</button> | |
| 49 | + <button onClick={props.onComplete}>Add Source</button> | |
| 50 | + </div> | |
| 51 | + </div> | |
| 52 | + )} | |
| 53 | + </div> | |
| 54 | + ); | |
| 55 | +} | |
| @@ -0,0 +1,13 @@ | ||
| 1 | +export default function EditOpenApiSource(props: { | |
| 2 | + sourceId: string; | |
| 3 | + onSave: () => void; | |
| 4 | +}) { | |
| 5 | + return ( | |
| 6 | + <div> | |
| 7 | + <h3>Edit OpenAPI Source</h3> | |
| 8 | + <p>Source: {props.sourceId}</p> | |
| 9 | + {/* TODO: show spec info, auth config, operation list */} | |
| 10 | + <button onClick={props.onSave}>Save</button> | |
| 11 | + </div> | |
| 12 | + ); | |
| 13 | +} | |
| @@ -0,0 +1,9 @@ | ||
| 1 | +export default function OpenApiSourceSummary(props: { | |
| 2 | + sourceId: string; | |
| 3 | +}) { | |
| 4 | + return ( | |
| 5 | + <span> | |
| 6 | + OpenAPI · {props.sourceId} | |
| 7 | + </span> | |
| 8 | + ); | |
| 9 | +} | |
| @@ -0,0 +1,35 @@ | ||
| 1 | +import { ScopeId } from "@executor/sdk"; | |
| 2 | + | |
| 3 | +import { OpenApiClient } from "./client"; | |
| 4 | + | |
| 5 | +// --------------------------------------------------------------------------- | |
| 6 | +// Default scope | |
| 7 | +// --------------------------------------------------------------------------- | |
| 8 | + | |
| 9 | +const DEFAULT_SCOPE = ScopeId.make("default"); | |
| 10 | + | |
| 11 | +// --------------------------------------------------------------------------- | |
| 12 | +// Query atoms | |
| 13 | +// --------------------------------------------------------------------------- | |
| 14 | + | |
| 15 | +export const openApiSpecsAtom = (scopeId: ScopeId = DEFAULT_SCOPE) => | |
| 16 | + OpenApiClient.query("openapi", "listSpecs", { | |
| 17 | + path: { scopeId }, | |
| 18 | + timeToLive: "30 seconds", | |
| 19 | + }); | |
| 20 | + | |
| 21 | +// --------------------------------------------------------------------------- | |
| 22 | +// Mutation atoms | |
| 23 | +// --------------------------------------------------------------------------- | |
| 24 | + | |
| 25 | +export const previewOpenApiSpec = OpenApiClient.mutation( | |
| 26 | + "openapi", | |
| 27 | + "previewSpec", | |
| 28 | +); | |
| 29 | + | |
| 30 | +export const addOpenApiSpec = OpenApiClient.mutation("openapi", "addSpec"); | |
| 31 | + | |
| 32 | +export const removeOpenApiSpec = OpenApiClient.mutation( | |
| 33 | + "openapi", | |
| 34 | + "removeSpec", | |
| 35 | +); | |
| @@ -0,0 +1,20 @@ | ||
| 1 | +import { AtomHttpApi } from "@effect-atom/atom-react"; | |
| 2 | +import { FetchHttpClient } from "@effect/platform"; | |
| 3 | +import { addGroup } from "@executor/api"; | |
| 4 | +import { getBaseUrl } from "@executor/react"; | |
| 5 | +import { OpenApiGroup } from "../api/group"; | |
| 6 | + | |
| 7 | +// --------------------------------------------------------------------------- | |
| 8 | +// OpenAPI-aware client — core routes + openapi routes | |
| 9 | +// --------------------------------------------------------------------------- | |
| 10 | + | |
| 11 | +const OpenApiApi = addGroup(OpenApiGroup); | |
| 12 | + | |
| 13 | +export const OpenApiClient = AtomHttpApi.Tag<"OpenApiClient">()( | |
| 14 | + "OpenApiClient", | |
| 15 | + { | |
| 16 | + api: OpenApiApi, | |
| 17 | + httpClient: FetchHttpClient.layer, | |
| 18 | + baseUrl: getBaseUrl(), | |
| 19 | + }, | |
| 20 | +); | |
| @@ -0,0 +1,8 @@ | ||
| 1 | +export { openApiSourcePlugin } from "./source-plugin"; | |
| 2 | +export { OpenApiClient } from "./client"; | |
| 3 | +export { | |
| 4 | + openApiSpecsAtom, | |
| 5 | + previewOpenApiSpec, | |
| 6 | + addOpenApiSpec, | |
| 7 | + removeOpenApiSpec, | |
| 8 | +} from "./atoms"; | |
| @@ -0,0 +1,14 @@ | ||
| 1 | +import { lazy } from "react"; | |
| 2 | +import type { SourcePlugin } from "@executor/react"; | |
| 3 | + | |
| 4 | +// --------------------------------------------------------------------------- | |
| 5 | +// OpenAPI source plugin — lazy-loaded components | |
| 6 | +// --------------------------------------------------------------------------- | |
| 7 | + | |
| 8 | +export const openApiSourcePlugin: SourcePlugin = { | |
| 9 | + key: "openapi", | |
| 10 | + label: "OpenAPI", | |
| 11 | + add: lazy(() => import("./AddOpenApiSource")), | |
| 12 | + edit: lazy(() => import("./EditOpenApiSource")), | |
| 13 | + summary: lazy(() => import("./OpenApiSourceSummary")), | |
| 14 | +}; | |