import { useState } from "react";import { useAtomSet } from "@effect-atom/atom-react"; import { useScope } from "@executor/react/api/scope-context";import { HeadersList } from "@executor/react/plugins/headers-list";import { type HeaderState } from "@executor/react/plugins/secret-header-auth";import { displayNameFromUrl, SourceIdentityFields, useSourceIdentity,} from "@executor/react/plugins/source-identity";import { useSecretPickerSecrets } from "@executor/react/plugins/use-secret-picker-secrets";import { Button } from "@executor/react/components/button";import { CardStack, CardStackContent, CardStackEntryField,} from "@executor/react/components/card-stack";import { FieldLabel } from "@executor/react/components/field";import { FloatActions } from "@executor/react/components/float-actions";import { Input } from "@executor/react/components/input";import { Spinner } from "@executor/react/components/spinner";import { addGraphqlSource } from "./atoms";import type { HeaderValue } from "../sdk/types"; const initialHeader = (): HeaderState => ({ name: "Authorization", prefix: "Bearer ", presetKey: "bearer", secretId: null,}); export default function AddGraphqlSource(props: { onComplete: () => void; onCancel: () => void; initialUrl?: string;}) { const [endpoint, setEndpoint] = useState(props.initialUrl ?? ""); const identity = useSourceIdentity({ fallbackName: displayNameFromUrl(endpoint) ?? "", }); const [headers, setHeaders] = useState<HeaderState[]>([initialHeader()]); const [adding, setAdding] = useState(false); const [addError, setAddError] = useState<string | null>(null); const scopeId = useScope(); const doAdd = useAtomSet(addGraphqlSource, { mode: "promise" }); const secretList = useSecretPickerSecrets(); const headersValid = headers.every((header) => header.name.trim() && header.secretId); const canAdd = endpoint.trim().length > 0 && (headers.length === 0 || headersValid); const handleAdd = async () => { setAdding(true); setAddError(null); try { const headerMap: Record<string, HeaderValue> = {}; for (const header of headers) { const name = header.name.trim(); if (name && header.secretId) { headerMap[name] = { secretId: header.secretId, ...(header.prefix ? { prefix: header.prefix } : {}), }; } } await doAdd({ path: { scopeId }, payload: { endpoint: endpoint.trim(), name: identity.name.trim() || undefined, namespace: identity.namespace.trim() || undefined, ...(Object.keys(headerMap).length > 0 ? { headers: headerMap } : {}), }, }); props.onComplete(); } catch (e) { setAddError(e instanceof Error ? e.message : "Failed to add source"); setAdding(false); } }; return ( <div className="flex flex-1 flex-col gap-6"> <h1 className="text-xl font-semibold text-foreground">Add GraphQL Source</h1> <CardStack> <CardStackContent className="border-t-0"> <CardStackEntryField label="Endpoint" hint="The endpoint will be introspected to discover available queries and mutations." > <Input value={endpoint} onChange={(e) => setEndpoint((e.target as HTMLInputElement).value)} placeholder="https://api.example.com/graphql" className="font-mono text-sm" /> </CardStackEntryField> </CardStackContent> </CardStack> <SourceIdentityFields identity={identity} namePlaceholder="e.g. Shopify API" /> <section className="space-y-2.5"> <FieldLabel>Headers</FieldLabel> <HeadersList headers={headers} onHeadersChange={setHeaders} existingSecrets={secretList} /> </section> {/* Error */} {addError && ( <div className="rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2"> <p className="text-[12px] text-destructive">{addError}</p> </div> )} <FloatActions> <Button variant="ghost" onClick={props.onCancel} disabled={adding}> Cancel </Button> <Button onClick={handleAdd} disabled={!canAdd || adding}> {adding && <Spinner className="size-3.5" />} {adding ? "Adding..." : "Add source"} </Button> </FloatActions> </div> );}
import { useState } from "react";
import { useAtomSet } from "@effect-atom/atom-react";
import { useScope } from "@executor/react/api/scope-context";
import { HeadersList } from "@executor/react/plugins/headers-list";
import { type HeaderState } from "@executor/react/plugins/secret-header-auth";
import {
displayNameFromUrl,
SourceIdentityFields,
useSourceIdentity,
} from "@executor/react/plugins/source-identity";
import { useSecretPickerSecrets } from "@executor/react/plugins/use-secret-picker-secrets";
import { Button } from "@executor/react/components/button";
import {
CardStack,
CardStackContent,
CardStackEntryField,
} from "@executor/react/components/card-stack";
import { FieldLabel } from "@executor/react/components/field";
import { FloatActions } from "@executor/react/components/float-actions";
import { Input } from "@executor/react/components/input";
import { Spinner } from "@executor/react/components/spinner";
import { addGraphqlSource } from "./atoms";
import type { HeaderValue } from "../sdk/types";
const initialHeader = (): HeaderState => ({
name: "Authorization",
prefix: "Bearer ",
presetKey: "bearer",
secretId: null,
});
export default function AddGraphqlSource(props: {
onComplete: () => void;
onCancel: () => void;
initialUrl?: string;
}) {
const [endpoint, setEndpoint] = useState(props.initialUrl ?? "");
const identity = useSourceIdentity({
fallbackName: displayNameFromUrl(endpoint) ?? "",
});
const [headers, setHeaders] = useState<HeaderState[]>([initialHeader()]);
const [adding, setAdding] = useState(false);
const [addError, setAddError] = useState<string | null>(null);
const scopeId = useScope();
const doAdd = useAtomSet(addGraphqlSource, { mode: "promise" });
const secretList = useSecretPickerSecrets();
const headersValid = headers.every((header) => header.name.trim() && header.secretId);
const canAdd = endpoint.trim().length > 0 && (headers.length === 0 || headersValid);
const handleAdd = async () => {
setAdding(true);
setAddError(null);
try {
const headerMap: Record<string, HeaderValue> = {};
for (const header of headers) {
const name = header.name.trim();
if (name && header.secretId) {
headerMap[name] = {
secretId: header.secretId,
...(header.prefix ? { prefix: header.prefix } : {}),
};
}
}
await doAdd({
path: { scopeId },
payload: {
endpoint: endpoint.trim(),
name: identity.name.trim() || undefined,
namespace: identity.namespace.trim() || undefined,
...(Object.keys(headerMap).length > 0 ? { headers: headerMap } : {}),
},
});
props.onComplete();
} catch (e) {
setAddError(e instanceof Error ? e.message : "Failed to add source");
setAdding(false);
}
};
return (
<div className="flex flex-1 flex-col gap-6">
<h1 className="text-xl font-semibold text-foreground">Add GraphQL Source</h1>
<CardStack>
<CardStackContent className="border-t-0">
<CardStackEntryField
label="Endpoint"
hint="The endpoint will be introspected to discover available queries and mutations."
>
<Input
value={endpoint}
onChange={(e) => setEndpoint((e.target as HTMLInputElement).value)}
placeholder="https://api.example.com/graphql"
className="font-mono text-sm"
/>
</CardStackEntryField>
</CardStackContent>
</CardStack>
<SourceIdentityFields
identity={identity}
namePlaceholder="e.g. Shopify API"
/>
<section className="space-y-2.5">
<FieldLabel>Headers</FieldLabel>
<HeadersList
headers={headers}
onHeadersChange={setHeaders}
existingSecrets={secretList}
/>
</section>
{/* Error */}
{addError && (
<div className="rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2">
<p className="text-[12px] text-destructive">{addError}</p>
</div>
)}
<FloatActions>
<Button variant="ghost" onClick={props.onCancel} disabled={adding}>
Cancel
</Button>
<Button onClick={handleAdd} disabled={!canAdd || adding}>
{adding && <Spinner className="size-3.5" />}
{adding ? "Adding..." : "Add source"}
</Button>
</FloatActions>
</div>
);
}