- Wire OpenAPI plugin into server (executor config + API handlers) - AddOpenApiSource calls previewSpec + addSpec APIs through the flow - Sources page uses core toolsAtom() grouped by namespace tag - Remove listSpecs/removeSpec from OpenAPI API (core tools handles it) - Remove listNamespaces from operation store, listSpecs from extension - Sources page refreshes tool list on add complete
Unified · Split
| @@ -32,4 +32,5 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json · 31 unmodified lines | ||
| 32 | 32 | |
| 33 | 33 | # Finder (MacOS) folder config |
| 34 | 34 | .DS_Store |
| 35 | −personal-notes/ | |
| 35 | +personal-notes/ | |
| 36 | +*.har | |
| @@ -10,6 +10,7 @@ · 9 unmodified lines | ||
| 10 | 10 | "@effect/platform": "catalog:", |
| 11 | 11 | |
| 12 | 12 | "@executor/api": "workspace:*", |
| 13 | + "@executor/plugin-openapi": "workspace:*", | |
| 13 | 14 | "@executor/sdk": "workspace:*", |
| 14 | 15 | "effect": "catalog:" |
| 15 | 16 | }, |
| @@ -0,0 +1,17 @@ | ||
| 1 | +import { Effect, Layer } from "effect"; | |
| 2 | +import { | |
| 3 | + OpenApiHandlers, | |
| 4 | + OpenApiExtensionService, | |
| 5 | +} from "@executor/plugin-openapi/api"; | |
| 6 | +import { ExecutorService } from "../services/executor"; | |
| 7 | + | |
| 8 | +// Wire OpenApiExtensionService from the executor's openapi extension | |
| 9 | +const OpenApiExtensionLive = Layer.effect( | |
| 10 | + OpenApiExtensionService, | |
| 11 | + Effect.map(ExecutorService, (executor) => executor.openapi), | |
| 12 | +); | |
| 13 | + | |
| 14 | +export const OpenApiHandlersLive = Layer.provide( | |
| 15 | + OpenApiHandlers, | |
| 16 | + OpenApiExtensionLive, | |
| 17 | +); | |
| @@ -6,17 +6,25 @@ import { · 5 unmodified lines | ||
| 6 | 6 | } from "@effect/platform"; |
| 7 | 7 | import { Layer } from "effect"; |
| 8 | 8 | |
| 9 | −import { ExecutorApi } from "@executor/api"; | |
| 9 | +import { addGroup } from "@executor/api"; | |
| 10 | +import { OpenApiGroup } from "@executor/plugin-openapi/api"; | |
| 10 | 11 | import { ToolsHandlers } from "./handlers/tools"; |
| 11 | 12 | import { SecretsHandlers } from "./handlers/secrets"; |
| 13 | +import { OpenApiHandlersLive } from "./handlers/openapi"; | |
| 12 | 14 | import { ExecutorServiceLive } from "./services/executor"; |
| 13 | 15 | |
| 16 | +// --------------------------------------------------------------------------- | |
| 17 | +// Composed API — core + plugin groups | |
| 18 | +// --------------------------------------------------------------------------- | |
| 19 | + | |
| 20 | +const ExecutorApiWithPlugins = addGroup(OpenApiGroup); | |
| 21 | + | |
| 14 | 22 | // --------------------------------------------------------------------------- |
| 15 | 23 | // API layer — wire handlers |
| 16 | 24 | // --------------------------------------------------------------------------- |
| 17 | 25 | |
| 18 | −const ApiLive = HttpApiBuilder.api(ExecutorApi).pipe( | |
| 19 | − Layer.provide([ToolsHandlers, SecretsHandlers]), | |
| 26 | +const ApiLive = HttpApiBuilder.api(ExecutorApiWithPlugins).pipe( | |
| 27 | + Layer.provide([ToolsHandlers, SecretsHandlers, OpenApiHandlersLive]), | |
| 20 | 28 | Layer.provide(ExecutorServiceLive), |
| 21 | 29 | ); |
| 22 | 30 | |
| @@ -1,7 +1,12 @@ | ||
| 1 | 1 | import { Context, Effect, Layer } from "effect"; |
| 2 | 2 | import { createExecutor, makeTestConfig } from "@executor/sdk"; |
| 3 | +import { openApiPlugin, type OpenApiPluginExtension } from "@executor/plugin-openapi"; | |
| 3 | 4 | |
| 4 | 5 | import type { Executor } from "@executor/sdk"; |
| 6 | +import type { ExecutorPlugin } from "@executor/sdk"; | |
| 7 | + | |
| 8 | +type ServerPlugins = readonly [ExecutorPlugin<"openapi", OpenApiPluginExtension>]; | |
| 9 | +type ServerExecutor = Executor<ServerPlugins>; | |
| 5 | 10 | |
| 6 | 11 | // --------------------------------------------------------------------------- |
| 7 | 12 | // Service tag — provides the executor instance to HTTP handlers |
| @@ -9,14 +14,18 @@ import type { Executor } from "@executor/sdk"; · 1 unmodified line | ||
| 9 | 14 | |
| 10 | 15 | export class ExecutorService extends Context.Tag("ExecutorService")< |
| 11 | 16 | ExecutorService, |
| 12 | − Executor | |
| 17 | + ServerExecutor | |
| 13 | 18 | >() {} |
| 14 | 19 | |
| 15 | 20 | // --------------------------------------------------------------------------- |
| 16 | −// Default layer — creates an in-memory executor | |
| 21 | +// Default layer — creates an in-memory executor with plugins | |
| 17 | 22 | // --------------------------------------------------------------------------- |
| 18 | 23 | |
| 19 | 24 | export const ExecutorServiceLive = Layer.effect( |
| 20 | 25 | ExecutorService, |
| 21 | − createExecutor(makeTestConfig()), | |
| 26 | + createExecutor( | |
| 27 | + makeTestConfig({ | |
| 28 | + plugins: [openApiPlugin()] as const, | |
| 29 | + }), | |
| 30 | + ), | |
| 22 | 31 | ); |
| @@ -4,6 +4,7 @@ · 3 unmodified lines | ||
| 4 | 4 | "private": true, |
| 5 | 5 | "type": "module", |
| 6 | 6 | "dependencies": { |
| 7 | + "@executor/plugin-openapi": "workspace:*", | |
| 7 | 8 | "@executor/react": "workspace:*", |
| 8 | 9 | "@executor/server": "workspace:*", |
| 9 | 10 | "@tanstack/react-router": "^1.168.10", |
| @@ -1,8 +1,92 @@ | ||
| 1 | +import { useState, Suspense, useMemo } from "react"; | |
| 2 | +import { Result, useAtomValue, useAtomRefresh, toolsAtom } from "@executor/react"; | |
| 3 | +import type { SourcePlugin } from "@executor/react"; | |
| 4 | +import { openApiSourcePlugin } from "@executor/plugin-openapi/react"; | |
| 5 | + | |
| 6 | +// --------------------------------------------------------------------------- | |
| 7 | +// Registered source plugins | |
| 8 | +// --------------------------------------------------------------------------- | |
| 9 | + | |
| 10 | +const sourcePlugins: SourcePlugin[] = [openApiSourcePlugin]; | |
| 11 | + | |
| 12 | +// --------------------------------------------------------------------------- | |
| 13 | +// Page | |
| 14 | +// --------------------------------------------------------------------------- | |
| 15 | + | |
| 1 | 16 | export function SourcesPage() { |
| 17 | + const [adding, setAdding] = useState<string | null>(null); | |
| 18 | + const tools = useAtomValue(toolsAtom()); | |
| 19 | + const refreshTools = useAtomRefresh(toolsAtom()); | |
| 20 | + | |
| 21 | + // Group tools by namespace (second tag after "openapi") | |
| 22 | + const sources = useMemo(() => { | |
| 23 | + if (tools._tag !== "Success") return []; | |
| 24 | + const namespaces = new Map<string, number>(); | |
| 25 | + for (const tool of tools.value) { | |
| 26 | + // Tools are tagged ["openapi", namespace, ...] | |
| 27 | + const ns = tool.tags.find( | |
| 28 | + (t) => t !== "openapi" && !sourcePlugins.some((p) => p.key === t), | |
| 29 | + ); | |
| 30 | + if (ns) { | |
| 31 | + namespaces.set(ns, (namespaces.get(ns) ?? 0) + 1); | |
| 32 | + } | |
| 33 | + } | |
| 34 | + return [...namespaces.entries()].map(([namespace, toolCount]) => ({ | |
| 35 | + namespace, | |
| 36 | + toolCount, | |
| 37 | + })); | |
| 38 | + }, [tools]); | |
| 39 | + | |
| 40 | + const plugin = adding | |
| 41 | + ? sourcePlugins.find((p) => p.key === adding) | |
| 42 | + : undefined; | |
| 43 | + | |
| 44 | + if (plugin) { | |
| 45 | + const AddComponent = plugin.add; | |
| 46 | + return ( | |
| 47 | + <Suspense fallback={<p>Loading…</p>}> | |
| 48 | + <AddComponent | |
| 49 | + onComplete={() => { | |
| 50 | + setAdding(null); | |
| 51 | + refreshTools(); | |
| 52 | + }} | |
| 53 | + onCancel={() => setAdding(null)} | |
| 54 | + /> | |
| 55 | + </Suspense> | |
| 56 | + ); | |
| 57 | + } | |
| 58 | + | |
| 2 | 59 | return ( |
| 3 | 60 | <div> |
| 4 | 61 | <h2>Sources</h2> |
| 5 | − <p style={{ color: "#888" }}>No sources configured yet.</p> | |
| 62 | + | |
| 63 | + {Result.match(tools, { | |
| 64 | + onInitial: () => <p>Loading…</p>, | |
| 65 | + onFailure: () => <p style={{ color: "red" }}>Failed to load sources</p>, | |
| 66 | + onSuccess: () => | |
| 67 | + sources.length === 0 ? ( | |
| 68 | + <p style={{ color: "#888" }}>No sources configured yet.</p> | |
| 69 | + ) : ( | |
| 70 | + <ul> | |
| 71 | + {sources.map((s) => ( | |
| 72 | + <li key={s.namespace}> | |
| 73 | + <strong>{s.namespace}</strong> — {s.toolCount} tools | |
| 74 | + </li> | |
| 75 | + ))} | |
| 76 | + </ul> | |
| 77 | + ), | |
| 78 | + })} | |
| 79 | + | |
| 80 | + <div style={{ marginTop: "1rem" }}> | |
| 81 | + <h3>Add a source</h3> | |
| 82 | + <div style={{ display: "flex", gap: "0.5rem" }}> | |
| 83 | + {sourcePlugins.map((p) => ( | |
| 84 | + <button key={p.key} onClick={() => setAdding(p.key)}> | |
| 85 | + {p.label} | |
| 86 | + </button> | |
| 87 | + ))} | |
| 88 | + </div> | |
| 89 | + </div> | |
| 6 | 90 | </div> |
| 7 | 91 | ); |
| 8 | 92 | } |
| @@ -17,6 +17,7 @@ · 16 unmodified lines | ||
| 17 | 17 | "dependencies": { |
| 18 | 18 | "@effect/platform": "catalog:", |
| 19 | 19 | "@executor/api": "workspace:*", |
| 20 | + "@executor/plugin-openapi": "workspace:*", | |
| 20 | 21 | "@executor/sdk": "workspace:*", |
| 21 | 22 | "effect": "catalog:", |
| 22 | 23 | }, |
| @@ -30,6 +31,7 @@ · 7 unmodified lines | ||
| 30 | 31 | "name": "@executor/web", |
| 31 | 32 | "version": "0.0.0", |
| 32 | 33 | "dependencies": { |
| 34 | + "@executor/plugin-openapi": "workspace:*", | |
| 33 | 35 | "@executor/react": "workspace:*", |
| 34 | 36 | "@executor/server": "workspace:*", |
| 35 | 37 | "@tanstack/react-router": "^1.168.10", |
| @@ -175,12 +177,14 @@ · 139 unmodified lines | ||
| 175 | 177 | "@effect-atom/atom-react": "^0.5.0", |
| 176 | 178 | "@executor/api": "workspace:*", |
| 177 | 179 | "@executor/react": "workspace:*", |
| 180 | + "@tanstack/react-router": "^1.168.10", | |
| 178 | 181 | "react": "^19.1.0", |
| 179 | 182 | }, |
| 180 | 183 | "optionalPeers": [ |
| 181 | 184 | "@effect-atom/atom-react", |
| 182 | 185 | "@executor/api", |
| 183 | 186 | "@executor/react", |
| 187 | + "@tanstack/react-router", | |
| 184 | 188 | "react", |
| 185 | 189 | ], |
| 186 | 190 | }, |
| @@ -25,12 +25,14 @@ · 24 unmodified lines | ||
| 25 | 25 | "peerDependencies": { |
| 26 | 26 | "react": "^19.1.0", |
| 27 | 27 | "@effect-atom/atom-react": "^0.5.0", |
| 28 | + "@tanstack/react-router": "^1.168.10", | |
| 28 | 29 | "@executor/api": "workspace:*", |
| 29 | 30 | "@executor/react": "workspace:*" |
| 30 | 31 | }, |
| 31 | 32 | "peerDependenciesMeta": { |
| 32 | 33 | "react": { "optional": true }, |
| 33 | 34 | "@effect-atom/atom-react": { "optional": true }, |
| 35 | + "@tanstack/react-router": { "optional": true }, | |
| 34 | 36 | "@executor/api": { "optional": true }, |
| 35 | 37 | "@executor/react": { "optional": true } |
| 36 | 38 | }, |
| @@ -40,13 +40,6 @@ const AddSpecResponse = Schema.Struct({ · 39 unmodified lines | ||
| 40 | 40 | namespace: Schema.String, |
| 41 | 41 | }); |
| 42 | 42 | |
| 43 | −const ListSpecsResponse = Schema.Array( | |
| 44 | − Schema.Struct({ | |
| 45 | − namespace: Schema.String, | |
| 46 | − toolCount: Schema.Number, | |
| 47 | − }), | |
| 48 | −); | |
| 49 | − | |
| 50 | 43 | // --------------------------------------------------------------------------- |
| 51 | 44 | // Errors with HTTP status |
| 52 | 45 | // --------------------------------------------------------------------------- |
| @@ -62,8 +55,6 @@ const ExtractionError = OpenApiExtractionError.annotations( · 9 unmodified lines | ||
| 62 | 55 | // Group |
| 63 | 56 | // --------------------------------------------------------------------------- |
| 64 | 57 | |
| 65 | −const namespaceParam = HttpApiSchema.param("namespace", Schema.String); | |
| 66 | − | |
| 67 | 58 | export class OpenApiGroup extends HttpApiGroup.make("openapi") |
| 68 | 59 | .add( |
| 69 | 60 | HttpApiEndpoint.post("previewSpec")`/scopes/${scopeIdParam}/openapi/preview` |
| @@ -79,12 +70,4 @@ export class OpenApiGroup extends HttpApiGroup.make("openapi") · 9 unmodified lines | ||
| 79 | 70 | .addError(ParseError) |
| 80 | 71 | .addError(ExtractionError), |
| 81 | 72 | ) |
| 82 | − .add( | |
| 83 | − HttpApiEndpoint.get("listSpecs")`/scopes/${scopeIdParam}/openapi/specs` | |
| 84 | − .addSuccess(ListSpecsResponse), | |
| 85 | − ) | |
| 86 | − .add( | |
| 87 | − HttpApiEndpoint.del("removeSpec")`/scopes/${scopeIdParam}/openapi/specs/${namespaceParam}` | |
| 88 | − .addSuccess(Schema.Struct({ removed: Schema.Boolean })), | |
| 89 | − ) | |
| 90 | 73 | .prefix("/v1") {} |
| @@ -49,15 +49,5 @@ export const OpenApiHandlers = HttpApiBuilder.group( · 48 unmodified lines | ||
| 49 | 49 | }; |
| 50 | 50 | }).pipe(Effect.orDie), |
| 51 | 51 | ) |
| 52 | − .handle("listSpecs", () => | |
| 53 | − // TODO: wire to operation store listByNamespace | |
| 54 | − Effect.succeed([] as { namespace: string; toolCount: number }[]), | |
| 55 | − ) | |
| 56 | − .handle("removeSpec", ({ path }) => | |
| 57 | − Effect.gen(function* () { | |
| 58 | − const ext = yield* OpenApiExtensionService; | |
| 59 | − yield* ext.removeSpec(path.namespace); | |
| 60 | − return { removed: true }; | |
| 61 | − }), | |
| 62 | − ), | |
| 52 | + , | |
| 63 | 53 | ); |
| @@ -1,55 +1,202 @@ | ||
| 1 | −import { useState } from "react"; | |
| 1 | +import { useState, useMemo, createContext, useContext } from "react"; | |
| 2 | +import { | |
| 3 | + createMemoryHistory, | |
| 4 | + createRootRoute, | |
| 5 | + createRoute, | |
| 6 | + createRouter, | |
| 7 | + Outlet, | |
| 8 | + RouterProvider, | |
| 9 | +} from "@tanstack/react-router"; | |
| 10 | +import { useAtomSet, Result } from "@effect-atom/atom-react"; | |
| 11 | +import { Option } from "effect"; | |
| 2 | 12 | |
| 3 | −export default function AddOpenApiSource(props: { | |
| 13 | +import { previewOpenApiSpec, addOpenApiSpec } from "./atoms"; | |
| 14 | +import type { SpecPreview } from "../sdk/preview"; | |
| 15 | + | |
| 16 | +// --------------------------------------------------------------------------- | |
| 17 | +// Shared state via context | |
| 18 | +// --------------------------------------------------------------------------- | |
| 19 | + | |
| 20 | +type Navigate = (to: string) => void; | |
| 21 | +const NavContext = createContext<Navigate>(() => {}); | |
| 22 | +const useNav = () => useContext(NavContext); | |
| 23 | + | |
| 24 | +interface AddState { | |
| 25 | + specUrl: string; | |
| 26 | + setSpecUrl: (v: string) => void; | |
| 27 | + preview: Result.Result<SpecPreview, unknown> | null; | |
| 28 | + doPreview: (spec: string) => void; | |
| 29 | + doAdd: (spec: string, baseUrl?: string, headers?: Record<string, unknown>) => void; | |
| 4 | 30 | onComplete: () => void; |
| 5 | 31 | onCancel: () => void; |
| 6 | −}) { | |
| 7 | − const [step, setStep] = useState<"url" | "auth" | "confirm">("url"); | |
| 8 | − const [specUrl, setSpecUrl] = useState(""); | |
| 32 | +} | |
| 33 | + | |
| 34 | +const AddStateContext = createContext<AddState>(null!); | |
| 35 | +const useAddState = () => useContext(AddStateContext); | |
| 36 | + | |
| 37 | +// --------------------------------------------------------------------------- | |
| 38 | +// Steps | |
| 39 | +// --------------------------------------------------------------------------- | |
| 40 | + | |
| 41 | +function UrlStep() { | |
| 42 | + const { specUrl, setSpecUrl, onCancel, doPreview } = useAddState(); | |
| 43 | + const nav = useNav(); | |
| 44 | + | |
| 45 | + const handleNext = () => { | |
| 46 | + doPreview(specUrl); | |
| 47 | + nav("/auth"); | |
| 48 | + }; | |
| 9 | 49 | |
| 10 | 50 | return ( |
| 11 | 51 | <div> |
| 12 | − <h3>Add OpenAPI Source</h3> | |
| 52 | + <label> | |
| 53 | + Spec URL or paste JSON/YAML: | |
| 54 | + <textarea | |
| 55 | + value={specUrl} | |
| 56 | + onChange={(e) => setSpecUrl((e.target as HTMLTextAreaElement).value)} | |
| 57 | + placeholder={'https://api.example.com/openapi.json\n\nor paste spec content here...'} | |
| 58 | + rows={6} | |
| 59 | + style={{ width: "100%", fontFamily: "monospace", fontSize: "0.85rem" }} | |
| 60 | + /> | |
| 61 | + </label> | |
| 62 | + <div style={{ marginTop: "0.5rem", display: "flex", gap: "0.5rem" }}> | |
| 63 | + <button onClick={onCancel}>Cancel</button> | |
| 64 | + <button disabled={!specUrl.trim()} onClick={handleNext}> | |
| 65 | + Next | |
| 66 | + </button> | |
| 67 | + </div> | |
| 68 | + </div> | |
| 69 | + ); | |
| 70 | +} | |
| 13 | 71 | |
| 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 | − )} | |
| 72 | +function AuthStep() { | |
| 73 | + const { preview } = useAddState(); | |
| 74 | + const nav = useNav(); | |
| 33 | 75 | |
| 34 | − {step === "auth" && ( | |
| 35 | − <div> | |
| 36 | − <p>Configure authentication headers…</p> | |
| 76 | + return ( | |
| 77 | + <div> | |
| 78 | + {preview && Result.match(preview, { | |
| 79 | + onInitial: () => <p>Analyzing spec…</p>, | |
| 80 | + onSuccess: ({ value }) => ( | |
| 37 | 81 | <div> |
| 38 | − <button onClick={() => setStep("url")}>Back</button> | |
| 39 | − <button onClick={() => setStep("confirm")}>Next</button> | |
| 82 | + <h4>{Option.getOrElse(value.title, () => "API")} {Option.getOrElse(value.version, () => "")}</h4> | |
| 83 | + <p>{value.operationCount} operations found</p> | |
| 84 | + {value.headerPresets.length > 0 && ( | |
| 85 | + <div> | |
| 86 | + <p>Authentication options:</p> | |
| 87 | + <ul> | |
| 88 | + {value.headerPresets.map((p, i) => ( | |
| 89 | + <li key={i}>{p.label}</li> | |
| 90 | + ))} | |
| 91 | + </ul> | |
| 92 | + </div> | |
| 93 | + )} | |
| 40 | 94 | </div> |
| 41 | − </div> | |
| 42 | − )} | |
| 95 | + ), | |
| 96 | + onFailure: () => <p style={{ color: "red" }}>Failed to parse spec</p>, | |
| 97 | + })} | |
| 98 | + <div style={{ marginTop: "0.5rem", display: "flex", gap: "0.5rem" }}> | |
| 99 | + <button onClick={() => nav("/")}>Back</button> | |
| 100 | + <button onClick={() => nav("/confirm")}>Next</button> | |
| 101 | + </div> | |
| 102 | + </div> | |
| 103 | + ); | |
| 104 | +} | |
| 43 | 105 | |
| 44 | − {step === "confirm" && ( | |
| 106 | +function ConfirmStep() { | |
| 107 | + const { specUrl, doAdd, onComplete } = useAddState(); | |
| 108 | + const [adding, setAdding] = useState(false); | |
| 109 | + const [error, setError] = useState<string | null>(null); | |
| 110 | + const nav = useNav(); | |
| 111 | + | |
| 112 | + const handleAdd = async () => { | |
| 113 | + setAdding(true); | |
| 114 | + setError(null); | |
| 115 | + try { | |
| 116 | + doAdd(specUrl); | |
| 117 | + onComplete(); | |
| 118 | + } catch (e) { | |
| 119 | + setError(e instanceof Error ? e.message : String(e)); | |
| 120 | + setAdding(false); | |
| 121 | + } | |
| 122 | + }; | |
| 123 | + | |
| 124 | + return ( | |
| 125 | + <div> | |
| 126 | + <p>Ready to add spec.</p> | |
| 127 | + {error && <p style={{ color: "red" }}>{error}</p>} | |
| 128 | + <div style={{ marginTop: "0.5rem", display: "flex", gap: "0.5rem" }}> | |
| 129 | + <button onClick={() => nav("/auth")} disabled={adding}>Back</button> | |
| 130 | + <button onClick={handleAdd} disabled={adding}> | |
| 131 | + {adding ? "Adding…" : "Add Source"} | |
| 132 | + </button> | |
| 133 | + </div> | |
| 134 | + </div> | |
| 135 | + ); | |
| 136 | +} | |
| 137 | + | |
| 138 | +// --------------------------------------------------------------------------- | |
| 139 | +// Component | |
| 140 | +// --------------------------------------------------------------------------- | |
| 141 | + | |
| 142 | +export default function AddOpenApiSource(props: { | |
| 143 | + onComplete: () => void; | |
| 144 | + onCancel: () => void; | |
| 145 | +}) { | |
| 146 | + const [specUrl, setSpecUrl] = useState(""); | |
| 147 | + const [preview] = useState<Result.Result<SpecPreview, unknown> | null>(null); | |
| 148 | + | |
| 149 | + const doPreviewMutation = useAtomSet(previewOpenApiSpec, { mode: "promise" }); | |
| 150 | + const doAddMutation = useAtomSet(addOpenApiSpec, { mode: "promise" }); | |
| 151 | + | |
| 152 | + const router = useMemo(() => { | |
| 153 | + const rootRoute = createRootRoute({ | |
| 154 | + component: () => ( | |
| 45 | 155 | <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> | |
| 156 | + <h3>Add OpenAPI Source</h3> | |
| 157 | + <Outlet /> | |
| 51 | 158 | </div> |
| 52 | − )} | |
| 53 | − </div> | |
| 159 | + ), | |
| 160 | + }); | |
| 161 | + | |
| 162 | + const routeTree = rootRoute.addChildren([ | |
| 163 | + createRoute({ getParentRoute: () => rootRoute, path: "/", component: UrlStep }), | |
| 164 | + createRoute({ getParentRoute: () => rootRoute, path: "/auth", component: AuthStep }), | |
| 165 | + createRoute({ getParentRoute: () => rootRoute, path: "/confirm", component: ConfirmStep }), | |
| 166 | + ]); | |
| 167 | + | |
| 168 | + return createRouter({ | |
| 169 | + routeTree, | |
| 170 | + history: createMemoryHistory({ initialEntries: ["/"] }), | |
| 171 | + }); | |
| 172 | + }, []); | |
| 173 | + | |
| 174 | + const navigate: Navigate = (to) => { | |
| 175 | + void router.navigate({ to }); | |
| 176 | + }; | |
| 177 | + | |
| 178 | + const state: AddState = { | |
| 179 | + specUrl, | |
| 180 | + setSpecUrl, | |
| 181 | + preview, | |
| 182 | + doPreview: (spec) => { | |
| 183 | + void doPreviewMutation({ path: { scopeId: "default" as never }, payload: { spec } }); | |
| 184 | + }, | |
| 185 | + doAdd: (spec, baseUrl, headers) => { | |
| 186 | + void doAddMutation({ | |
| 187 | + path: { scopeId: "default" as never }, | |
| 188 | + payload: { spec, baseUrl, headers }, | |
| 189 | + }); | |
| 190 | + }, | |
| 191 | + onComplete: props.onComplete, | |
| 192 | + onCancel: props.onCancel, | |
| 193 | + }; | |
| 194 | + | |
| 195 | + return ( | |
| 196 | + <AddStateContext.Provider value={state}> | |
| 197 | + <NavContext.Provider value={navigate}> | |
| 198 | + <RouterProvider router={router} /> | |
| 199 | + </NavContext.Provider> | |
| 200 | + </AddStateContext.Provider> | |
| 54 | 201 | ); |
| 55 | 202 | } |
| @@ -1,23 +1,5 @@ | ||
| 1 | −import { ScopeId } from "@executor/sdk"; | |
| 2 | − | |
| 3 | 1 | import { OpenApiClient } from "./client"; |
| 4 | 2 | |
| 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 | 3 | // --------------------------------------------------------------------------- |
| 22 | 4 | // Mutation atoms |
| 23 | 5 | // --------------------------------------------------------------------------- |
| @@ -28,8 +10,3 @@ export const previewOpenApiSpec = OpenApiClient.mutation( · 4 unmodified lines | ||
| 28 | 10 | ); |
| 29 | 11 | |
| 30 | 12 | export const addOpenApiSpec = OpenApiClient.mutation("openapi", "addSpec"); |
| 31 | − | |
| 32 | −export const removeOpenApiSpec = OpenApiClient.mutation( | |
| 33 | − "openapi", | |
| 34 | − "removeSpec", | |
| 35 | −); | |
| @@ -1,8 +1,6 @@ | ||
| 1 | 1 | export { openApiSourcePlugin } from "./source-plugin"; |
| 2 | 2 | export { OpenApiClient } from "./client"; |
| 3 | 3 | export { |
| 4 | − openApiSpecsAtom, | |
| 5 | 4 | previewOpenApiSpec, |
| 6 | 5 | addOpenApiSpec, |
| 7 | − removeOpenApiSpec, | |
| 8 | 6 | } from "./atoms"; |
| @@ -26,6 +26,7 @@ export interface OpenApiOperationStore { · 25 unmodified lines | ||
| 26 | 26 | |
| 27 | 27 | /** Remove all entries for a namespace */ |
| 28 | 28 | readonly removeByNamespace: (namespace: string) => Effect.Effect<readonly ToolId[]>; |
| 29 | + | |
| 29 | 30 | } |
| 30 | 31 | |
| 31 | 32 | // --------------------------------------------------------------------------- |
| @@ -73,5 +74,6 @@ export const makeInMemoryOperationStore = (): OpenApiOperationStore => { · 41 unmodified lines | ||
| 73 | 74 | } |
| 74 | 75 | return ids; |
| 75 | 76 | }), |
| 77 | + | |
| 76 | 78 | }; |
| 77 | 79 | }; |
| @@ -57,6 +57,7 @@ export interface OpenApiPluginExtension { · 56 unmodified lines | ||
| 57 | 57 | |
| 58 | 58 | /** Remove all tools from a previously added spec by namespace */ |
| 59 | 59 | readonly removeSpec: (namespace: string) => Effect.Effect<void>; |
| 60 | + | |
| 60 | 61 | } |
| 61 | 62 | |
| 62 | 63 | // --------------------------------------------------------------------------- |
| @@ -168,6 +169,7 @@ export const openApiPlugin = (options?: { · 105 unmodified lines | ||
| 168 | 169 | yield* ctx.tools.unregister(toolIds); |
| 169 | 170 | } |
| 170 | 171 | }), |
| 172 | + | |
| 171 | 173 | }, |
| 172 | 174 | |
| 173 | 175 | close: () => |