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 | }; | ||