Unified · Split
| @@ -1,7 +1,6 @@ | |||
| 1 | import { useEffect, useState } from "react"; | 1 | import { useEffect, useState } from "react"; |
| 2 | import { Button } from "@executor/react/plugins"; | 2 | import { Button } from "@executor/react/plugins"; |
| 3 | import { CodeBlock } from "./code-block"; | 3 | import { CodeBlock } from "./code-block"; |
| 4 | import { cn } from "../lib/utils"; | ||
| 5 | 4 | ||
| 6 | type LocalMcpInstallMode = "stdio" | "http"; | 5 | type LocalMcpInstallMode = "stdio" | "http"; |
| 7 | 6 | ||
| @@ -34,6 +34,20 @@ const workspaceExecutorPackages = Object.keys(webPackage.dependencies ?? {}).fil · 33 unmodified lines | |||
| 34 | (name) => name.startsWith("@executor/"), | 34 | (name) => name.startsWith("@executor/"), |
| 35 | ); | 35 | ); |
| 36 | 36 | ||
| 37 | const pluginWorkspaceSegment = `${path.sep}plugins${path.sep}`; | ||
| 38 | |||
| 39 | const reloadOnPluginChange = () => ({ | ||
| 40 | name: "executor-reload-on-plugin-change", | ||
| 41 | handleHotUpdate({ file, server }: { file: string; server: { hot: { send: (payload: { type: "full-reload" }) => void } } }) { | ||
| 42 | if (!file.includes(pluginWorkspaceSegment)) { | ||
| 43 | return; | ||
| 44 | } | ||
| 45 | |||
| 46 | server.hot.send({ type: "full-reload" }); | ||
| 47 | return []; | ||
| 48 | }, | ||
| 49 | }); | ||
| 50 | |||
| 37 | export default defineConfig({ | 51 | export default defineConfig({ |
| 38 | root: "src", | 52 | root: "src", |
| 39 | resolve: { | 53 | resolve: { |
| @@ -42,6 +56,7 @@ export default defineConfig({ · 2 unmodified lines | |||
| 42 | }, | 56 | }, |
| 43 | }, | 57 | }, |
| 44 | plugins: [ | 58 | plugins: [ |
| 59 | reloadOnPluginChange(), | ||
| 45 | tailwindcss(), | 60 | tailwindcss(), |
| 46 | react(), | 61 | react(), |
| 47 | devServer({ | 62 | devServer({ |
| @@ -80,9 +80,9 @@ export function SecretReferenceField(props: SecretReferenceFieldProps) { · 79 unmodified lines | |||
| 80 | path: { | 80 | path: { |
| 81 | storeId: input.storeId, | 81 | storeId: input.storeId, |
| 82 | }, | 82 | }, |
| 83 | payload: { | 83 | payload: input.query?.trim() |
| 84 | ...(input.query?.trim() ? { query: input.query.trim() } : {}), | 84 | ? { query: input.query.trim() } |
| 85 | }, | 85 | : {}, |
| 86 | }), | 86 | }), |
| 87 | [browseSecretStoreMutation], | 87 | [browseSecretStoreMutation], |
| 88 | ); | 88 | ); |
| @@ -35,6 +35,7 @@ export { · 34 unmodified lines | |||
| 35 | export { | 35 | export { |
| 36 | defineExecutorFrontendPlugin, | 36 | defineExecutorFrontendPlugin, |
| 37 | defineFrontendPluginRoute, | 37 | defineFrontendPluginRoute, |
| 38 | liveFrontendPluginComponent, | ||
| 38 | registerExecutorFrontendPlugins, | 39 | registerExecutorFrontendPlugins, |
| 39 | type RegisteredFrontendPluginRoute, | 40 | type RegisteredFrontendPluginRoute, |
| 40 | } from "./registry"; | 41 | } from "./registry"; |
| @@ -1,3 +1,7 @@ | |||
| 1 | import { | ||
| 2 | createElement, | ||
| 3 | type ComponentType, | ||
| 4 | } from "react"; | ||
| 1 | import type { | 5 | import type { |
| 2 | ExecutorFrontendPlugin, | 6 | ExecutorFrontendPlugin, |
| 3 | FrontendPluginRouteDefinition, | 7 | FrontendPluginRouteDefinition, |
| @@ -23,6 +27,24 @@ export const defineExecutorFrontendPlugin = < · 19 unmodified lines | |||
| 23 | input: TPlugin, | 27 | input: TPlugin, |
| 24 | ): TPlugin => input; | 28 | ): TPlugin => input; |
| 25 | 29 | ||
| 30 | // Plugin objects are registered once at module load, so route components need | ||
| 31 | // to resolve lazily for React Fast Refresh to see edits inside plugin packages. | ||
| 32 | export const liveFrontendPluginComponent = < | ||
| 33 | TProps extends object, | ||
| 34 | >( | ||
| 35 | resolveComponent: () => ComponentType<TProps>, | ||
| 36 | ): ComponentType<TProps> => { | ||
| 37 | const resolvedComponent = resolveComponent(); | ||
| 38 | |||
| 39 | const LiveFrontendPluginComponent = (props: TProps) => | ||
| 40 | createElement(resolveComponent(), props); | ||
| 41 | |||
| 42 | LiveFrontendPluginComponent.displayName = | ||
| 43 | `LiveFrontendPluginComponent(${resolvedComponent.displayName ?? resolvedComponent.name ?? "Anonymous"})`; | ||
| 44 | |||
| 45 | return LiveFrontendPluginComponent; | ||
| 46 | }; | ||
| 47 | |||
| 26 | export const registerExecutorFrontendPlugins = ( | 48 | export const registerExecutorFrontendPlugins = ( |
| 27 | plugins: readonly ExecutorFrontendPlugin[], | 49 | plugins: readonly ExecutorFrontendPlugin[], |
| 28 | ) => { | 50 | ) => { |
| @@ -1,5 +1,6 @@ | |||
| 1 | import { | 1 | import { |
| 2 | defineExecutorFrontendPlugin, | 2 | defineExecutorFrontendPlugin, |
| 3 | liveFrontendPluginComponent, | ||
| 3 | } from "@executor/react/plugins"; | 4 | } from "@executor/react/plugins"; |
| 4 | 5 | ||
| 5 | import { | 6 | import { |
| @@ -14,7 +15,7 @@ export const ExecutionHistoryReactPlugin = defineExecutorFrontendPlugin({ · 8 unmodified lines | |||
| 14 | routes: [ | 15 | routes: [ |
| 15 | { | 16 | { |
| 16 | key: "history", | 17 | key: "history", |
| 17 | component: ExecutionHistoryPage, | 18 | component: liveFrontendPluginComponent(() => ExecutionHistoryPage), |
| 18 | nav: { | 19 | nav: { |
| 19 | label: "Runs", | 20 | label: "Runs", |
| 20 | section: "main", | 21 | section: "main", |
| @@ -23,7 +24,7 @@ export const ExecutionHistoryReactPlugin = defineExecutorFrontendPlugin({ · 2 unmodified lines | |||
| 23 | { | 24 | { |
| 24 | key: "detail", | 25 | key: "detail", |
| 25 | path: "$executionId", | 26 | path: "$executionId", |
| 26 | component: ExecutionHistoryDetailPage, | 27 | component: liveFrontendPluginComponent(() => ExecutionHistoryDetailPage), |
| 27 | }, | 28 | }, |
| 28 | ], | 29 | ], |
| 29 | }); | 30 | }); |
| @@ -14,7 +14,6 @@ import { · 13 unmodified lines | |||
| 14 | Alert, | 14 | Alert, |
| 15 | Badge, | 15 | Badge, |
| 16 | Button, | 16 | Button, |
| 17 | Card, | ||
| 18 | IconPencil, | 17 | IconPencil, |
| 19 | Input, | 18 | Input, |
| 20 | Label, | 19 | Label, |
| @@ -573,7 +572,7 @@ function GoogleDiscoverySourceForm(props: { · 552 unmodified lines | |||
| 573 | }; | 572 | }; |
| 574 | 573 | ||
| 575 | return ( | 574 | return ( |
| 576 | <Card className="space-y-6 p-6"> | 575 | <div className="space-y-6 rounded-lg border border-border bg-card p-6 text-sm ring-1 ring-foreground/[0.04]"> |
| 577 | <div className="grid gap-4 sm:grid-cols-2"> | 576 | <div className="grid gap-4 sm:grid-cols-2"> |
| 578 | <div className="grid gap-2 sm:col-span-2"> | 577 | <div className="grid gap-2 sm:col-span-2"> |
| 579 | <Label>Name</Label> | 578 | <Label>Name</Label> |
| @@ -753,7 +752,7 @@ function GoogleDiscoverySourceForm(props: { · 173 unmodified lines | |||
| 753 | : props.mode === "create" ? "Create Source" : "Save Changes"} | 752 | : props.mode === "create" ? "Create Source" : "Save Changes"} |
| 754 | </Button> | 753 | </Button> |
| 755 | </div> | 754 | </div> |
| 756 | </Card> | 755 | </div> |
| 757 | ); | 756 | ); |
| 758 | } | 757 | } |
| 759 | 758 | ||
| @@ -857,7 +856,7 @@ function GoogleDiscoveryBatchConnectPanel(props: { · 97 unmodified lines | |||
| 857 | }; | 856 | }; |
| 858 | 857 | ||
| 859 | return ( | 858 | return ( |
| 860 | <Card className="space-y-6 p-6"> | 859 | <div className="space-y-6 rounded-lg border border-border bg-card p-6 text-sm ring-1 ring-foreground/[0.04]"> |
| 861 | <div> | 860 | <div> |
| 862 | <h2 className="text-base font-semibold text-foreground"> | 861 | <h2 className="text-base font-semibold text-foreground"> |
| 863 | Connect multiple Google APIs | 862 | Connect multiple Google APIs |
| @@ -1008,7 +1007,7 @@ function GoogleDiscoveryBatchConnectPanel(props: { · 144 unmodified lines | |||
| 1008 | : "Connect Google APIs"} | 1007 | : "Connect Google APIs"} |
| 1009 | </Button> | 1008 | </Button> |
| 1010 | </div> | 1009 | </div> |
| 1011 | </Card> | 1010 | </div> |
| 1012 | ); | 1011 | ); |
| 1013 | } | 1012 | } |
| 1014 | 1013 | ||
| @@ -1027,51 +1026,64 @@ export function GoogleDiscoveryAddPage() { · 12 unmodified lines | |||
| 1027 | } | 1026 | } |
| 1028 | 1027 | ||
| 1029 | return ( | 1028 | return ( |
| 1030 | <div className="space-y-6"> | 1029 | <div className="min-h-0 flex-1 overflow-y-auto"> |
| 1031 | <GoogleDiscoveryBatchConnectPanel | 1030 | <div className="mx-auto w-full max-w-3xl px-6 py-10"> |
| 1032 | onConnected={(sources) => { | 1031 | <div className="mb-8"> |
| 1033 | startTransition(() => { | 1032 | <h1 className="font-display text-2xl tracking-tight text-foreground"> |
| 1034 | if (sources.length === 1) { | 1033 | Add Google API Source |
| 1035 | void navigation.detail(sources[0]!.id, { | 1034 | </h1> |
| 1036 | tab: "model", | 1035 | <p className="mt-1.5 text-sm leading-6 text-muted-foreground"> |
| 1036 | Connect Google Workspace and Cloud APIs via discovery documents. | ||
| 1037 | </p> | ||
| 1038 | </div> | ||
| 1039 | |||
| 1040 | <div className="space-y-6"> | ||
| 1041 | <GoogleDiscoveryBatchConnectPanel | ||
| 1042 | onConnected={(sources) => { | ||
| 1043 | startTransition(() => { | ||
| 1044 | if (sources.length === 1) { | ||
| 1045 | void navigation.detail(sources[0]!.id, { | ||
| 1046 | tab: "model", | ||
| 1047 | }); | ||
| 1048 | return; | ||
| 1049 | } | ||
| 1050 | |||
| 1051 | void navigation.home(); | ||
| 1037 | }); | 1052 | }); |
| 1038 | return; | 1053 | }} |
| 1039 | } | 1054 | /> |
| 1040 | 1055 | ||
| 1041 | void navigation.home(); | 1056 | <div className="flex items-center gap-3"> |
| 1042 | }); | 1057 | <Separator className="flex-1" /> |
| 1043 | }} | 1058 | <div className="text-xs font-medium text-muted-foreground"> |
| 1044 | /> | 1059 | or add a single API |
| 1060 | </div> | ||
| 1061 | <Separator className="flex-1" /> | ||
| 1062 | </div> | ||
| 1045 | 1063 | ||
| 1046 | <div className="flex items-center gap-3"> | 1064 | <GoogleDiscoverySourceForm |
| 1047 | <Separator className="flex-1" /> | 1065 | initialValue={initialValue} |
| 1048 | <div className="text-xs font-medium text-muted-foreground"> | 1066 | mode="create" |
| 1049 | or add a single API | 1067 | onSubmit={async (input) => { |
| 1068 | const source = await createSource({ | ||
| 1069 | path: { | ||
| 1070 | workspaceId: installation.data.scopeId, | ||
| 1071 | }, | ||
| 1072 | payload: input, | ||
| 1073 | reactivityKeys: { | ||
| 1074 | sources: [installation.data.scopeId], | ||
| 1075 | }, | ||
| 1076 | }); | ||
| 1077 | |||
| 1078 | startTransition(() => { | ||
| 1079 | void navigation.detail(source.id, { | ||
| 1080 | tab: "model", | ||
| 1081 | }); | ||
| 1082 | }); | ||
| 1083 | }} | ||
| 1084 | /> | ||
| 1050 | </div> | 1085 | </div> |
| 1051 | <Separator className="flex-1" /> | ||
| 1052 | </div> | 1086 | </div> |
| 1053 | |||
| 1054 | <GoogleDiscoverySourceForm | ||
| 1055 | initialValue={initialValue} | ||
| 1056 | mode="create" | ||
| 1057 | onSubmit={async (input) => { | ||
| 1058 | const source = await createSource({ | ||
| 1059 | path: { | ||
| 1060 | workspaceId: installation.data.scopeId, | ||
| 1061 | }, | ||
| 1062 | payload: input, | ||
| 1063 | reactivityKeys: { | ||
| 1064 | sources: [installation.data.scopeId], | ||
| 1065 | }, | ||
| 1066 | }); | ||
| 1067 | |||
| 1068 | startTransition(() => { | ||
| 1069 | void navigation.detail(source.id, { | ||
| 1070 | tab: "model", | ||
| 1071 | }); | ||
| 1072 | }); | ||
| 1073 | }} | ||
| 1074 | /> | ||
| 1075 | </div> | 1087 | </div> |
| 1076 | ); | 1088 | ); |
| 1077 | } | 1089 | } |
| @@ -1120,32 +1132,45 @@ export function GoogleDiscoveryEditPage(props: { · 42 unmodified lines | |||
| 1120 | } | 1132 | } |
| 1121 | 1133 | ||
| 1122 | return ( | 1134 | return ( |
| 1123 | <GoogleDiscoverySourceForm | 1135 | <div className="min-h-0 flex-1 overflow-y-auto"> |
| 1124 | initialValue={configResult.value} | 1136 | <div className="mx-auto w-full max-w-3xl px-6 py-10"> |
| 1125 | mode="edit" | 1137 | <div className="mb-8"> |
| 1126 | onSubmit={async (input) => { | 1138 | <h1 className="font-display text-2xl tracking-tight text-foreground"> |
| 1127 | const source = await updateSource({ | 1139 | Edit Google API Source |
| 1128 | path: { | 1140 | </h1> |
| 1129 | workspaceId: installation.data.scopeId, | 1141 | <p className="mt-1.5 text-sm leading-6 text-muted-foreground"> |
| 1130 | sourceId: props.source.id, | 1142 | Update the connection settings for {props.source.name}. |
| 1131 | }, | 1143 | </p> |
| 1132 | payload: input, | 1144 | </div> |
| 1133 | reactivityKeys: { | ||
| 1134 | sources: [installation.data.scopeId], | ||
| 1135 | source: [installation.data.scopeId, props.source.id], | ||
| 1136 | sourceInspection: [installation.data.scopeId, props.source.id], | ||
| 1137 | sourceInspectionTool: [installation.data.scopeId, props.source.id], | ||
| 1138 | sourceDiscovery: [installation.data.scopeId, props.source.id], | ||
| 1139 | }, | ||
| 1140 | }); | ||
| 1141 | 1145 | ||
| 1142 | startTransition(() => { | 1146 | <GoogleDiscoverySourceForm |
| 1143 | void navigation.detail(source.id, { | 1147 | initialValue={configResult.value} |
| 1144 | tab: "model", | 1148 | mode="edit" |
| 1145 | }); | 1149 | onSubmit={async (input) => { |
| 1146 | }); | 1150 | const source = await updateSource({ |
| 1147 | }} | 1151 | path: { |
| 1148 | /> | 1152 | workspaceId: installation.data.scopeId, |
| 1153 | sourceId: props.source.id, | ||
| 1154 | }, | ||
| 1155 | payload: input, | ||
| 1156 | reactivityKeys: { | ||
| 1157 | sources: [installation.data.scopeId], | ||
| 1158 | source: [installation.data.scopeId, props.source.id], | ||
| 1159 | sourceInspection: [installation.data.scopeId, props.source.id], | ||
| 1160 | sourceInspectionTool: [installation.data.scopeId, props.source.id], | ||
| 1161 | sourceDiscovery: [installation.data.scopeId, props.source.id], | ||
| 1162 | }, | ||
| 1163 | }); | ||
| 1164 | |||
| 1165 | startTransition(() => { | ||
| 1166 | void navigation.detail(source.id, { | ||
| 1167 | tab: "model", | ||
| 1168 | }); | ||
| 1169 | }); | ||
| 1170 | }} | ||
| 1171 | /> | ||
| 1172 | </div> | ||
| 1173 | </div> | ||
| 1149 | ); | 1174 | ); |
| 1150 | } | 1175 | } |
| 1151 | 1176 | ||
| @@ -1,4 +1,7 @@ | |||
| 1 | import { defineExecutorFrontendPlugin } from "@executor/react/plugins"; | 1 | import { |
| 2 | defineExecutorFrontendPlugin, | ||
| 3 | liveFrontendPluginComponent, | ||
| 4 | } from "@executor/react/plugins"; | ||
| 2 | 5 | ||
| 3 | import { | 6 | import { |
| 4 | GOOGLE_DISCOVERY_PLUGIN_KEY, | 7 | GOOGLE_DISCOVERY_PLUGIN_KEY, |
| @@ -22,17 +25,17 @@ export const GoogleDiscoveryReactPlugin = defineExecutorFrontendPlugin({ · 17 unmodified lines | |||
| 22 | { | 25 | { |
| 23 | key: "add", | 26 | key: "add", |
| 24 | path: "add", | 27 | path: "add", |
| 25 | component: GoogleDiscoveryAddPage, | 28 | component: liveFrontendPluginComponent(() => GoogleDiscoveryAddPage), |
| 26 | }, | 29 | }, |
| 27 | { | 30 | { |
| 28 | key: "detail", | 31 | key: "detail", |
| 29 | path: "sources/$sourceId", | 32 | path: "sources/$sourceId", |
| 30 | component: GoogleDiscoveryDetailRoute, | 33 | component: liveFrontendPluginComponent(() => GoogleDiscoveryDetailRoute), |
| 31 | }, | 34 | }, |
| 32 | { | 35 | { |
| 33 | key: "edit", | 36 | key: "edit", |
| 34 | path: "sources/$sourceId/edit", | 37 | path: "sources/$sourceId/edit", |
| 35 | component: GoogleDiscoveryEditRoute, | 38 | component: liveFrontendPluginComponent(() => GoogleDiscoveryEditRoute), |
| 36 | }, | 39 | }, |
| 37 | ], | 40 | ], |
| 38 | }); | 41 | }); |
| @@ -214,7 +214,7 @@ function GraphqlSourceForm(props: { · 213 unmodified lines | |||
| 214 | const [error, setError] = useState<string | null>(null); | 214 | const [error, setError] = useState<string | null>(null); |
| 215 | 215 | ||
| 216 | return ( | 216 | return ( |
| 217 | <Card className="space-y-6 p-6"> | 217 | <div className="space-y-6 rounded-lg border border-border bg-card p-6 text-sm ring-1 ring-foreground/[0.04]"> |
| 218 | <div className="grid gap-4"> | 218 | <div className="grid gap-4"> |
| 219 | <div className="grid gap-2"> | 219 | <div className="grid gap-2"> |
| 220 | <Label>Name</Label> | 220 | <Label>Name</Label> |
| @@ -327,7 +327,7 @@ function GraphqlSourceForm(props: { · 106 unmodified lines | |||
| 327 | : props.mode === "create" ? "Create Source" : "Save Changes"} | 327 | : props.mode === "create" ? "Create Source" : "Save Changes"} |
| 328 | </Button> | 328 | </Button> |
| 329 | </div> | 329 | </div> |
| 330 | </Card> | 330 | </div> |
| 331 | ); | 331 | ); |
| 332 | } | 332 | } |
| 333 | 333 | ||
| @@ -341,32 +341,46 @@ export function GraphqlAddPage() { · 7 unmodified lines | |||
| 341 | { mode: "promise" }, | 341 | { mode: "promise" }, |
| 342 | ); | 342 | ); |
| 343 | 343 | ||
| 344 | |||
| 344 | if (installation.status !== "ready") { | 345 | if (installation.status !== "ready") { |
| 345 | return <div className="text-sm text-muted-foreground">Loading workspace...</div>; | 346 | return <div className="text-sm text-muted-foreground">Loading workspace...</div>; |
| 346 | } | 347 | } |
| 347 | 348 | ||
| 348 | return ( | 349 | return ( |
| 349 | <GraphqlSourceForm | 350 | <div className="min-h-0 flex-1 overflow-y-auto"> |
| 350 | initialValue={initialValue} | 351 | <div className="mx-auto w-full max-w-3xl px-6 py-10"> |
| 351 | mode="create" | 352 | <div className="mb-8"> |
| 352 | onSubmit={async (input) => { | 353 | <h1 className="font-display text-2xl tracking-tight text-foreground"> |
| 353 | const source = await createSource({ | 354 | Add GraphQL Source |
| 354 | path: { | 355 | </h1> |
| 355 | workspaceId: installation.data.scopeId, | 356 | <p className="mt-1.5 text-sm leading-6 text-muted-foreground"> |
| 356 | }, | 357 | Connect a GraphQL endpoint and expose its operations as tools. |
| 357 | payload: input, | 358 | </p> |
| 358 | reactivityKeys: { | 359 | </div> |
| 359 | sources: [installation.data.scopeId], | 360 | |
| 360 | }, | 361 | <GraphqlSourceForm |
| 361 | }); | 362 | initialValue={initialValue} |
| 362 | 363 | mode="create" | |
| 363 | startTransition(() => { | 364 | onSubmit={async (input) => { |
| 364 | void navigation.detail(source.id, { | 365 | const source = await createSource({ |
| 365 | tab: "model", | 366 | path: { |
| 366 | }); | 367 | workspaceId: installation.data.scopeId, |
| 367 | }); | 368 | }, |
| 368 | }} | 369 | payload: input, |
| 369 | /> | 370 | reactivityKeys: { |
| 371 | sources: [installation.data.scopeId], | ||
| 372 | }, | ||
| 373 | }); | ||
| 374 | |||
| 375 | startTransition(() => { | ||
| 376 | void navigation.detail(source.id, { | ||
| 377 | tab: "model", | ||
| 378 | }); | ||
| 379 | }); | ||
| 380 | }} | ||
| 381 | /> | ||
| 382 | </div> | ||
| 383 | </div> | ||
| 370 | ); | 384 | ); |
| 371 | } | 385 | } |
| 372 | 386 | ||
| @@ -414,32 +428,45 @@ export function GraphqlEditPage(props: { · 41 unmodified lines | |||
| 414 | } | 428 | } |
| 415 | 429 | ||
| 416 | return ( | 430 | return ( |
| 417 | <GraphqlSourceForm | 431 | <div className="min-h-0 flex-1 overflow-y-auto"> |
| 418 | initialValue={configResult.value} | 432 | <div className="mx-auto w-full max-w-3xl px-6 py-10"> |
| 419 | mode="edit" | 433 | <div className="mb-8"> |
| 420 | onSubmit={async (input) => { | 434 | <h1 className="font-display text-2xl tracking-tight text-foreground"> |
| 421 | const source = await updateSource({ | 435 | Edit GraphQL Source |
| 422 | path: { | 436 | </h1> |
| 423 | workspaceId: installation.data.scopeId, | 437 | <p className="mt-1.5 text-sm leading-6 text-muted-foreground"> |
| 424 | sourceId: props.source.id, | 438 | Update the connection settings for {props.source.name}. |
| 425 | }, | 439 | </p> |
| 426 | payload: input, | 440 | </div> |
| 427 | reactivityKeys: { | 441 | |
| 428 | sources: [installation.data.scopeId], | 442 | <GraphqlSourceForm |
| 429 | source: [installation.data.scopeId, props.source.id], | 443 | initialValue={configResult.value} |
| 430 | sourceInspection: [installation.data.scopeId, props.source.id], | 444 | mode="edit" |
| 431 | sourceInspectionTool: [installation.data.scopeId, props.source.id], | 445 | onSubmit={async (input) => { |
| 432 | sourceDiscovery: [installation.data.scopeId, props.source.id], | 446 | const source = await updateSource({ |
| 433 | }, | 447 | path: { |
| 434 | }); | 448 | workspaceId: installation.data.scopeId, |
| 435 | 449 | sourceId: props.source.id, | |
| 436 | startTransition(() => { | 450 | }, |
| 437 | void navigation.detail(source.id, { | 451 | payload: input, |
| 438 | tab: "model", | 452 | reactivityKeys: { |
| 439 | }); | 453 | sources: [installation.data.scopeId], |
| 440 | }); | 454 | source: [installation.data.scopeId, props.source.id], |
| 441 | }} | 455 | sourceInspection: [installation.data.scopeId, props.source.id], |
| 442 | /> | 456 | sourceInspectionTool: [installation.data.scopeId, props.source.id], |
| 457 | sourceDiscovery: [installation.data.scopeId, props.source.id], | ||
| 458 | }, | ||
| 459 | }); | ||
| 460 | |||
| 461 | startTransition(() => { | ||
| 462 | void navigation.detail(source.id, { | ||
| 463 | tab: "model", | ||
| 464 | }); | ||
| 465 | }); | ||
| 466 | }} | ||
| 467 | /> | ||
| 468 | </div> | ||
| 469 | </div> | ||
| 443 | ); | 470 | ); |
| 444 | } | 471 | } |
| 445 | 472 | ||
| @@ -542,6 +569,91 @@ function GraphqlDetailExplorer(props: { · 96 unmodified lines | |||
| 542 | }; | 569 | }; |
| 543 | 570 | ||
| 544 | const config = Result.isSuccess(configResult) ? configResult.value : null; | 571 | const config = Result.isSuccess(configResult) ? configResult.value : null; |
| 572 | const sourceNeedsRecovery = | ||
| 573 | props.source.status === "auth_required" || props.source.status === "error"; | ||
| 574 | |||
| 575 | if (sourceNeedsRecovery) { | ||
| 576 | const title = | ||
| 577 | props.source.status === "auth_required" | ||
| 578 | ? "Credentials required" | ||
| 579 | : "Source needs attention"; | ||
| 580 | const description = | ||
| 581 | props.source.status === "auth_required" | ||
| 582 | ? "This endpoint requires credentials before its schema can be introspected. Edit the source, configure bearer auth, and save again." | ||
| 583 | : "This source could not be indexed successfully. Edit the configuration and save again to rebuild its catalog."; | ||
| 584 | |||
| 585 | return ( | ||
| 586 | <div className="min-h-0 flex-1 overflow-y-auto"> | ||
| 587 | <div className="mx-auto w-full max-w-3xl px-6 py-10"> | ||
| 588 | <Card className="space-y-6 p-6"> | ||
| 589 | <div className="space-y-2"> | ||
| 590 | <div className="flex items-center gap-2"> | ||
| 591 | <h1 className="font-display text-2xl tracking-tight text-foreground"> | ||
| 592 | {title} | ||
| 593 | </h1> | ||
| 594 | <Badge variant="outline">{props.source.status}</Badge> | ||
| 595 | </div> | ||
| 596 | <p className="text-sm leading-6 text-muted-foreground"> | ||
| 597 | {description} | ||
| 598 | </p> | ||
| 599 | </div> | ||
| 600 | |||
| 601 | {config ? ( | ||
| 602 | <div className="flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground"> | ||
| 603 | <span className="font-mono text-foreground">{config.endpoint}</span> | ||
| 604 | <Badge variant="muted">Auth: {config.auth.kind}</Badge> | ||
| 605 | </div> | ||
| 606 | ) : null} | ||
| 607 | |||
| 608 | <div className="flex flex-wrap items-center gap-2"> | ||
| 609 | <Button | ||
| 610 | variant="outline" | ||
| 611 | size="sm" | ||
| 612 | onClick={() => void navigation.edit(props.source.id)} | ||
| 613 | > | ||
| 614 | <IconPencil className="size-3" /> | ||
| 615 | Edit | ||
| 616 | </Button> | ||
| 617 | {confirmDelete ? ( | ||
| 618 | <> | ||
| 619 | <span className="text-[11px] font-medium text-destructive"> | ||
| 620 | Confirm delete? | ||
| 621 | </span> | ||
| 622 | <Button | ||
| 623 | variant="outline" | ||
| 624 | size="sm" | ||
| 625 | onClick={() => setConfirmDelete(false)} | ||
| 626 | disabled={isDeleting} | ||
| 627 | > | ||
| 628 | Cancel | ||
| 629 | </Button> | ||
| 630 | <Button | ||
| 631 | variant="destructive-outline" | ||
| 632 | size="sm" | ||
| 633 | onClick={() => { | ||
| 634 | void handleDelete().catch(() => {}); | ||
| 635 | }} | ||
| 636 | disabled={isDeleting} | ||
| 637 | > | ||
| 638 | {isDeleting ? "Deleting..." : "Delete"} | ||
| 639 | </Button> | ||
| 640 | </> | ||
| 641 | ) : ( | ||
| 642 | <Button | ||
| 643 | variant="destructive-outline" | ||
| 644 | size="sm" | ||
| 645 | onClick={() => setConfirmDelete(true)} | ||
| 646 | disabled={isDeleting} | ||
| 647 | > | ||
| 648 | Delete | ||
| 649 | </Button> | ||
| 650 | )} | ||
| 651 | </div> | ||
| 652 | </Card> | ||
| 653 | </div> | ||
| 654 | </div> | ||
| 655 | ); | ||
| 656 | } | ||
| 545 | 657 | ||
| 546 | return ( | 658 | return ( |
| 547 | <SourceToolExplorer | 659 | <SourceToolExplorer |
| @@ -661,7 +773,7 @@ function GraphqlSourceRoute(props: { · 113 unmodified lines | |||
| 661 | const sourceId = typeof params.sourceId === "string" ? params.sourceId : null; | 773 | const sourceId = typeof params.sourceId === "string" ? params.sourceId : null; |
| 662 | const source = useSource(sourceId ?? ""); | 774 | const source = useSource(sourceId ?? ""); |
| 663 | 775 | ||
| 664 | if (sourceId === null || source.status === "error") { | 776 | if (sourceId === null) { |
| 665 | return ( | 777 | return ( |
| 666 | <div className="px-6 py-8 text-sm text-destructive"> | 778 | <div className="px-6 py-8 text-sm text-destructive"> |
| 667 | This GraphQL source is unavailable. | 779 | This GraphQL source is unavailable. |
| @@ -677,6 +789,14 @@ function GraphqlSourceRoute(props: { · 9 unmodified lines | |||
| 677 | ); | 789 | ); |
| 678 | } | 790 | } |
| 679 | 791 | ||
| 792 | if (source.status === "error") { | ||
| 793 | return ( | ||
| 794 | <div className="px-6 py-8 text-sm text-destructive"> | ||
| 795 | Failed loading this GraphQL source. | ||
| 796 | </div> | ||
| 797 | ); | ||
| 798 | } | ||
| 799 | |||
| 680 | if (source.data.kind !== "graphql") { | 800 | if (source.data.kind !== "graphql") { |
| 681 | return ( | 801 | return ( |
| 682 | <div className="px-6 py-8 text-sm text-destructive"> | 802 | <div className="px-6 py-8 text-sm text-destructive"> |
| @@ -1,4 +1,7 @@ | |||
| 1 | import { defineExecutorFrontendPlugin } from "@executor/react/plugins"; | 1 | import { |
| 2 | defineExecutorFrontendPlugin, | ||
| 3 | liveFrontendPluginComponent, | ||
| 4 | } from "@executor/react/plugins"; | ||
| 2 | 5 | ||
| 3 | import { | 6 | import { |
| 4 | GraphqlAddPage, | 7 | GraphqlAddPage, |
| @@ -15,22 +18,22 @@ export const GraphqlReactPlugin = defineExecutorFrontendPlugin({ · 10 unmodified lines | |||
| 15 | { | 18 | { |
| 16 | key: "add", | 19 | key: "add", |
| 17 | path: "add", | 20 | path: "add", |
| 18 | component: GraphqlAddPage, | 21 | component: liveFrontendPluginComponent(() => GraphqlAddPage), |
| 19 | }, | 22 | }, |
| 20 | { | 23 | { |
| 21 | key: "detail", | 24 | key: "detail", |
| 22 | path: "sources/$sourceId", | 25 | path: "sources/$sourceId", |
| 23 | component: GraphqlDetailRoute, | 26 | component: liveFrontendPluginComponent(() => GraphqlDetailRoute), |
| 24 | }, | 27 | }, |
| 25 | { | 28 | { |
| 26 | key: "edit", | 29 | key: "edit", |
| 27 | path: "sources/$sourceId/edit", | 30 | path: "sources/$sourceId/edit", |
| 28 | component: GraphqlEditRoute, | 31 | component: liveFrontendPluginComponent(() => GraphqlEditRoute), |
| 29 | }, | 32 | }, |
| 30 | { | 33 | { |
| 31 | key: "tool-detail", | 34 | key: "tool-detail", |
| 32 | path: "sources/$sourceId/tool/$toolPath", | 35 | path: "sources/$sourceId/tool/$toolPath", |
| 33 | component: GraphqlToolDetailRoute, | 36 | component: liveFrontendPluginComponent(() => GraphqlToolDetailRoute), |
| 34 | }, | 37 | }, |
| 35 | ], | 38 | ], |
| 36 | }); | 39 | }); |
| @@ -9,7 +9,6 @@ import { · 8 unmodified lines | |||
| 9 | import { | 9 | import { |
| 10 | Alert, | 10 | Alert, |
| 11 | Button, | 11 | Button, |
| 12 | Card, | ||
| 13 | SourceToolExplorer, | 12 | SourceToolExplorer, |
| 14 | parseSourceToolExplorerSearch, | 13 | parseSourceToolExplorerSearch, |
| 15 | useSourcePluginNavigation, | 14 | useSourcePluginNavigation, |
| @@ -118,52 +117,54 @@ export function LocalToolsAddPage() { · 102 unmodified lines | |||
| 118 | ) ?? null; | 117 | ) ?? null; |
| 119 | 118 | ||
| 120 | return ( | 119 | return ( |
| 121 | <div className="mx-auto w-full max-w-3xl px-6 py-10"> | 120 | <div className="min-h-0 flex-1 overflow-y-auto"> |
| 122 | <Card className="p-6"> | 121 | <div className="mx-auto w-full max-w-3xl px-6 py-10"> |
| 123 | <h1 className="font-display text-2xl tracking-tight text-foreground"> | 122 | <div className="mb-8"> |
| 124 | Local Tools | 123 | <h1 className="font-display text-2xl tracking-tight text-foreground"> |
| 125 | </h1> | 124 | Local Tools |
| 126 | <p className="mt-2 text-sm leading-6 text-muted-foreground"> | 125 | </h1> |
| 127 | Promote file-backed tools in <code className="rounded bg-muted px-1 py-0.5 font-mono">.executor/tools</code> | 126 | <p className="mt-1.5 text-sm leading-6 text-muted-foreground"> |
| 128 | {" "}into a first-class source with inspection and discovery. | 127 | File-backed tools from <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">.executor/tools</code> promoted into a first-class source. |
| 129 | </p> | ||
| 130 | |||
| 131 | <Card className="mt-6 bg-background/60 p-4 text-sm text-muted-foreground"> | ||
| 132 | <p> | ||
| 133 | The source is provisioned automatically when the executor session starts and finds | ||
| 134 | local tool files in <code className="rounded bg-muted px-1 py-0.5 font-mono">.executor/tools</code>. | ||
| 135 | </p> | 128 | </p> |
| 136 | <p className="mt-3"> | 129 | </div> |
| 137 | Existing tool path behavior is preserved, so a file like | 130 | |
| 138 | {" "}<code className="rounded bg-muted px-1 py-0.5 font-mono">.executor/tools/demo.ts</code> | 131 | <div className="space-y-6 rounded-lg border border-border bg-card p-6 text-sm ring-1 ring-foreground/[0.04]"> |
| 139 | {" "}still appears as <code className="rounded bg-muted px-1 py-0.5 font-mono">tools.demo(...)</code>. | 132 | <div className="rounded-lg border border-border bg-muted/20 p-4 text-sm leading-6 text-muted-foreground"> |
| 140 | </p> | 133 | <p> |
| 141 | </Card> | 134 | The source is provisioned automatically when the executor session starts and finds |
| 142 | 135 | local tool files in <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">.executor/tools</code>. | |
| 143 | <div className="mt-6 flex flex-wrap items-center gap-3"> | 136 | </p> |
| 144 | {localToolsSource ? ( | 137 | <p className="mt-3"> |
| 145 | <Button | 138 | Existing tool path behavior is preserved, so a file like |
| 146 | size="lg" | 139 | {" "}<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">.executor/tools/demo.ts</code> |
| 147 | type="button" | 140 | {" "}still appears as <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">tools.demo(...)</code>. |
| 148 | onClick={() => { | 141 | </p> |
| 149 | startTransition(() => { | 142 | </div> |
| 150 | void navigation.detail(localToolsSource.id, { | 143 | |
| 151 | tab: "model", | 144 | <div className="flex flex-wrap items-center gap-3"> |
| 145 | {localToolsSource ? ( | ||
| 146 | <Button | ||
| 147 | type="button" | ||
| 148 | onClick={() => { | ||
| 149 | startTransition(() => { | ||
| 150 | void navigation.detail(localToolsSource.id, { | ||
| 151 | tab: "model", | ||
| 152 | }); | ||
| 152 | }); | 153 | }); |
| 153 | }); | 154 | }} |
| 154 | }} | 155 | > |
| 155 | > | 156 | Open Local Tools Source |
| 156 | Open Local Tools Source | 157 | </Button> |
| 157 | </Button> | 158 | ) : ( |
| 158 | ) : ( | 159 | <Alert variant="warning"> |
| 159 | <Alert variant="warning"> | 160 | No local tools source is active yet. Create a tool in |
| 160 | No local tools source is active yet. Create a tool in | 161 | {" "}<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">.executor/tools</code> |
| 161 | {" "}<code className="rounded bg-muted px-1 py-0.5 font-mono">.executor/tools</code> | 162 | {" "}and restart the executor session. |
| 162 | {" "}and restart the executor session. | 163 | </Alert> |
| 163 | </Alert> | 164 | )} |
| 164 | )} | 165 | </div> |
| 165 | </div> | 166 | </div> |
| 166 | </Card> | 167 | </div> |
| 167 | </div> | 168 | </div> |
| 168 | ); | 169 | ); |
| 169 | } | 170 | } |
| @@ -1,4 +1,7 @@ | |||
| 1 | import { defineExecutorFrontendPlugin } from "@executor/react/plugins"; | 1 | import { |
| 2 | defineExecutorFrontendPlugin, | ||
| 3 | liveFrontendPluginComponent, | ||
| 4 | } from "@executor/react/plugins"; | ||
| 2 | 5 | ||
| 3 | import { | 6 | import { |
| 4 | LocalToolsAddPage, | 7 | LocalToolsAddPage, |
| @@ -13,12 +16,12 @@ export const LocalToolsReactPlugin = defineExecutorFrontendPlugin({ · 8 unmodified lines | |||
| 13 | { | 16 | { |
| 14 | key: "add", | 17 | key: "add", |
| 15 | path: "add", | 18 | path: "add", |
| 16 | component: LocalToolsAddPage, | 19 | component: liveFrontendPluginComponent(() => LocalToolsAddPage), |
| 17 | }, | 20 | }, |
| 18 | { | 21 | { |
| 19 | key: "detail", | 22 | key: "detail", |
| 20 | path: "sources/$sourceId", | 23 | path: "sources/$sourceId", |
| 21 | component: LocalToolsDetailRoute, | 24 | component: liveFrontendPluginComponent(() => LocalToolsDetailRoute), |
| 22 | }, | 25 | }, |
| 23 | ], | 26 | ], |
| 24 | }); | 27 | }); |
| @@ -13,12 +13,7 @@ import { · 12 unmodified lines | |||
| 13 | Alert, | 13 | Alert, |
| 14 | Badge, | 14 | Badge, |
| 15 | Button, | 15 | Button, |
| 16 | Card, | ||
| 17 | cn, | ||
| 18 | IconCheck, | ||
| 19 | IconPencil, | 16 | IconPencil, |
| 20 | IconSearch, | ||
| 21 | IconSpinner, | ||
| 22 | Input, | 17 | Input, |
| 23 | Label, | 18 | Label, |
| 24 | Select, | 19 | Select, |
| @@ -36,8 +31,6 @@ import { · 11 unmodified lines | |||
| 36 | } from "@executor/plugin-mcp-http"; | 31 | } from "@executor/plugin-mcp-http"; |
| 37 | import { | 32 | import { |
| 38 | type McpConnectInput, | 33 | type McpConnectInput, |
| 39 | type McpDiscoverInput, | ||
| 40 | type McpDiscoverResult, | ||
| 41 | type McpConnectionAuth, | 34 | type McpConnectionAuth, |
| 42 | type McpOAuthPopupResult, | 35 | type McpOAuthPopupResult, |
| 43 | type McpStartOAuthInput, | 36 | type McpStartOAuthInput, |
| @@ -78,82 +71,6 @@ const defaultMcpInput = (): McpConnectInput => ({ · 34 unmodified lines | |||
| 78 | }, | 71 | }, |
| 79 | }); | 72 | }); |
| 80 | 73 | ||
| 81 | type McpQuickPreset = { | ||
| 82 | id: string; | ||
| 83 | name: string; | ||
| 84 | summary: string; | ||
| 85 | input: McpConnectInput; | ||
| 86 | }; | ||
| 87 | |||
| 88 | type ProbeAuthState = { | ||
| 89 | kind: NonNullable<McpDiscoverInput["probeAuth"]>["kind"]; | ||
| 90 | token: string; | ||
| 91 | headerName: string; | ||
| 92 | prefix: string; | ||
| 93 | username: string; | ||
| 94 | password: string; | ||
| 95 | headersText: string; | ||
| 96 | }; | ||
| 97 | |||
| 98 | const defaultProbeAuthState = (): ProbeAuthState => ({ | ||
| 99 | kind: "none", | ||
| 100 | token: "", | ||
| 101 | headerName: "Authorization", | ||
| 102 | prefix: "Bearer ", | ||
| 103 | username: "", | ||
| 104 | password: "", | ||
| 105 | headersText: "", | ||
| 106 | }); | ||
| 107 | |||
| 108 | const mcpQuickPresets: ReadonlyArray<McpQuickPreset> = [ | ||
| 109 | { | ||
| 110 | id: "deepwiki-mcp", | ||
| 111 | name: "DeepWiki MCP", | ||
| 112 | summary: "Repository docs and knowledge graphs via a remote MCP endpoint.", | ||
| 113 | input: { | ||
| 114 | ...defaultMcpInput(), | ||
| 115 | name: "DeepWiki MCP", | ||
| 116 | endpoint: "https://mcp.deepwiki.com/mcp", | ||
| 117 | transport: "auto", | ||
| 118 | }, | ||
| 119 | }, | ||
| 120 | { | ||
| 121 | id: "axiom-mcp", | ||
| 122 | name: "Axiom MCP", | ||
| 123 | summary: "Query and analyze logs and traces through Axiom's MCP server.", | ||
| 124 | input: { | ||
| 125 | ...defaultMcpInput(), | ||
| 126 | name: "Axiom MCP", | ||
| 127 | endpoint: "https://mcp.axiom.co/mcp", | ||
| 128 | transport: "auto", | ||
| 129 | }, | ||
| 130 | }, | ||
| 131 | { | ||
| 132 | id: "neon-mcp", | ||
| 133 | name: "Neon MCP", | ||
| 134 | summary: "Manage databases, branches, and queries via Neon MCP.", | ||
| 135 | input: { | ||
| 136 | ...defaultMcpInput(), | ||
| 137 | name: "Neon MCP", | ||
| 138 | endpoint: "https://mcp.neon.tech/mcp", | ||
| 139 | transport: "auto", | ||
| 140 | }, | ||
| 141 | }, | ||
| 142 | { | ||
| 143 | id: "chrome-devtools-mcp", | ||
| 144 | name: "Chrome DevTools MCP", | ||
| 145 | summary: "Launch the local Chrome DevTools MCP server over stdio.", | ||
| 146 | input: { | ||
| 147 | ...defaultMcpInput(), | ||
| 148 | name: "Chrome DevTools MCP", | ||
| 149 | endpoint: null, | ||
| 150 | transport: "stdio", | ||
| 151 | command: "npx", | ||
| 152 | args: ["-y", "chrome-devtools-mcp@latest"], | ||
| 153 | }, | ||
| 154 | }, | ||
| 155 | ]; | ||
| 156 | |||
| 157 | const presetString = ( | 74 | const presetString = ( |
| 158 | search: Record<string, unknown>, | 75 | search: Record<string, unknown>, |
| 159 | key: string, | 76 | key: string, |
| @@ -250,49 +167,6 @@ const transportFieldsFromInput = (input: McpConnectInput): McpTransportFields => · 90 unmodified lines | |||
| 250 | headersText: stringifyStringMap(input.headers), | 167 | headersText: stringifyStringMap(input.headers), |
| 251 | }; | 168 | }; |
| 252 | 169 | ||
| 253 | const buildProbeAuth = ( | ||
| 254 | state: ProbeAuthState, | ||
| 255 | ): McpDiscoverInput["probeAuth"] => { | ||
| 256 | if (state.kind === "none") { | ||
| 257 | return { kind: "none" }; | ||
| 258 | } | ||
| 259 | |||
| 260 | if (state.kind === "bearer") { | ||
| 261 | if (!state.token.trim()) { | ||
| 262 | throw new Error("Token is required for bearer discovery auth."); | ||
| 263 | } | ||
| 264 | |||
| 265 | return { | ||
| 266 | kind: "bearer", | ||
| 267 | headerName: state.headerName.trim() || null, | ||
| 268 | prefix: state.prefix.length > 0 ? state.prefix : null, | ||
| 269 | token: state.token.trim(), | ||
| 270 | }; | ||
| 271 | } | ||
| 272 | |||
| 273 | if (state.kind === "basic") { | ||
| 274 | if (!state.username.trim()) { | ||
| 275 | throw new Error("Username is required for basic discovery auth."); | ||
| 276 | } | ||
| 277 | |||
| 278 | return { | ||
| 279 | kind: "basic", | ||
| 280 | username: state.username.trim(), | ||
| 281 | password: state.password, | ||
| 282 | }; | ||
| 283 | } | ||
| 284 | |||
| 285 | const headers = parseJsonStringMap("Discovery headers", state.headersText); | ||
| 286 | if (!headers) { | ||
| 287 | throw new Error("At least one discovery header is required."); | ||
| 288 | } | ||
| 289 | |||
| 290 | return { | ||
| 291 | kind: "headers", | ||
| 292 | headers, | ||
| 293 | }; | ||
| 294 | }; | ||
| 295 | |||
| 296 | const waitForOauthPopupResult = async ( | 170 | const waitForOauthPopupResult = async ( |
| 297 | sessionId: string, | 171 | sessionId: string, |
| 298 | ): Promise<McpOAuthPopupResult> => | 172 | ): Promise<McpOAuthPopupResult> => |
| @@ -387,10 +261,6 @@ function McpSourceForm(props: { · 88 unmodified lines | |||
| 387 | }) { | 261 | }) { |
| 388 | const installation = useLocalInstallation(); | 262 | const installation = useLocalInstallation(); |
| 389 | const client = getMcpHttpClient(); | 263 | const client = getMcpHttpClient(); |
| 390 | const discoverSource = useAtomSet( | ||
| 391 | client.mutation("mcp", "discoverSource"), | ||
| 392 | { mode: "promise" }, | ||
| 393 | ); | ||
| 394 | const startOAuth = useAtomSet( | 264 | const startOAuth = useAtomSet( |
| 395 | client.mutation("mcp", "startOAuth"), | 265 | client.mutation("mcp", "startOAuth"), |
| 396 | { mode: "promise" }, | 266 | { mode: "promise" }, |
| @@ -415,104 +285,9 @@ function McpSourceForm(props: { · 18 unmodified lines | |||
| 415 | props.initialValue.auth.kind === "oauth2" ? "connected" : "idle", | 285 | props.initialValue.auth.kind === "oauth2" ? "connected" : "idle", |
| 416 | ); | 286 | ); |
| 417 | const [error, setError] = useState<string | null>(null); | 287 | const [error, setError] = useState<string | null>(null); |
| 418 | const [discoveryEndpoint, setDiscoveryEndpoint] = useState( | ||
| 419 | props.initialValue.command ? "" : (props.initialValue.endpoint ?? ""), | ||
| 420 | ); | ||
| 421 | const [showProbeAuth, setShowProbeAuth] = useState(false); | ||
| 422 | const [probeAuth, setProbeAuth] = useState<ProbeAuthState>(defaultProbeAuthState); | ||
| 423 | const [discoveryMessage, setDiscoveryMessage] = useState<string | null>(null); | ||
| 424 | const discoverMutation = useExecutorMutation<McpDiscoverInput, McpDiscoverResult>( | ||
| 425 | async (input) => { | ||
| 426 | if (installation.status !== "ready") { | ||
| 427 | throw new Error("Workspace is still loading."); | ||
| 428 | } | ||
| 429 | |||
| 430 | return discoverSource({ | ||
| 431 | path: { | ||
| 432 | workspaceId: installation.data.scopeId, | ||
| 433 | }, | ||
| 434 | payload: input, | ||
| 435 | }); | ||
| 436 | }, | ||
| 437 | ); | ||
| 438 | 288 | ||
| 439 | const isStdio = transportFields.transport === "stdio"; | 289 | const isStdio = transportFields.transport === "stdio"; |
| 440 | 290 | ||
| 441 | const applyPreset = (preset: McpQuickPreset) => { | ||
| 442 | setName(preset.input.name); | ||
| 443 | setEndpoint(preset.input.endpoint ?? ""); | ||
| 444 | setTransportFields(transportFieldsFromInput(preset.input)); | ||
| 445 | setAuthKind(preset.input.auth.kind); | ||
| 446 | setOauthAuth(preset.input.auth.kind === "oauth2" ? preset.input.auth : null); | ||
| 447 | setOauthStatus(preset.input.auth.kind === "oauth2" ? "connected" : "idle"); | ||
| 448 | setDiscoveryEndpoint(preset.input.endpoint ?? ""); | ||
| 449 | setDiscoveryMessage(`Loaded ${preset.name}.`); | ||
| 450 | setError(null); | ||
| 451 | }; | ||
| 452 | |||
| 453 | const applyDiscoveredRemoteSource = (result: NonNullable<McpDiscoverResult>) => { | ||
| 454 | const remoteTransport = | ||
| 455 | result.transport === "streamable-http" || | ||
| 456 | result.transport === "sse" || | ||
| 457 | result.transport === "auto" | ||
| 458 | ? result.transport | ||
| 459 | : "auto"; | ||
| 460 | |||
| 461 | setName(result.name?.trim() || name); | ||
| 462 | setEndpoint(result.endpoint); | ||
| 463 | setTransportFields({ | ||
| 464 | ...defaultMcpRemoteTransportFields(remoteTransport), | ||
| 465 | queryParamsText: | ||
| 466 | transportFields.transport === "stdio" ? "" : transportFields.queryParamsText, | ||
| 467 | headersText: | ||
| 468 | transportFields.transport === "stdio" ? "" : transportFields.headersText, | ||
| 469 | }); | ||
| 470 | if (result.authInference.supported && result.authInference.suggestedKind === "oauth2") { | ||
| 471 | setAuthKind("oauth2"); | ||
| 472 | setDiscoveryMessage( | ||
| 473 | result.warnings[0] | ||
| 474 | ?? "The server advertised OAuth during discovery. Connect OAuth before saving.", | ||
| 475 | ); | ||
| 476 | } else { | ||
| 477 | setAuthKind("none"); | ||
| 478 | setOauthAuth(null); | ||
| 479 | setOauthStatus("idle"); | ||
| 480 | setDiscoveryMessage( | ||
| 481 | result.warnings[0] | ||
| 482 | ?? `Discovered ${result.toolCount ?? "unknown"} MCP tools and prefilled the connection.`, | ||
| 483 | ); | ||
| 484 | } | ||
| 485 | setError(null); | ||
| 486 | }; | ||
| 487 | |||
| 488 | const handleDiscover = async () => { | ||
| 489 | if (installation.status !== "ready") { | ||
| 490 | setError("Workspace is still loading."); | ||
| 491 | return; | ||
| 492 | } | ||
| 493 | |||
| 494 | setError(null); | ||
| 495 | setDiscoveryMessage(null); | ||
| 496 | |||
| 497 | try { | ||
| 498 | const result = await discoverMutation.mutateAsync({ | ||
| 499 | endpoint: discoveryEndpoint.trim(), | ||
| 500 | probeAuth: showProbeAuth ? buildProbeAuth(probeAuth) : { kind: "none" }, | ||
| 501 | }); | ||
| 502 | |||
| 503 | if (result === null) { | ||
| 504 | setDiscoveryMessage( | ||
| 505 | "Could not verify this endpoint as MCP. You can still continue with manual setup.", | ||
| 506 | ); | ||
| 507 | return; | ||
| 508 | } | ||
| 509 | |||
| 510 | applyDiscoveredRemoteSource(result); | ||
| 511 | } catch (cause) { | ||
| 512 | setError(cause instanceof Error ? cause.message : String(cause)); | ||
| 513 | } | ||
| 514 | }; | ||
| 515 | |||
| 516 | const runOauth = async () => { | 291 | const runOauth = async () => { |
| 517 | if (installation.status !== "ready") { | 292 | if (installation.status !== "ready") { |
| 518 | throw new Error("Workspace is still loading."); | 293 | throw new Error("Workspace is still loading."); |
| @@ -591,382 +366,218 @@ function McpSourceForm(props: { · 72 unmodified lines | |||
| 591 | 366 | ||
| 592 | return ( | 367 | return ( |
| 593 | <div className="space-y-8"> | 368 | <div className="space-y-8"> |
| 594 | {props.mode === "create" && ( | 369 | <div className="space-y-6 rounded-lg border border-border bg-card p-6 text-sm ring-1 ring-foreground/[0.04]"> |
| 595 | <Card className="p-6"> | 370 | <div className="grid gap-2"> |
| 596 | <div className="text-sm font-medium text-foreground">Presets</div> | 371 | <Label>Name</Label> |
| 597 | <div className="mt-3 grid gap-3 sm:grid-cols-2"> | 372 | <Input |
| 598 | {mcpQuickPresets.map((preset) => { | 373 | value={name} |
| 599 | const selected = | 374 | onChange={(event) => setName(event.target.value)} |
| 600 | preset.input.transport === "stdio" | 375 | /> |
| 601 | ? transportFields.transport === "stdio" | 376 | </div> |
| 602 | && transportFields.command === (preset.input.command ?? "") | ||
| 603 | : !isStdio && endpoint.trim() === (preset.input.endpoint ?? ""); | ||
| 604 | |||
| 605 | return ( | ||
| 606 | <Button | ||
| 607 | key={preset.id} | ||
| 608 | type="button" | ||
| 609 | variant="outline" | ||
| 610 | onClick={() => applyPreset(preset)} | ||
| 611 | className={cn( | ||
| 612 | "h-auto flex-col items-start px-4 py-3 text-left", | ||
| 613 | selected && "border-primary/30 bg-primary/5", | ||
| 614 | )} | ||
| 615 | > | ||
| 616 | <div className="flex items-center gap-2"> | ||
| 617 | {selected ? <IconCheck className="size-3.5 text-primary" /> : null} | ||
| 618 | <div className="text-sm font-medium text-foreground">{preset.name}</div> | ||
| 619 | </div> | ||
| 620 | <div className="mt-1 text-xs text-muted-foreground">{preset.summary}</div> | ||
| 621 | </Button> | ||
| 622 | ); | ||
| 623 | })} | ||
| 624 | </div> | ||
| 625 | </Card> | ||
| 626 | )} | ||
| 627 | 377 | ||
| 628 | {props.mode === "create" && ( | 378 | <div className="grid gap-2"> |
| 629 | <Card className="p-6"> | 379 | <Label>Transport</Label> |
| 630 | <div> | 380 | <Select |
| 631 | <div className="flex items-center justify-between gap-3"> | 381 | value={transportFields.transport} |
| 632 | <div className="text-sm font-medium text-foreground">Discover</div> | 382 | onChange={(event) => { |
| 633 | <Button | 383 | const nextTransport = event.target.value as McpTransportValue; |
| 634 | variant="ghost" | 384 | setTransportFields((current) => |
| 635 | onClick={() => setShowProbeAuth((current) => !current)} | 385 | setMcpTransportFieldsTransport(current, nextTransport) |
| 636 | className="text-xs font-medium text-muted-foreground transition-colors hover:text-foreground" | 386 | ); |
| 637 | > | 387 | if (nextTransport === "stdio") { |
| 638 | {showProbeAuth ? "Hide auth" : "Need auth?"} | 388 | setAuthKind("none"); |
| 639 | </Button> | 389 | setOauthAuth(null); |
| 640 | </div> | 390 | setOauthStatus("idle"); |
| 391 | } | ||
| 392 | }} | ||
| 393 | > | ||
| 394 | <option value="">Auto (remote)</option> | ||
| 395 | <option value="auto">Auto</option> | ||
| 396 | <option value="streamable-http">Streamable HTTP</option> | ||
| 397 | <option value="sse">SSE</option> | ||
| 398 | <option value="stdio">stdio</option> | ||
| 399 | </Select> | ||
| 400 | </div> | ||
| 641 | 401 | ||
| 642 | <div className="mt-2 flex flex-col gap-3 sm:flex-row"> | 402 | {isStdio ? ( |
| 403 | <div className="grid gap-4 md:grid-cols-2"> | ||
| 404 | <div className="grid gap-2 md:col-span-2"> | ||
| 405 | <Label>Command</Label> | ||
| 643 | <Input | 406 | <Input |
| 644 | value={discoveryEndpoint} | 407 | value={transportFields.command} |
| 645 | onChange={(event) => setDiscoveryEndpoint(event.target.value)} | 408 | onChange={(event) => |
| 646 | placeholder="https://mcp.example.com/mcp" | 409 | setTransportFields({ |
| 647 | className="flex-1 font-mono text-xs" | 410 | ...transportFields, |
| 411 | command: event.target.value, | ||
| 412 | })} | ||
| 413 | className="font-mono text-xs" | ||
| 648 | /> | 414 | /> |
| 649 | <Button | ||
| 650 | variant="outline" | ||
| 651 | onClick={() => { | ||
| 652 | void handleDiscover(); | ||
| 653 | }} | ||
| 654 | disabled={discoverMutation.status === "pending"} | ||
| 655 | > | ||
| 656 | {discoverMutation.status === "pending" | ||
| 657 | ? <IconSpinner className="size-3.5" /> | ||
| 658 | : <IconSearch className="size-3.5" />} | ||
| 659 | {discoverMutation.status === "pending" ? "Discovering..." : "Discover"} | ||
| 660 | </Button> | ||
| 661 | </div> | 415 | </div> |
| 662 | 416 | ||
| 663 | {showProbeAuth && ( | 417 | <div className="grid gap-2"> |
| 664 | <div className="mt-3 space-y-3 rounded-lg border border-border bg-muted/20 p-4"> | 418 | <Label>Args</Label> |
| 665 | <div className="grid gap-2"> | 419 | <Textarea |
| 666 | <Label>Discovery auth</Label> | 420 | value={transportFields.argsText} |
| 667 | <Select | 421 | onChange={(event) => |
| 668 | value={probeAuth.kind} | 422 | setTransportFields({ |
| 669 | onChange={(event) => | 423 | ...transportFields, |
| 670 | setProbeAuth((current) => ({ | 424 | argsText: event.target.value, |
| 671 | ...current, | 425 | })} |
| 672 | kind: event.target.value as ProbeAuthState["kind"], | 426 | rows={3} |
| 673 | }))} | 427 | placeholder='["server.js","--port","8787"]' |
| 674 | > | 428 | /> |
| 675 | <option value="none">None</option> | 429 | </div> |
| 676 | <option value="bearer">Bearer</option> | ||
| 677 | <option value="basic">Basic</option> | ||
| 678 | <option value="headers">Custom headers</option> | ||
| 679 | </Select> | ||
| 680 | </div> | ||
| 681 | 430 | ||
| 682 | {probeAuth.kind === "bearer" && ( | 431 | <div className="grid gap-2"> |
| 683 | <div className="grid gap-4 md:grid-cols-2"> | 432 | <Label>Environment</Label> |
| 684 | <div className="grid gap-2"> | 433 | <Textarea |
| 685 | <Label>Header name</Label> | 434 | value={transportFields.envText} |
| 686 | <Input | 435 | onChange={(event) => |
| 687 | value={probeAuth.headerName} | 436 | setTransportFields({ |
| 688 | onChange={(event) => | 437 | ...transportFields, |
| 689 | setProbeAuth((current) => ({ ...current, headerName: event.target.value }))} | 438 | envText: event.target.value, |
| 690 | /> | 439 | })} |
| 691 | </div> | 440 | rows={3} |
| 692 | <div className="grid gap-2"> | 441 | placeholder='{"NODE_ENV":"production"}' |
| 693 | <Label>Prefix</Label> | 442 | /> |
| 694 | <Input | 443 | </div> |
| 695 | value={probeAuth.prefix} | ||
| 696 | onChange={(event) => | ||
| 697 | setProbeAuth((current) => ({ ...current, prefix: event.target.value }))} | ||
| 698 | /> | ||
| 699 | </div> | ||
| 700 | <div className="grid gap-2 md:col-span-2"> | ||
| 701 | <Label>Token</Label> | ||
| 702 | <Input | ||
| 703 | value={probeAuth.token} | ||
| 704 | onChange={(event) => | ||
| 705 | setProbeAuth((current) => ({ ...current, token: event.target.value }))} | ||
| 706 | className="font-mono text-xs" | ||
| 707 | /> | ||
| 708 | </div> | ||
| 709 | </div> | ||
| 710 | )} | ||
| 711 | |||
| 712 | {probeAuth.kind === "basic" && ( | ||
| 713 | <div className="grid gap-4 md:grid-cols-2"> | ||
| 714 | <div className="grid gap-2"> | ||
| 715 | <Label>Username</Label> | ||
| 716 | <Input | ||
| 717 | value={probeAuth.username} | ||
| 718 | onChange={(event) => | ||
| 719 | setProbeAuth((current) => ({ ...current, username: event.target.value }))} | ||
| 720 | /> | ||
| 721 | </div> | ||
| 722 | <div className="grid gap-2"> | ||
| 723 | <Label>Password</Label> | ||
| 724 | <Input | ||
| 725 | value={probeAuth.password} | ||
| 726 | onChange={(event) => | ||
| 727 | setProbeAuth((current) => ({ ...current, password: event.target.value }))} | ||
| 728 | type="password" | ||
| 729 | /> | ||
| 730 | </div> | ||
| 731 | </div> | ||
| 732 | )} | ||
| 733 | |||
| 734 | {probeAuth.kind === "headers" && ( | ||
| 735 | <div className="grid gap-2"> | ||
| 736 | <Label>Headers JSON</Label> | ||
| 737 | <Textarea | ||
| 738 | value={probeAuth.headersText} | ||
| 739 | onChange={(event) => | ||
| 740 | setProbeAuth((current) => ({ ...current, headersText: event.target.value }))} | ||
| 741 | rows={3} | ||
| 742 | placeholder='{"x-api-key":"..."}' | ||
| 743 | /> | ||
| 744 | </div> | ||
| 745 | )} | ||
| 746 | </div> | ||
| 747 | )} | ||
| 748 | 444 | ||
| 749 | {discoveryMessage && ( | 445 | <div className="grid gap-2 md:col-span-2"> |
| 750 | <div className="mt-2 text-xs text-muted-foreground"> | 446 | <Label>Working Directory</Label> |
| 751 | {discoveryMessage} | 447 | <Input |
| 752 | </div> | 448 | value={transportFields.cwd} |
| 753 | )} | 449 | onChange={(event) => |
| 450 | setTransportFields({ | ||
| 451 | ...transportFields, | ||
| 452 | cwd: event.target.value, | ||
| 453 | })} | ||
| 454 | className="font-mono text-xs" | ||
| 455 | /> | ||
| 456 | </div> | ||
| 754 | </div> | 457 | </div> |
| 755 | </Card> | 458 | ) : ( |
| 756 | )} | 459 | <div className="grid gap-4 md:grid-cols-2"> |
| 757 | 460 | <div className="grid gap-2 md:col-span-2"> | |
| 758 | <Card className="space-y-6 p-6"> | 461 | <Label>Endpoint</Label> |
| 759 | <div className="grid gap-2"> | 462 | <Input |
| 760 | <Label>Name</Label> | 463 | value={endpoint} |
| 761 | <Input | 464 | onChange={(event) => setEndpoint(event.target.value)} |
| 762 | value={name} | 465 | className="font-mono text-xs" |
| 763 | onChange={(event) => setName(event.target.value)} | 466 | /> |
| 764 | /> | 467 | </div> |
| 765 | </div> | ||
| 766 | |||
| 767 | <div className="grid gap-2"> | ||
| 768 | <Label>Transport</Label> | ||
| 769 | <Select | ||
| 770 | value={transportFields.transport} | ||
| 771 | onChange={(event) => { | ||
| 772 | const nextTransport = event.target.value as McpTransportValue; | ||
| 773 | setTransportFields((current) => | ||
| 774 | setMcpTransportFieldsTransport(current, nextTransport) | ||
| 775 | ); | ||
| 776 | if (nextTransport === "stdio") { | ||
| 777 | setAuthKind("none"); | ||
| 778 | setOauthAuth(null); | ||
| 779 | setOauthStatus("idle"); | ||
| 780 | } | ||
| 781 | }} | ||
| 782 | > | ||
| 783 | <option value="">Auto (remote)</option> | ||
| 784 | <option value="auto">Auto</option> | ||
| 785 | <option value="streamable-http">Streamable HTTP</option> | ||
| 786 | <option value="sse">SSE</option> | ||
| 787 | <option value="stdio">stdio</option> | ||
| 788 | </Select> | ||
| 789 | </div> | ||
| 790 | 468 | ||
| 791 | {isStdio ? ( | 469 | <div className="grid gap-2"> |
| 792 | <div className="grid gap-4 md:grid-cols-2"> | 470 | <Label>Query Params</Label> |
| 793 | <div className="grid gap-2 md:col-span-2"> | 471 | <Textarea |
| 794 | <Label>Command</Label> | 472 | value={transportFields.queryParamsText} |
| 795 | <Input | 473 | onChange={(event) => |
| 796 | value={transportFields.command} | 474 | setTransportFields({ |
| 797 | onChange={(event) => | 475 | ...transportFields, |
| 798 | setTransportFields({ | 476 | queryParamsText: event.target.value, |
| 799 | ...transportFields, | 477 | })} |
| 800 | command: event.target.value, | 478 | rows={3} |
| 801 | })} | 479 | placeholder='{"transport":"streamable-http"}' |
| 802 | className="font-mono text-xs" | 480 | /> |
| 803 | /> | 481 | </div> |
| 804 | </div> | ||
| 805 | 482 | ||
| 806 | <div className="grid gap-2"> | 483 | <div className="grid gap-2"> |
| 807 | <Label>Args</Label> | 484 | <Label>Headers</Label> |
| 808 | <Textarea | 485 | <Textarea |
| 809 | value={transportFields.argsText} | 486 | value={transportFields.headersText} |
| 810 | onChange={(event) => | 487 | onChange={(event) => |
| 811 | setTransportFields({ | 488 | setTransportFields({ |
| 812 | ...transportFields, | 489 | ...transportFields, |
| 813 | argsText: event.target.value, | 490 | headersText: event.target.value, |
| 814 | })} | 491 | })} |
| 815 | rows={3} | 492 | rows={3} |
| 816 | placeholder='["server.js","--port","8787"]' | 493 | placeholder='{"x-api-key":"..."}' |
| 817 | /> | 494 | /> |
| 495 | </div> | ||
| 818 | </div> | 496 | </div> |
| 497 | )} | ||
| 819 | 498 | ||
| 499 | {!isStdio && ( | ||
| 820 | <div className="grid gap-2"> | 500 | <div className="grid gap-2"> |
| 821 | <Label>Environment</Label> | 501 | <Label>Auth</Label> |
| 822 | <Textarea | 502 | <Select |
| 823 | value={transportFields.envText} | 503 | value={authKind} |
| 824 | onChange={(event) => | 504 | onChange={(event) => { |
| 825 | setTransportFields({ | 505 | const nextKind = event.target.value as McpConnectionAuth["kind"]; |
| 826 | ...transportFields, | 506 | setAuthKind(nextKind); |
| 827 | envText: event.target.value, | 507 | if (nextKind !== "oauth2") { |
| 828 | })} | 508 | setOauthAuth(null); |
| 829 | rows={3} | 509 | setOauthStatus("idle"); |
| 830 | placeholder='{"NODE_ENV":"production"}' | 510 | } |
| 831 | /> | 511 | }} |
| 832 | </div> | 512 | > |
| 833 | 513 | <option value="none">None</option> | |
| 834 | <div className="grid gap-2 md:col-span-2"> | 514 | <option value="oauth2">OAuth 2.0</option> |
| 835 | <Label>Working Directory</Label> | 515 | </Select> |
| 836 | <Input | ||
| 837 | value={transportFields.cwd} | ||
| 838 | onChange={(event) => | ||
| 839 | setTransportFields({ | ||
| 840 | ...transportFields, | ||
| 841 | cwd: event.target.value, | ||
| 842 | })} | ||
| 843 | className="font-mono text-xs" | ||
| 844 | /> | ||
| 845 | </div> | ||
| 846 | </div> | ||
| 847 | ) : ( | ||
| 848 | <div className="grid gap-4 md:grid-cols-2"> | ||
| 849 | <div className="grid gap-2 md:col-span-2"> | ||
| 850 | <Label>Endpoint</Label> | ||
| 851 | <Input | ||
| 852 | value={endpoint} | ||
| 853 | onChange={(event) => setEndpoint(event.target.value)} | ||
| 854 | className="font-mono text-xs" | ||
| 855 | /> | ||
| 856 | </div> | 516 | </div> |
| 857 | 517 | )} | |
| 858 | <div className="grid gap-2"> | 518 | |
| 859 | <Label>Query Params</Label> | 519 | {!isStdio && authKind === "oauth2" && ( |
| 860 | <Textarea | 520 | <div className="rounded-lg border border-border bg-muted/20 p-4"> |
| 861 | value={transportFields.queryParamsText} | 521 | <div className="flex flex-wrap items-center justify-between gap-3"> |
| 862 | onChange={(event) => | 522 | <div> |
| 863 | setTransportFields({ | 523 | <div className="text-sm font-medium text-foreground">OAuth</div> |
| 864 | ...transportFields, | 524 | <div className="mt-1 text-xs text-muted-foreground"> |
| 865 | queryParamsText: event.target.value, | 525 | Authenticate with the MCP server's built-in OAuth flow. |
| 866 | })} | 526 | </div> |
| 867 | rows={3} | 527 | </div> |
| 868 | placeholder='{"transport":"streamable-http"}' | 528 | <Button |
| 869 | /> | 529 | variant="outline" |
| 530 | onClick={() => { | ||
| 531 | setError(null); | ||
| 532 | setOauthStatus("pending"); | ||
| 533 | void runOauth() | ||
| 534 | .catch((cause) => { | ||
| 535 | setOauthStatus("idle"); | ||
| 536 | setError( | ||
| 537 | cause instanceof Error ? cause.message : String(cause), | ||
| 538 | ); | ||
| 539 | }); | ||
| 540 | }} | ||
| 541 | > | ||
| 542 | {oauthStatus === "connected" ? "Reconnect OAuth" : "Connect OAuth"} | ||
| 543 | </Button> | ||
| 544 | </div> | ||
| 545 | {oauthStatus === "connected" && ( | ||
| 546 | <div className="mt-3 text-xs text-primary"> | ||
| 547 | Connected | ||
| 548 | </div> | ||
| 549 | )} | ||
| 870 | </div> | 550 | </div> |
| 551 | )} | ||
| 871 | 552 | ||
| 872 | <div className="grid gap-2"> | 553 | {error && ( |
| 873 | <Label>Headers</Label> | 554 | <Alert variant="destructive"> |
| 874 | <Textarea | 555 | {error} |
| 875 | value={transportFields.headersText} | 556 | </Alert> |
| 876 | onChange={(event) => | 557 | )} |
| 877 | setTransportFields({ | ||
| 878 | ...transportFields, | ||
| 879 | headersText: event.target.value, | ||
| 880 | })} | ||
| 881 | rows={3} | ||
| 882 | placeholder='{"x-api-key":"..."}' | ||
| 883 | /> | ||
| 884 | </div> | ||
| 885 | </div> | ||
| 886 | )} | ||
| 887 | 558 | ||
| 888 | {!isStdio && ( | 559 | <div className="flex items-center justify-end gap-3"> |
| 889 | <div className="grid gap-2"> | 560 | <Button |
| 890 | <Label>Auth</Label> | 561 | onClick={() => { |
| 891 | <Select | 562 | setError(null); |
| 892 | value={authKind} | 563 | void submitMutation |
| 893 | onChange={(event) => { | 564 | .mutateAsync(buildInput()) |
| 894 | const nextKind = event.target.value as McpConnectionAuth["kind"]; | 565 | .catch((cause: unknown) => |
| 895 | setAuthKind(nextKind); | 566 | setError(cause instanceof Error ? cause.message : String(cause)) |
| 896 | if (nextKind !== "oauth2") { | 567 | ); |
| 897 | setOauthAuth(null); | ||
| 898 | setOauthStatus("idle"); | ||
| 899 | } | ||
| 900 | }} | 568 | }} |
| 569 | disabled={submitMutation.status === "pending"} | ||
| 901 | > | 570 | > |
| 902 | <option value="none">None</option> | 571 | {submitMutation.status === "pending" |
| 903 | <option value="oauth2">OAuth 2.0</option> | 572 | ? props.mode === "create" |
| 904 | </Select> | 573 | ? "Creating..." |
| 905 | </div> | 574 | : "Saving..." |
| 906 | )} | 575 | : props.mode === "create" |
| 907 | 576 | ? "Create Source" | |
| 908 | {!isStdio && authKind === "oauth2" && ( | 577 | : "Save Changes"} |
| 909 | <div className="rounded-lg border border-border bg-muted/20 p-4"> | 578 | </Button> |
| 910 | <div className="flex flex-wrap items-center justify-between gap-3"> | ||
| 911 | <div> | ||
| 912 | <div className="text-sm font-medium text-foreground">OAuth</div> | ||
| 913 | <div className="mt-1 text-xs text-muted-foreground"> | ||
| 914 | Authenticate with the MCP server's built-in OAuth flow. | ||
| 915 | </div> | ||
| 916 | </div> | ||
| 917 | <Button | ||
| 918 | variant="outline" | ||
| 919 | onClick={() => { | ||
| 920 | setError(null); | ||
| 921 | setOauthStatus("pending"); | ||
| 922 | void runOauth() | ||
| 923 | .catch((cause) => { | ||
| 924 | setOauthStatus("idle"); | ||
| 925 | setError( | ||
| 926 | cause instanceof Error ? cause.message : String(cause), | ||
| 927 | ); | ||
| 928 | }); | ||
| 929 | }} | ||
| 930 | > | ||
| 931 | {oauthStatus === "connected" ? "Reconnect OAuth" : "Connect OAuth"} | ||
| 932 | </Button> | ||
| 933 | </div> | ||
| 934 | {oauthStatus === "connected" && ( | ||
| 935 | <div className="mt-3 text-xs text-primary"> | ||
| 936 | Connected | ||
| 937 | </div> | ||
| 938 | )} | ||
| 939 | </div> | 579 | </div> |
| 940 | )} | ||
| 941 | |||
| 942 | {error && ( | ||
| 943 | <Alert variant="destructive"> | ||
| 944 | {error} | ||
| 945 | </Alert> | ||
| 946 | )} | ||
| 947 | |||
| 948 | <div className="flex items-center justify-end gap-3"> | ||
| 949 | <Button | ||
| 950 | onClick={() => { | ||
| 951 | setError(null); | ||
| 952 | void submitMutation | ||
| 953 | .mutateAsync(buildInput()) | ||
| 954 | .catch((cause: unknown) => | ||
| 955 | setError(cause instanceof Error ? cause.message : String(cause)) | ||
| 956 | ); | ||
| 957 | }} | ||
| 958 | disabled={submitMutation.status === "pending"} | ||
| 959 | > | ||
| 960 | {submitMutation.status === "pending" | ||
| 961 | ? props.mode === "create" | ||
| 962 | ? "Creating..." | ||
| 963 | : "Saving..." | ||
| 964 | : props.mode === "create" | ||
| 965 | ? "Create Source" | ||
| 966 | : "Save Changes"} | ||
| 967 | </Button> | ||
| 968 | </div> | 580 | </div> |
| 969 | </Card> | ||
| 970 | </div> | 581 | </div> |
| 971 | ); | 582 | ); |
| 972 | } | 583 | } |
| @@ -986,27 +597,40 @@ export function McpAddPage() { · 13 unmodified lines | |||
| 986 | } | 597 | } |
| 987 | 598 | ||
| 988 | return ( | 599 | return ( |
| 989 | <McpSourceForm | 600 | <div className="min-h-0 flex-1 overflow-y-auto"> |
| 990 | initialValue={initialValue} | 601 | <div className="mx-auto w-full max-w-3xl px-6 py-10"> |
| 991 | mode="create" | 602 | <div className="mb-8"> |
| 992 | onSubmit={async (input) => { | 603 | <h1 className="font-display text-2xl tracking-tight text-foreground"> |
| 993 | const source = await createSource({ | 604 | Add MCP Source |
| 994 | path: { | 605 | </h1> |
| 995 | workspaceId: installation.data.scopeId, | 606 | <p className="mt-1.5 text-sm leading-6 text-muted-foreground"> |
| 996 | }, | 607 | Connect a Model Context Protocol server and import its tools. |
| 997 | payload: input, | 608 | </p> |
| 998 | reactivityKeys: { | 609 | </div> |
| 999 | sources: [installation.data.scopeId], | 610 | |
| 1000 | }, | 611 | <McpSourceForm |
| 1001 | }); | 612 | initialValue={initialValue} |
| 1002 | 613 | mode="create" | |
| 1003 | startTransition(() => { | 614 | onSubmit={async (input) => { |
| 1004 | void navigation.detail(source.id, { | 615 | const source = await createSource({ |
| 1005 | tab: "model", | 616 | path: { |
| 1006 | }); | 617 | workspaceId: installation.data.scopeId, |
| 1007 | }); | 618 | }, |
| 1008 | }} | 619 | payload: input, |
| 1009 | /> | 620 | reactivityKeys: { |
| 621 | sources: [installation.data.scopeId], | ||
| 622 | }, | ||
| 623 | }); | ||
| 624 | |||
| 625 | startTransition(() => { | ||
| 626 | void navigation.detail(source.id, { | ||
| 627 | tab: "model", | ||
| 628 | }); | ||
| 629 | }); | ||
| 630 | }} | ||
| 631 | /> | ||
| 632 | </div> | ||
| 633 | </div> | ||
| 1010 | ); | 634 | ); |
| 1011 | } | 635 | } |
| 1012 | 636 | ||
| @@ -1054,32 +678,45 @@ export function McpEditPage(props: { · 41 unmodified lines | |||
| 1054 | } | 678 | } |
| 1055 | 679 | ||
| 1056 | return ( | 680 | return ( |
| 1057 | <McpSourceForm | 681 | <div className="min-h-0 flex-1 overflow-y-auto"> |
| 1058 | initialValue={configResult.value} | 682 | <div className="mx-auto w-full max-w-3xl px-6 py-10"> |
| 1059 | mode="edit" | 683 | <div className="mb-8"> |
| 1060 | onSubmit={async (input) => { | 684 | <h1 className="font-display text-2xl tracking-tight text-foreground"> |
| 1061 | const source = await updateSource({ | 685 | Edit MCP Source |
| 1062 | path: { | 686 | </h1> |
| 1063 | workspaceId: installation.data.scopeId, | 687 | <p className="mt-1.5 text-sm leading-6 text-muted-foreground"> |
| 1064 | sourceId: props.source.id, | 688 | Update the connection settings for {props.source.name}. |
| 1065 | }, | 689 | </p> |
| 1066 | payload: input, | 690 | </div> |
| 1067 | reactivityKeys: { | 691 | |
| 1068 | sources: [installation.data.scopeId], | 692 | <McpSourceForm |
| 1069 | source: [installation.data.scopeId, props.source.id], | 693 | initialValue={configResult.value} |
| 1070 | sourceInspection: [installation.data.scopeId, props.source.id], | 694 | mode="edit" |
| 1071 | sourceInspectionTool: [installation.data.scopeId, props.source.id], | 695 | onSubmit={async (input) => { |
| 1072 | sourceDiscovery: [installation.data.scopeId, props.source.id], | 696 | const source = await updateSource({ |
| 1073 | }, | 697 | path: { |
| 1074 | }); | 698 | workspaceId: installation.data.scopeId, |
| 1075 | 699 | sourceId: props.source.id, | |
| 1076 | startTransition(() => { | 700 | }, |
| 1077 | void navigation.detail(source.id, { | 701 | payload: input, |
| 1078 | tab: "model", | 702 | reactivityKeys: { |
| 1079 | }); | 703 | sources: [installation.data.scopeId], |
| 1080 | }); | 704 | source: [installation.data.scopeId, props.source.id], |
| 1081 | }} | 705 | sourceInspection: [installation.data.scopeId, props.source.id], |
| 1082 | /> | 706 | sourceInspectionTool: [installation.data.scopeId, props.source.id], |
| 707 | sourceDiscovery: [installation.data.scopeId, props.source.id], | ||
| 708 | }, | ||
| 709 | }); | ||
| 710 | |||
| 711 | startTransition(() => { | ||
| 712 | void navigation.detail(source.id, { | ||
| 713 | tab: "model", | ||
| 714 | }); | ||
| 715 | }); | ||
| 716 | }} | ||
| 717 | /> | ||
| 718 | </div> | ||
| 719 | </div> | ||
| 1083 | ); | 720 | ); |
| 1084 | } | 721 | } |
| 1085 | 722 | ||
| @@ -1,4 +1,7 @@ | |||
| 1 | import { defineExecutorFrontendPlugin } from "@executor/react/plugins"; | 1 | import { |
| 2 | defineExecutorFrontendPlugin, | ||
| 3 | liveFrontendPluginComponent, | ||
| 4 | } from "@executor/react/plugins"; | ||
| 2 | 5 | ||
| 3 | import { | 6 | import { |
| 4 | McpAddPage, | 7 | McpAddPage, |
| @@ -14,17 +17,17 @@ export const McpReactPlugin = defineExecutorFrontendPlugin({ · 9 unmodified lines | |||
| 14 | { | 17 | { |
| 15 | key: "add", | 18 | key: "add", |
| 16 | path: "add", | 19 | path: "add", |
| 17 | component: McpAddPage, | 20 | component: liveFrontendPluginComponent(() => McpAddPage), |
| 18 | }, | 21 | }, |
| 19 | { | 22 | { |
| 20 | key: "detail", | 23 | key: "detail", |
| 21 | path: "sources/$sourceId", | 24 | path: "sources/$sourceId", |
| 22 | component: McpDetailRoute, | 25 | component: liveFrontendPluginComponent(() => McpDetailRoute), |
| 23 | }, | 26 | }, |
| 24 | { | 27 | { |
| 25 | key: "edit", | 28 | key: "edit", |
| 26 | path: "sources/$sourceId/edit", | 29 | path: "sources/$sourceId/edit", |
| 27 | component: McpEditRoute, | 30 | component: liveFrontendPluginComponent(() => McpEditRoute), |
| 28 | }, | 31 | }, |
| 29 | ], | 32 | ], |
| 30 | }); | 33 | }); |
| @@ -1,4 +1,7 @@ | |||
| 1 | import { defineExecutorFrontendPlugin } from "@executor/react/plugins"; | 1 | import { |
| 2 | defineExecutorFrontendPlugin, | ||
| 3 | liveFrontendPluginComponent, | ||
| 4 | } from "@executor/react/plugins"; | ||
| 2 | import { ONEPASSWORD_SECRET_STORE_KIND } from "@executor/plugin-onepassword-shared"; | 5 | import { ONEPASSWORD_SECRET_STORE_KIND } from "@executor/plugin-onepassword-shared"; |
| 3 | 6 | ||
| 4 | import { OnePasswordSecretStoreCreateForm } from "./components"; | 7 | import { OnePasswordSecretStoreCreateForm } from "./components"; |
| @@ -8,6 +11,8 @@ export const OnePasswordReactPlugin = defineExecutorFrontendPlugin({ · 3 unmodified lines | |||
| 8 | displayName: "1Password", | 11 | displayName: "1Password", |
| 9 | secretStore: { | 12 | secretStore: { |
| 10 | kind: ONEPASSWORD_SECRET_STORE_KIND, | 13 | kind: ONEPASSWORD_SECRET_STORE_KIND, |
| 11 | CreateStoreForm: OnePasswordSecretStoreCreateForm, | 14 | CreateStoreForm: liveFrontendPluginComponent( |
| 15 | () => OnePasswordSecretStoreCreateForm, | ||
| 16 | ), | ||
| 12 | }, | 17 | }, |
| 13 | }); | 18 | }); |
| @@ -15,7 +15,6 @@ import { · 14 unmodified lines | |||
| 15 | import { | 15 | import { |
| 16 | Alert, | 16 | Alert, |
| 17 | Button, | 17 | Button, |
| 18 | Card, | ||
| 19 | DocumentPanel, | 18 | DocumentPanel, |
| 20 | IconPencil, | 19 | IconPencil, |
| 21 | Input, | 20 | Input, |
| @@ -149,7 +148,7 @@ const Section = (props: { · 127 unmodified lines | |||
| 149 | children: ReactNode; | 148 | children: ReactNode; |
| 150 | }) => ( | 149 | }) => ( |
| 151 | <section> | 150 | <section> |
| 152 | <h2 className="mb-3 text-sm font-semibold text-foreground">{props.title}</h2> | 151 | <h2 className="mb-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">{props.title}</h2> |
| 153 | {props.children} | 152 | {props.children} |
| 154 | </section> | 153 | </section> |
| 155 | ); | 154 | ); |
| @@ -387,7 +386,7 @@ function OpenApiSourceForm(props: { · 231 unmodified lines | |||
| 387 | }; | 386 | }; |
| 388 | 387 | ||
| 389 | return ( | 388 | return ( |
| 390 | <Card className="space-y-6 p-6"> | 389 | <div className="space-y-6 rounded-lg border border-border bg-card p-6 text-sm ring-1 ring-foreground/[0.04]"> |
| 391 | <Section title="Connection"> | 390 | <Section title="Connection"> |
| 392 | <div className="grid gap-4"> | 391 | <div className="grid gap-4"> |
| 393 | <div className="grid gap-2"> | 392 | <div className="grid gap-2"> |
| @@ -477,59 +476,47 @@ function OpenApiSourceForm(props: { · 83 unmodified lines | |||
| 477 | </div> | 476 | </div> |
| 478 | </Section> | 477 | </Section> |
| 479 | 478 | ||
| 480 | <Section title="Preview"> | 479 | {(preview || previewMutation.status === "pending") && ( |
| 481 | <div className="flex items-center gap-3"> | 480 | <Section title="Preview"> |
| 482 | <Button | 481 | {previewMutation.status === "pending" ? ( |
| 483 | variant="outline" | 482 | <div className="text-xs text-muted-foreground">Loading preview...</div> |
| 484 | type="button" | 483 | ) : preview && ( |
| 485 | onClick={() => { | 484 | <div className="rounded-lg border border-border bg-muted/30 px-4 py-3 text-sm"> |
| 486 | void runPreview({ mode: "manual" }); | 485 | <dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5 text-xs"> |
| 487 | }} | 486 | <dt className="font-medium text-foreground">Title</dt> |
| 488 | disabled={previewMutation.status === "pending" || submitMutation.status === "pending"} | 487 | <dd className="text-muted-foreground">{preview.title ?? "—"}</dd> |
| 489 | > | 488 | <dt className="font-medium text-foreground">Version</dt> |
| 490 | {previewMutation.status === "pending" ? "Previewing..." : "Preview Spec"} | 489 | <dd className="text-muted-foreground">{preview.version ?? "—"}</dd> |
| 491 | </Button> | 490 | <dt className="font-medium text-foreground">Operations</dt> |
| 492 | {preview && ( | 491 | <dd className="text-muted-foreground">{preview.operationCount}</dd> |
| 493 | <div className="text-xs text-muted-foreground"> | 492 | <dt className="font-medium text-foreground">Base URL</dt> |
| 494 | {preview.operationCount} operations | 493 | <dd className="font-mono text-muted-foreground">{preview.baseUrl ?? "—"}</dd> |
| 495 | {preview.version ? ` · v${preview.version}` : ""} | 494 | {preview.namespace && ( |
| 495 | <> | ||
| 496 | <dt className="font-medium text-foreground">Namespace</dt> | ||
| 497 | <dd className="text-muted-foreground">{preview.namespace}</dd> | ||
| 498 | </> | ||
| 499 | )} | ||
| 500 | {preview.securitySchemes.length > 0 && ( | ||
| 501 | <> | ||
| 502 | <dt className="font-medium text-foreground">Auth</dt> | ||
| 503 | <dd className="text-muted-foreground"> | ||
| 504 | {preview.securitySchemes | ||
| 505 | .map((scheme) => `${scheme.name} (${previewSecuritySchemeLabel(scheme)})`) | ||
| 506 | .join(", ")} | ||
| 507 | </dd> | ||
| 508 | </> | ||
| 509 | )} | ||
| 510 | </dl> | ||
| 511 | {preview.warnings.length > 0 && ( | ||
| 512 | <Alert variant="warning" className="mt-3 text-xs"> | ||
| 513 | {preview.warnings.join(" ")} | ||
| 514 | </Alert> | ||
| 515 | )} | ||
| 496 | </div> | 516 | </div> |
| 497 | )} | 517 | )} |
| 498 | </div> | 518 | </Section> |
| 499 | {preview && ( | 519 | )} |
| 500 | <div className="mt-3 rounded-lg border border-border bg-muted/30 px-4 py-3 text-sm"> | ||
| 501 | <dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5 text-xs"> | ||
| 502 | <dt className="font-medium text-foreground">Title</dt> | ||
| 503 | <dd className="text-muted-foreground">{preview.title ?? "—"}</dd> | ||
| 504 | <dt className="font-medium text-foreground">Version</dt> | ||
| 505 | <dd className="text-muted-foreground">{preview.version ?? "—"}</dd> | ||
| 506 | <dt className="font-medium text-foreground">Base URL</dt> | ||
| 507 | <dd className="font-mono text-muted-foreground">{preview.baseUrl ?? "—"}</dd> | ||
| 508 | {preview.namespace && ( | ||
| 509 | <> | ||
| 510 | <dt className="font-medium text-foreground">Namespace</dt> | ||
| 511 | <dd className="text-muted-foreground">{preview.namespace}</dd> | ||
| 512 | </> | ||
| 513 | )} | ||
| 514 | {preview.securitySchemes.length > 0 && ( | ||
| 515 | <> | ||
| 516 | <dt className="font-medium text-foreground">Auth</dt> | ||
| 517 | <dd className="text-muted-foreground"> | ||
| 518 | {preview.securitySchemes | ||
| 519 | .map((scheme) => `${scheme.name} (${previewSecuritySchemeLabel(scheme)})`) | ||
| 520 | .join(", ")} | ||
| 521 | </dd> | ||
| 522 | </> | ||
| 523 | )} | ||
| 524 | </dl> | ||
| 525 | {preview.warnings.length > 0 && ( | ||
| 526 | <Alert variant="warning" className="mt-3 text-xs"> | ||
| 527 | {preview.warnings.join(" ")} | ||
| 528 | </Alert> | ||
| 529 | )} | ||
| 530 | </div> | ||
| 531 | )} | ||
| 532 | </Section> | ||
| 533 | 520 | ||
| 534 | {error && ( | 521 | {error && ( |
| 535 | <Alert variant="destructive"> | 522 | <Alert variant="destructive"> |
| @@ -548,7 +535,7 @@ function OpenApiSourceForm(props: { · 12 unmodified lines | |||
| 548 | {submitMutation.status === "pending" ? props.busyLabel : props.submitLabel} | 535 | {submitMutation.status === "pending" ? props.busyLabel : props.submitLabel} |
| 549 | </Button> | 536 | </Button> |
| 550 | </div> | 537 | </div> |
| 551 | </Card> | 538 | </div> |
| 552 | ); | 539 | ); |
| 553 | } | 540 | } |
| 554 | 541 | ||
| @@ -564,35 +551,50 @@ export function OpenApiAddSourcePage() { · 9 unmodified lines | |||
| 564 | 551 | ||
| 565 | if (!workspace.enabled) { | 552 | if (!workspace.enabled) { |
| 566 | return ( | 553 | return ( |
| 567 | <Section title="Add Source"> | 554 | <div className="min-h-0 flex-1 overflow-y-auto"> |
| 568 | <div className="text-sm text-muted-foreground">Loading workspace...</div> | 555 | <div className="mx-auto w-full max-w-3xl px-6 py-10"> |
| 569 | </Section> | 556 | <div className="text-sm text-muted-foreground">Loading workspace...</div> |
| 557 | </div> | ||
| 558 | </div> | ||
| 570 | ); | 559 | ); |
| 571 | } | 560 | } |
| 572 | 561 | ||
| 573 | return ( | 562 | return ( |
| 574 | <OpenApiSourceForm | 563 | <div className="min-h-0 flex-1 overflow-y-auto"> |
| 575 | mode="create" | 564 | <div className="mx-auto w-full max-w-3xl px-6 py-10"> |
| 576 | workspaceId={workspace.workspaceId} | 565 | <div className="mb-8"> |
| 577 | initialValue={initialValue} | 566 | <h1 className="font-display text-2xl tracking-tight text-foreground"> |
| 578 | submitLabel="Create Source" | 567 | Add OpenAPI Source |
| 579 | busyLabel="Creating..." | 568 | </h1> |
| 580 | onSubmit={async (payload) => { | 569 | <p className="mt-1.5 text-sm leading-6 text-muted-foreground"> |
| 581 | const source = await createSource({ | 570 | Import operations from an OpenAPI specification as callable tools. |
| 582 | path: { workspaceId: workspace.workspaceId }, | 571 | </p> |
| 583 | payload, | 572 | </div> |
| 584 | reactivityKeys: { | 573 | |
| 585 | sources: [workspace.workspaceId], | 574 | <OpenApiSourceForm |
| 586 | }, | 575 | mode="create" |
| 587 | }); | 576 | workspaceId={workspace.workspaceId} |
| 588 | 577 | initialValue={initialValue} | |
| 589 | startTransition(() => { | 578 | submitLabel="Create Source" |
| 590 | void navigation.detail(source.id, { | 579 | busyLabel="Creating..." |
| 591 | tab: "model", | 580 | onSubmit={async (payload) => { |
| 592 | }); | 581 | const source = await createSource({ |
| 593 | }); | 582 | path: { workspaceId: workspace.workspaceId }, |
| 594 | }} | 583 | payload, |
| 595 | /> | 584 | reactivityKeys: { |
| 585 | sources: [workspace.workspaceId], | ||
| 586 | }, | ||
| 587 | }); | ||
| 588 | |||
| 589 | startTransition(() => { | ||
| 590 | void navigation.detail(source.id, { | ||
| 591 | tab: "model", | ||
| 592 | }); | ||
| 593 | }); | ||
| 594 | }} | ||
| 595 | /> | ||
| 596 | </div> | ||
| 597 | </div> | ||
| 596 | ); | 598 | ); |
| 597 | } | 599 | } |
| 598 | 600 | ||
| @@ -603,9 +605,11 @@ export function OpenApiEditSourcePage(props: { · 4 unmodified lines | |||
| 603 | 605 | ||
| 604 | if (!workspace.enabled) { | 606 | if (!workspace.enabled) { |
| 605 | return ( | 607 | return ( |
| 606 | <Section title="Edit Source"> | 608 | <div className="min-h-0 flex-1 overflow-y-auto"> |
| 607 | <div className="text-sm text-muted-foreground">Loading workspace...</div> | 609 | <div className="mx-auto w-full max-w-3xl px-6 py-10"> |
| 608 | </Section> | 610 | <div className="text-sm text-muted-foreground">Loading workspace...</div> |
| 611 | </div> | ||
| 612 | </div> | ||
| 609 | ); | 613 | ); |
| 610 | } | 614 | } |
| 611 | 615 | ||
| @@ -641,53 +645,64 @@ function OpenApiEditSourcePageReady(props: { · 29 unmodified lines | |||
| 641 | ); | 645 | ); |
| 642 | 646 | ||
| 643 | if (!Result.isSuccess(configResult)) { | 647 | if (!Result.isSuccess(configResult)) { |
| 644 | if (Result.isFailure(configResult)) { | ||
| 645 | return ( | ||
| 646 | <Section title="Edit Source"> | ||
| 647 | <Alert variant="destructive"> | ||
| 648 | Failed loading source configuration. | ||
| 649 | </Alert> | ||
| 650 | </Section> | ||
| 651 | ); | ||
| 652 | } | ||
| 653 | |||
| 654 | return ( | 648 | return ( |
| 655 | <Section title="Edit Source"> | 649 | <div className="min-h-0 flex-1 overflow-y-auto"> |
| 656 | <div className="text-sm text-muted-foreground">Loading configuration...</div> | 650 | <div className="mx-auto w-full max-w-3xl px-6 py-10"> |
| 657 | </Section> | 651 | {Result.isFailure(configResult) ? ( |
| 652 | <Alert variant="destructive"> | ||
| 653 | Failed loading source configuration. | ||
| 654 | </Alert> | ||
| 655 | ) : ( | ||
| 656 | <div className="text-sm text-muted-foreground">Loading configuration...</div> | ||
| 657 | )} | ||
| 658 | </div> | ||
| 659 | </div> | ||
| 658 | ); | 660 | ); |
| 659 | } | 661 | } |
| 660 | 662 | ||
| 661 | return ( | 663 | return ( |
| 662 | <OpenApiSourceForm | 664 | <div className="min-h-0 flex-1 overflow-y-auto"> |
| 663 | mode="edit" | 665 | <div className="mx-auto w-full max-w-3xl px-6 py-10"> |
| 664 | workspaceId={props.workspaceId} | 666 | <div className="mb-8"> |
| 665 | initialValue={inputFromConfig(configResult.value)} | 667 | <h1 className="font-display text-2xl tracking-tight text-foreground"> |
| 666 | submitLabel="Save Changes" | 668 | Edit OpenAPI Source |
| 667 | busyLabel="Saving..." | 669 | </h1> |
| 668 | onSubmit={async (config) => { | 670 | <p className="mt-1.5 text-sm leading-6 text-muted-foreground"> |
| 669 | const source = await updateSource({ | 671 | Update the connection settings for {props.source.name}. |
| 670 | path: { | 672 | </p> |
| 671 | workspaceId: props.workspaceId, | 673 | </div> |
| 672 | sourceId: props.source.id, | 674 | |
| 673 | }, | 675 | <OpenApiSourceForm |
| 674 | payload: config, | 676 | mode="edit" |
| 675 | reactivityKeys: { | 677 | workspaceId={props.workspaceId} |
| 676 | sources: [props.workspaceId], | 678 | initialValue={inputFromConfig(configResult.value)} |
| 677 | source: [props.workspaceId, props.source.id], | 679 | submitLabel="Save Changes" |
| 678 | sourceInspection: [props.workspaceId, props.source.id], | 680 | busyLabel="Saving..." |
| 679 | sourceInspectionTool: [props.workspaceId, props.source.id], | 681 | onSubmit={async (config) => { |
| 680 | sourceDiscovery: [props.workspaceId, props.source.id], | 682 | const source = await updateSource({ |
| 681 | }, | 683 | path: { |
| 682 | }); | 684 | workspaceId: props.workspaceId, |
| 683 | 685 | sourceId: props.source.id, | |
| 684 | startTransition(() => { | 686 | }, |
| 685 | void navigation.detail(source.id, { | 687 | payload: config, |
| 686 | tab: "model", | 688 | reactivityKeys: { |
| 687 | }); | 689 | sources: [props.workspaceId], |
| 688 | }); | 690 | source: [props.workspaceId, props.source.id], |
| 689 | }} | 691 | sourceInspection: [props.workspaceId, props.source.id], |
| 690 | /> | 692 | sourceInspectionTool: [props.workspaceId, props.source.id], |
| 693 | sourceDiscovery: [props.workspaceId, props.source.id], | ||
| 694 | }, | ||
| 695 | }); | ||
| 696 | |||
| 697 | startTransition(() => { | ||
| 698 | void navigation.detail(source.id, { | ||
| 699 | tab: "model", | ||
| 700 | }); | ||
| 701 | }); | ||
| 702 | }} | ||
| 703 | /> | ||
| 704 | </div> | ||
| 705 | </div> | ||
| 691 | ); | 706 | ); |
| 692 | } | 707 | } |
| 693 | 708 | ||
| @@ -1,4 +1,7 @@ | |||
| 1 | import { defineExecutorFrontendPlugin } from "@executor/react/plugins"; | 1 | import { |
| 2 | defineExecutorFrontendPlugin, | ||
| 3 | liveFrontendPluginComponent, | ||
| 4 | } from "@executor/react/plugins"; | ||
| 2 | 5 | ||
| 3 | import { | 6 | import { |
| 4 | OpenApiAddSourcePage, | 7 | OpenApiAddSourcePage, |
| @@ -13,17 +16,17 @@ export const OpenApiReactPlugin = defineExecutorFrontendPlugin({ · 8 unmodified lines | |||
| 13 | { | 16 | { |
| 14 | key: "add", | 17 | key: "add", |
| 15 | path: "add", | 18 | path: "add", |
| 16 | component: OpenApiAddSourcePage, | 19 | component: liveFrontendPluginComponent(() => OpenApiAddSourcePage), |
| 17 | }, | 20 | }, |
| 18 | { | 21 | { |
| 19 | key: "detail", | 22 | key: "detail", |
| 20 | path: "sources/$sourceId", | 23 | path: "sources/$sourceId", |
| 21 | component: OpenApiDetailRoute, | 24 | component: liveFrontendPluginComponent(() => OpenApiDetailRoute), |
| 22 | }, | 25 | }, |
| 23 | { | 26 | { |
| 24 | key: "edit", | 27 | key: "edit", |
| 25 | path: "sources/$sourceId/edit", | 28 | path: "sources/$sourceId/edit", |
| 26 | component: OpenApiEditRoute, | 29 | component: liveFrontendPluginComponent(() => OpenApiEditRoute), |
| 27 | }, | 30 | }, |
| 28 | ], | 31 | ], |
| 29 | }); | 32 | }); |