- 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 | # Finder (MacOS) folder config | 33 | # Finder (MacOS) folder config |
| 34 | .DS_Store | 34 | .DS_Store |
| 35 | personal-notes/ | 35 | personal-notes/ |
| 36 | *.har | ||
| @@ -10,6 +10,7 @@ · 9 unmodified lines | |||
| 10 | "@effect/platform": "catalog:", | 10 | "@effect/platform": "catalog:", |
| 11 | 11 | ||
| 12 | "@executor/api": "workspace:*", | 12 | "@executor/api": "workspace:*", |
| 13 | "@executor/plugin-openapi": "workspace:*", | ||
| 13 | "@executor/sdk": "workspace:*", | 14 | "@executor/sdk": "workspace:*", |
| 14 | "effect": "catalog:" | 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 | } from "@effect/platform"; | 6 | } from "@effect/platform"; |
| 7 | import { Layer } from "effect"; | 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 | import { ToolsHandlers } from "./handlers/tools"; | 11 | import { ToolsHandlers } from "./handlers/tools"; |
| 11 | import { SecretsHandlers } from "./handlers/secrets"; | 12 | import { SecretsHandlers } from "./handlers/secrets"; |
| 13 | import { OpenApiHandlersLive } from "./handlers/openapi"; | ||
| 12 | import { ExecutorServiceLive } from "./services/executor"; | 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 | // API layer — wire handlers | 23 | // API layer — wire handlers |
| 16 | // --------------------------------------------------------------------------- | 24 | // --------------------------------------------------------------------------- |
| 17 | 25 | ||
| 18 | const ApiLive = HttpApiBuilder.api(ExecutorApi).pipe( | 26 | const ApiLive = HttpApiBuilder.api(ExecutorApiWithPlugins).pipe( |
| 19 | Layer.provide([ToolsHandlers, SecretsHandlers]), | 27 | Layer.provide([ToolsHandlers, SecretsHandlers, OpenApiHandlersLive]), |
| 20 | Layer.provide(ExecutorServiceLive), | 28 | Layer.provide(ExecutorServiceLive), |
| 21 | ); | 29 | ); |
| 22 | 30 | ||
| @@ -1,7 +1,12 @@ | |||
| 1 | import { Context, Effect, Layer } from "effect"; | 1 | import { Context, Effect, Layer } from "effect"; |
| 2 | import { createExecutor, makeTestConfig } from "@executor/sdk"; | 2 | import { createExecutor, makeTestConfig } from "@executor/sdk"; |
| 3 | import { openApiPlugin, type OpenApiPluginExtension } from "@executor/plugin-openapi"; | ||
| 3 | 4 | ||
| 4 | import type { Executor } from "@executor/sdk"; | 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 | // Service tag — provides the executor instance to HTTP handlers | 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 | export class ExecutorService extends Context.Tag("ExecutorService")< | 15 | export class ExecutorService extends Context.Tag("ExecutorService")< |
| 11 | ExecutorService, | 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 | export const ExecutorServiceLive = Layer.effect( | 24 | export const ExecutorServiceLive = Layer.effect( |
| 20 | ExecutorService, | 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 | "private": true, | 4 | "private": true, |
| 5 | "type": "module", | 5 | "type": "module", |
| 6 | "dependencies": { | 6 | "dependencies": { |
| 7 | "@executor/plugin-openapi": "workspace:*", | ||
| 7 | "@executor/react": "workspace:*", | 8 | "@executor/react": "workspace:*", |
| 8 | "@executor/server": "workspace:*", | 9 | "@executor/server": "workspace:*", |
| 9 | "@tanstack/react-router": "^1.168.10", | 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 | export function SourcesPage() { | 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 | return ( | 59 | return ( |
| 3 | <div> | 60 | <div> |
| 4 | <h2>Sources</h2> | 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 | </div> | 90 | </div> |
| 7 | ); | 91 | ); |
| 8 | } | 92 | } |
| @@ -17,6 +17,7 @@ · 16 unmodified lines | |||
| 17 | "dependencies": { | 17 | "dependencies": { |
| 18 | "@effect/platform": "catalog:", | 18 | "@effect/platform": "catalog:", |
| 19 | "@executor/api": "workspace:*", | 19 | "@executor/api": "workspace:*", |
| 20 | "@executor/plugin-openapi": "workspace:*", | ||
| 20 | "@executor/sdk": "workspace:*", | 21 | "@executor/sdk": "workspace:*", |
| 21 | "effect": "catalog:", | 22 | "effect": "catalog:", |
| 22 | }, | 23 | }, |
| @@ -30,6 +31,7 @@ · 7 unmodified lines | |||
| 30 | "name": "@executor/web", | 31 | "name": "@executor/web", |
| 31 | "version": "0.0.0", | 32 | "version": "0.0.0", |
| 32 | "dependencies": { | 33 | "dependencies": { |
| 34 | "@executor/plugin-openapi": "workspace:*", | ||
| 33 | "@executor/react": "workspace:*", | 35 | "@executor/react": "workspace:*", |
| 34 | "@executor/server": "workspace:*", | 36 | "@executor/server": "workspace:*", |
| 35 | "@tanstack/react-router": "^1.168.10", | 37 | "@tanstack/react-router": "^1.168.10", |
| @@ -175,12 +177,14 @@ · 139 unmodified lines | |||
| 175 | "@effect-atom/atom-react": "^0.5.0", | 177 | "@effect-atom/atom-react": "^0.5.0", |
| 176 | "@executor/api": "workspace:*", | 178 | "@executor/api": "workspace:*", |
| 177 | "@executor/react": "workspace:*", | 179 | "@executor/react": "workspace:*", |
| 180 | "@tanstack/react-router": "^1.168.10", | ||
| 178 | "react": "^19.1.0", | 181 | "react": "^19.1.0", |
| 179 | }, | 182 | }, |
| 180 | "optionalPeers": [ | 183 | "optionalPeers": [ |
| 181 | "@effect-atom/atom-react", | 184 | "@effect-atom/atom-react", |
| 182 | "@executor/api", | 185 | "@executor/api", |
| 183 | "@executor/react", | 186 | "@executor/react", |
| 187 | "@tanstack/react-router", | ||
| 184 | "react", | 188 | "react", |
| 185 | ], | 189 | ], |
| 186 | }, | 190 | }, |
| @@ -25,12 +25,14 @@ · 24 unmodified lines | |||
| 25 | "peerDependencies": { | 25 | "peerDependencies": { |
| 26 | "react": "^19.1.0", | 26 | "react": "^19.1.0", |
| 27 | "@effect-atom/atom-react": "^0.5.0", | 27 | "@effect-atom/atom-react": "^0.5.0", |
| 28 | "@tanstack/react-router": "^1.168.10", | ||
| 28 | "@executor/api": "workspace:*", | 29 | "@executor/api": "workspace:*", |
| 29 | "@executor/react": "workspace:*" | 30 | "@executor/react": "workspace:*" |
| 30 | }, | 31 | }, |
| 31 | "peerDependenciesMeta": { | 32 | "peerDependenciesMeta": { |
| 32 | "react": { "optional": true }, | 33 | "react": { "optional": true }, |
| 33 | "@effect-atom/atom-react": { "optional": true }, | 34 | "@effect-atom/atom-react": { "optional": true }, |
| 35 | "@tanstack/react-router": { "optional": true }, | ||
| 34 | "@executor/api": { "optional": true }, | 36 | "@executor/api": { "optional": true }, |
| 35 | "@executor/react": { "optional": true } | 37 | "@executor/react": { "optional": true } |
| 36 | }, | 38 | }, |
| @@ -40,13 +40,6 @@ const AddSpecResponse = Schema.Struct({ · 39 unmodified lines | |||
| 40 | namespace: Schema.String, | 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 | // Errors with HTTP status | 44 | // Errors with HTTP status |
| 52 | // --------------------------------------------------------------------------- | 45 | // --------------------------------------------------------------------------- |
| @@ -62,8 +55,6 @@ const ExtractionError = OpenApiExtractionError.annotations( · 9 unmodified lines | |||
| 62 | // Group | 55 | // Group |
| 63 | // --------------------------------------------------------------------------- | 56 | // --------------------------------------------------------------------------- |
| 64 | 57 | ||
| 65 | const namespaceParam = HttpApiSchema.param("namespace", Schema.String); | ||
| 66 | |||
| 67 | export class OpenApiGroup extends HttpApiGroup.make("openapi") | 58 | export class OpenApiGroup extends HttpApiGroup.make("openapi") |
| 68 | .add( | 59 | .add( |
| 69 | HttpApiEndpoint.post("previewSpec")`/scopes/${scopeIdParam}/openapi/preview` | 60 | HttpApiEndpoint.post("previewSpec")`/scopes/${scopeIdParam}/openapi/preview` |
| @@ -79,12 +70,4 @@ export class OpenApiGroup extends HttpApiGroup.make("openapi") · 9 unmodified lines | |||
| 79 | .addError(ParseError) | 70 | .addError(ParseError) |
| 80 | .addError(ExtractionError), | 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 | .prefix("/v1") {} | 73 | .prefix("/v1") {} |
| @@ -49,15 +49,5 @@ export const OpenApiHandlers = HttpApiBuilder.group( · 48 unmodified lines | |||
| 49 | }; | 49 | }; |
| 50 | }).pipe(Effect.orDie), | 50 | }).pipe(Effect.orDie), |
| 51 | ) | 51 | ) |
| 52 | .handle("listSpecs", () => | 52 | , |
| 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 | ), | ||
| 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 | onComplete: () => void; | 30 | onComplete: () => void; |
| 5 | onCancel: () => void; | 31 | onCancel: () => void; |
| 6 | }) { | 32 | } |
| 7 | const [step, setStep] = useState<"url" | "auth" | "confirm">("url"); | 33 | |
| 8 | const [specUrl, setSpecUrl] = useState(""); | 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 | return ( | 50 | return ( |
| 11 | <div> | 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" && ( | 72 | function AuthStep() { |
| 15 | <div> | 73 | const { preview } = useAddState(); |
| 16 | <label> | 74 | const nav = useNav(); |
| 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 | 75 | ||
| 34 | {step === "auth" && ( | 76 | return ( |
| 35 | <div> | 77 | <div> |
| 36 | <p>Configure authentication headers…</p> | 78 | {preview && Result.match(preview, { |
| 79 | onInitial: () => <p>Analyzing spec…</p>, | ||
| 80 | onSuccess: ({ value }) => ( | ||
| 37 | <div> | 81 | <div> |
| 38 | <button onClick={() => setStep("url")}>Back</button> | 82 | <h4>{Option.getOrElse(value.title, () => "API")} {Option.getOrElse(value.version, () => "")}</h4> |
| 39 | <button onClick={() => setStep("confirm")}>Next</button> | 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 | </div> | 94 | </div> |
| 41 | </div> | 95 | ), |
| 42 | )} | 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 | <div> | 155 | <div> |
| 46 | <p>Ready to add spec from: {specUrl}</p> | 156 | <h3>Add OpenAPI Source</h3> |
| 47 | <div> | 157 | <Outlet /> |
| 48 | <button onClick={() => setStep("auth")}>Back</button> | ||
| 49 | <button onClick={props.onComplete}>Add Source</button> | ||
| 50 | </div> | ||
| 51 | </div> | 158 | </div> |
| 52 | )} | 159 | ), |
| 53 | </div> | 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 | import { OpenApiClient } from "./client"; | 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 | // Mutation atoms | 4 | // Mutation atoms |
| 23 | // --------------------------------------------------------------------------- | 5 | // --------------------------------------------------------------------------- |
| @@ -28,8 +10,3 @@ export const previewOpenApiSpec = OpenApiClient.mutation( · 4 unmodified lines | |||
| 28 | ); | 10 | ); |
| 29 | 11 | ||
| 30 | export const addOpenApiSpec = OpenApiClient.mutation("openapi", "addSpec"); | 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 | export { openApiSourcePlugin } from "./source-plugin"; | 1 | export { openApiSourcePlugin } from "./source-plugin"; |
| 2 | export { OpenApiClient } from "./client"; | 2 | export { OpenApiClient } from "./client"; |
| 3 | export { | 3 | export { |
| 4 | openApiSpecsAtom, | ||
| 5 | previewOpenApiSpec, | 4 | previewOpenApiSpec, |
| 6 | addOpenApiSpec, | 5 | addOpenApiSpec, |
| 7 | removeOpenApiSpec, | ||
| 8 | } from "./atoms"; | 6 | } from "./atoms"; |
| @@ -26,6 +26,7 @@ export interface OpenApiOperationStore { · 25 unmodified lines | |||
| 26 | 26 | ||
| 27 | /** Remove all entries for a namespace */ | 27 | /** Remove all entries for a namespace */ |
| 28 | readonly removeByNamespace: (namespace: string) => Effect.Effect<readonly ToolId[]>; | 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 | return ids; | 75 | return ids; |
| 75 | }), | 76 | }), |
| 77 | |||
| 76 | }; | 78 | }; |
| 77 | }; | 79 | }; |
| @@ -57,6 +57,7 @@ export interface OpenApiPluginExtension { · 56 unmodified lines | |||
| 57 | 57 | ||
| 58 | /** Remove all tools from a previously added spec by namespace */ | 58 | /** Remove all tools from a previously added spec by namespace */ |
| 59 | readonly removeSpec: (namespace: string) => Effect.Effect<void>; | 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 | yield* ctx.tools.unregister(toolIds); | 169 | yield* ctx.tools.unregister(toolIds); |
| 169 | } | 170 | } |
| 170 | }), | 171 | }), |
| 172 | |||
| 171 | }, | 173 | }, |
| 172 | 174 | ||
| 173 | close: () => | 175 | close: () => |