941 lines · 27.1 KB · TypeScript
RawDownload
import type {  Source,  SourceInspectionToolDetail,} from "@executor/react";import {  Result,  SecretReferenceField,  defineExecutorPluginHttpApiClient,  useAtomValue,  useAtomSet,  useExecutorMutation,  useSource,  useWorkspaceRequestContext,} from "@executor/react";import {  Alert,  Button,  DocumentPanel,  IconPencil,  Input,  Label,  Select,  SourceToolDetailPanel,  SourceToolExplorer,  parseSourceToolExplorerSearch,  type SourceToolExplorerSearch,  useSourcePluginNavigation,  useSourcePluginRouteParams,  useSourcePluginSearch,} from "@executor/react/plugins";import {  openApiHttpApiExtension,} from "@executor/plugin-openapi-http";import type {  OpenApiConnectInput,  OpenApiPreviewRequest,  OpenApiPreviewSecurityScheme,  OpenApiPreviewResponse,  OpenApiSourceConfigPayload,} from "@executor/plugin-openapi-shared";import { startTransition, useEffect, useState, type ReactNode } from "react"; type RouteToolSearch = SourceToolExplorerSearch; const defaultOpenApiInput = (): OpenApiConnectInput => ({  name: "My OpenAPI Source",  specUrl: "https://example.com/openapi.json",  baseUrl: null,  auth: {    kind: "none",  },}); const DEFAULT_BEARER_HEADER_NAME = "Authorization";const DEFAULT_BEARER_PREFIX = "Bearer "; const presetString = (  search: Record<string, unknown>,  key: string,): string | null => {  const value = search[key];  return typeof value === "string" && value.trim().length > 0    ? value.trim()    : null;}; const firstSearchString = (  search: Record<string, unknown>,  keys: ReadonlyArray<string>,): string | null => {  for (const key of keys) {    const value = presetString(search, key);    if (value) {      return value;    }  }   return null;}; const looksLikeOpenApiSpecUrl = (value: string): boolean => {  const lower = value.trim().toLowerCase();  if (!/^https?:\/\//.test(lower)) {    return false;  }   return (    lower.endsWith(".json")    || lower.endsWith(".yaml")    || lower.endsWith(".yml")    || lower.includes("/openapi")    || lower.includes("openapi.")    || lower.includes("/swagger")    || lower.includes("swagger.")    || lower.includes("/api-docs")  );}; const inferOpenApiUrls = (value: string): {  specUrl: string;  baseUrl: string | null;} => {  const trimmed = value.trim();  if (!trimmed) {    return {      specUrl: defaultOpenApiInput().specUrl,      baseUrl: defaultOpenApiInput().baseUrl,    };  }   if (looksLikeOpenApiSpecUrl(trimmed)) {    return {      specUrl: trimmed,      baseUrl: null,    };  }   const normalizedBaseUrl = trimmed.replace(/\/+$/, "");  return {    specUrl: `${normalizedBaseUrl}/openapi.json`,    baseUrl: normalizedBaseUrl,  };}; const openApiInputFromSearch = (  search: Record<string, unknown>,): OpenApiConnectInput => {  const defaults = defaultOpenApiInput();  const genericUrl = firstSearchString(search, ["inputUrl", "pastedUrl", "url"]);  const inferred = genericUrl ? inferOpenApiUrls(genericUrl) : defaults;   return {    ...defaults,    name: presetString(search, "presetName") ?? defaults.name,    specUrl: presetString(search, "presetSpecUrl") ?? inferred.specUrl,    baseUrl: presetString(search, "presetBaseUrl") ?? inferred.baseUrl,  };}; const getOpenApiHttpClient = defineExecutorPluginHttpApiClient<"OpenApiReactHttpClient">()(  "OpenApiReactHttpClient",  [openApiHttpApiExtension] as const,);  const Section = (props: {  title: string;  children: ReactNode;}) => (  <section>    <h2 className="mb-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">{props.title}</h2>    {props.children}  </section>); const isPreviewableSpecUrl = (value: string): boolean => {  const trimmed = value.trim();  if (!/^https?:\/\//i.test(trimmed)) {    return false;  }   try {    const url = new URL(trimmed);    return url.hostname !== "example.com";  } catch {    return false;  }}; const previewSecuritySchemeLabel = (scheme: OpenApiPreviewSecurityScheme): string => {  if (scheme.kind === "apiKey") {    return scheme.placement ? `API key in ${scheme.placement}` : "API key";  }   if (scheme.kind === "http") {    return scheme.scheme ? `HTTP ${scheme.scheme}` : "HTTP auth";  }   if (scheme.kind === "oauth2") {    return "OAuth 2.0";  }   if (scheme.kind === "openIdConnect") {    return "OpenID Connect";  }   return "Custom auth";}; const inputFromConfig = (  config: OpenApiSourceConfigPayload,): OpenApiConnectInput => ({  name: config.name,  specUrl: config.specUrl,  baseUrl: config.baseUrl,  auth: config.auth,}); const secretValue = (input: OpenApiConnectInput["auth"]): string =>  input.kind === "bearer"    ? JSON.stringify(input.tokenSecretRef)    : ""; const bearerHeaderNameValue = (input: OpenApiConnectInput["auth"]): string =>  input.kind === "bearer"    ? input.headerName?.trim() || DEFAULT_BEARER_HEADER_NAME    : DEFAULT_BEARER_HEADER_NAME; const bearerPrefixValue = (input: OpenApiConnectInput["auth"]): string =>  input.kind === "bearer"    ? input.prefix ?? DEFAULT_BEARER_PREFIX    : DEFAULT_BEARER_PREFIX; const trimToNull = (value: string): string | null => {  const trimmed = value.trim();  return trimmed.length > 0 ? trimmed : null;}; const authFromSecretValue = (  authKind: OpenApiConnectInput["auth"]["kind"],  value: string,  headerName: string,  prefix: string,): OpenApiConnectInput["auth"] => {  if (authKind === "none") {    return {      kind: "none",    };  }   if (!value) {    throw new Error("Select a secret for bearer auth.");  }   return {    kind: "bearer",    tokenSecretRef: JSON.parse(value) as OpenApiConnectInput["auth"] & { tokenSecretRef: never }["tokenSecretRef"],    headerName: trimToNull(headerName),    prefix: prefix === DEFAULT_BEARER_PREFIX ? null : prefix,  };}; function OpenApiSourceForm(props: {  mode: "create" | "edit";  workspaceId: Source["scopeId"];  initialValue: OpenApiConnectInput;  submitLabel: string;  busyLabel: string;  onSubmit: (input: OpenApiConnectInput) => Promise<void>;}) {  const openApiHttpClient = getOpenApiHttpClient();  const previewDocument = useAtomSet(    openApiHttpClient.mutation("openapi", "previewDocument"),    { mode: "promise" },  );  const [name, setName] = useState(props.initialValue.name);  const [specUrl, setSpecUrl] = useState(props.initialValue.specUrl);  const [baseUrl, setBaseUrl] = useState(props.initialValue.baseUrl ?? "");  const [authKind, setAuthKind] = useState<OpenApiConnectInput["auth"]["kind"]>(    props.initialValue.auth.kind,  );  const [tokenSecretRef, setTokenSecretRef] = useState(    secretValue(props.initialValue.auth),  );  const [authHeaderName, setAuthHeaderName] = useState(    bearerHeaderNameValue(props.initialValue.auth),  );  const [authPrefix, setAuthPrefix] = useState(    bearerPrefixValue(props.initialValue.auth),  );  const [error, setError] = useState<string | null>(null);  const [preview, setPreview] = useState<OpenApiPreviewResponse | null>(null);  const [nameEdited, setNameEdited] = useState(false);  const [baseUrlEdited, setBaseUrlEdited] = useState(false);  const [lastPreviewedSpecUrl, setLastPreviewedSpecUrl] = useState<string | null>(null);  const previewMutation = useExecutorMutation<    OpenApiPreviewRequest,    OpenApiPreviewResponse  >(async (payload) =>    previewDocument({      path: { workspaceId: props.workspaceId },      payload,    })  );  const submitMutation = useExecutorMutation<OpenApiConnectInput, void>(props.onSubmit);   const runPreview = async (input: {    mode: "auto" | "manual";  }) => {    const trimmedSpecUrl = specUrl.trim();    if (!trimmedSpecUrl) {      if (input.mode === "manual") {        setError("Spec URL is required.");      }      setPreview(null);      setLastPreviewedSpecUrl(null);      return;    }     if (!isPreviewableSpecUrl(trimmedSpecUrl)) {      return;    }     try {      const result = await previewMutation.mutateAsync({        specUrl: trimmedSpecUrl,      });      setPreview({        ...result,        warnings: [...result.warnings],        securitySchemes: [...result.securitySchemes],      });      setLastPreviewedSpecUrl(trimmedSpecUrl);      if (error) {        setError(null);      }       if (!nameEdited && result.title) {        setName(result.title);      }      if (!baseUrlEdited && result.baseUrl) {        setBaseUrl(result.baseUrl);      }    } catch (cause) {      if (input.mode === "manual") {        setError(cause instanceof Error ? cause.message : "Failed previewing document.");      }      setPreview(null);    }  };   useEffect(() => {    const trimmedSpecUrl = specUrl.trim();    if (!isPreviewableSpecUrl(trimmedSpecUrl)) {      return;    }     if (trimmedSpecUrl === lastPreviewedSpecUrl) {      return;    }     const timeout = window.setTimeout(() => {      void runPreview({ mode: "auto" });    }, 450);     return () => {      window.clearTimeout(timeout);    };  }, [specUrl, lastPreviewedSpecUrl]);   const handleSubmit = async () => {    setError(null);    const trimmedName = name.trim();    const trimmedSpecUrl = specUrl.trim();    const trimmedBaseUrl = baseUrl.trim();     if (!trimmedName) {      setError("Name is required.");      return;    }    if (!trimmedSpecUrl) {      setError("Spec URL is required.");      return;    }    if (authKind === "bearer" && tokenSecretRef.trim().length === 0) {      setError("Select a secret for bearer auth.");      return;    }     try {      const auth = authFromSecretValue(        authKind,        tokenSecretRef,        authHeaderName,        authPrefix,      );      await submitMutation.mutateAsync({        name: trimmedName,        specUrl: trimmedSpecUrl,        baseUrl: trimmedBaseUrl || null,        auth,      });    } catch (cause) {      setError(cause instanceof Error ? cause.message : "Failed saving source.");    }  };   return (    <div className="space-y-6 rounded-lg border border-border bg-card p-6 text-sm ring-1 ring-foreground/[0.04]">      <Section title="Connection">        <div className="grid gap-4">          <div className="grid gap-2">            <Label>Name</Label>            <Input              value={name}              onChange={(event) => {                setNameEdited(true);                setName(event.target.value);              }}              placeholder="GitHub REST"            />          </div>           <div className="grid gap-2">            <Label>Spec URL</Label>            <Input              value={specUrl}              onChange={(event) => setSpecUrl(event.target.value)}              onBlur={() => {                void runPreview({ mode: "manual" });              }}              placeholder="https://example.com/openapi.json"              className="font-mono text-xs"            />          </div>           <div className="grid gap-2">            <Label>Base URL</Label>            <Input              value={baseUrl}              onChange={(event) => {                setBaseUrlEdited(true);                setBaseUrl(event.target.value);              }}              placeholder="https://api.example.com"              className="font-mono text-xs"            />          </div>           <div className="grid gap-2">            <Label>Auth</Label>            <Select              value={authKind}              onChange={(event) =>                setAuthKind(event.target.value as OpenApiConnectInput["auth"]["kind"])}            >              <option value="none">None</option>              <option value="bearer">Bearer Secret</option>            </Select>          </div>           {authKind === "bearer" && (            <div className="space-y-4 rounded-lg border border-border bg-muted/20 p-4">              <SecretReferenceField                label="Secret"                value={tokenSecretRef}                emptyLabel="Select a secret"                draftNamePlaceholder="OpenAPI bearer token"                draftValuePlaceholder="Paste the token value"                onChange={setTokenSecretRef}              />               <div className="grid gap-4 sm:grid-cols-2">                <div className="grid gap-2">                  <Label>Header Name</Label>                  <Input                    value={authHeaderName}                    onChange={(event) => setAuthHeaderName(event.target.value)}                    placeholder={DEFAULT_BEARER_HEADER_NAME}                    className="font-mono text-xs"                  />                </div>                 <div className="grid gap-2">                  <Label>Prefix</Label>                  <Input                    value={authPrefix}                    onChange={(event) => setAuthPrefix(event.target.value)}                    placeholder={DEFAULT_BEARER_PREFIX}                    className="font-mono text-xs"                  />                </div>              </div>            </div>          )}        </div>      </Section>       {(preview || previewMutation.status === "pending") && (        <Section title="Preview">          {previewMutation.status === "pending" ? (            <div className="text-xs text-muted-foreground">Loading preview...</div>          ) : preview && (            <div className="rounded-lg border border-border bg-muted/30 px-4 py-3 text-sm">              <dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5 text-xs">                <dt className="font-medium text-foreground">Title</dt>                <dd className="text-muted-foreground">{preview.title ?? "—"}</dd>                <dt className="font-medium text-foreground">Version</dt>                <dd className="text-muted-foreground">{preview.version ?? "—"}</dd>                <dt className="font-medium text-foreground">Operations</dt>                <dd className="text-muted-foreground">{preview.operationCount}</dd>                <dt className="font-medium text-foreground">Base URL</dt>                <dd className="font-mono text-muted-foreground">{preview.baseUrl ?? "—"}</dd>                {preview.namespace && (                  <>                    <dt className="font-medium text-foreground">Namespace</dt>                    <dd className="text-muted-foreground">{preview.namespace}</dd>                  </>                )}                {preview.securitySchemes.length > 0 && (

Lines 1–500 of 941.