import { useState } from "react";import { useAtomValue, useAtomSet } from "@effect/atom-react";import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";import { graphqlSourceAtom, updateGraphqlSource } from "./atoms";import { useScope } from "@executor-js/react/api/scope-context";import { sourceWriteKeys } from "@executor-js/react/api/reactivity-keys";import { useSecretPickerSecrets } from "@executor-js/react/plugins/use-secret-picker-secrets";import { HttpCredentialsEditor, httpCredentialsFromValues, serializeHttpCredentials, type HttpCredentialsState,} from "@executor-js/react/plugins/http-credentials";import { SourceIdentityFields, useSourceIdentity,} from "@executor-js/react/plugins/source-identity";import { Button } from "@executor-js/react/components/button";import { FilterTabs } from "@executor-js/react/components/filter-tabs";import { CardStack, CardStackContent, CardStackEntryField,} from "@executor-js/react/components/card-stack";import { Input } from "@executor-js/react/components/input";import { Badge } from "@executor-js/react/components/badge";import type { HeaderValue } from "../sdk/types";import type { StoredGraphqlSource } from "../sdk/store"; // UI only needs the fields the API exposes; `scope` on the SDK interface// isn't part of the HTTP response.type EditableSource = Omit<StoredGraphqlSource, "scope">;type AuthMode = "none" | "oauth2"; const graphqlOAuthConnectionId = (namespaceSlug: string): string => `graphql-oauth2-${namespaceSlug || "default"}`; // ---------------------------------------------------------------------------// Edit form// --------------------------------------------------------------------------- function EditForm(props: { sourceId: string; initial: EditableSource; onSave: () => void }) { const scopeId = useScope(); const doUpdate = useAtomSet(updateGraphqlSource, { mode: "promise" }); const secretList = useSecretPickerSecrets(); const identity = useSourceIdentity({ fallbackName: props.initial.name, fallbackNamespace: props.initial.namespace, }); const [endpoint, setEndpoint] = useState(props.initial.endpoint); const [credentials, setCredentials] = useState<HttpCredentialsState>(() => httpCredentialsFromValues({ headers: props.initial.headers, queryParams: props.initial.queryParams, }), ); const [authMode, setAuthMode] = useState<AuthMode>(props.initial.auth.kind); const [saving, setSaving] = useState(false); const [error, setError] = useState<string | null>(null); const [dirty, setDirty] = useState(false); const identityDirty = identity.name.trim() !== props.initial.name.trim(); const handleCredentialsChange = (next: HttpCredentialsState) => { setCredentials(next); setDirty(true); }; const handleSave = async () => { setSaving(true); setError(null); const { headers, queryParams } = serializeHttpCredentials(credentials); try { await doUpdate({ params: { scopeId, namespace: props.sourceId }, payload: { name: identity.name.trim() || undefined, endpoint: endpoint.trim() || undefined, headers, queryParams: queryParams as Record<string, HeaderValue>, auth: authMode === "oauth2" ? { kind: "oauth2", connectionId: props.initial.auth.kind === "oauth2" ? props.initial.auth.connectionId : graphqlOAuthConnectionId(props.initial.namespace), } : { kind: "none" }, }, reactivityKeys: sourceWriteKeys, }); setDirty(false); props.onSave(); } catch (e) { setError(e instanceof Error ? e.message : "Failed to update source"); } finally { setSaving(false); } }; return ( <div className="space-y-6"> <div> <h1 className="text-xl font-semibold text-foreground">Edit GraphQL Source</h1> <p className="mt-1 text-sm text-muted-foreground"> Update the endpoint and authentication headers for this source. </p> </div> <div className="flex items-center gap-3 rounded-lg border border-border bg-card px-4 py-3"> <div className="min-w-0 flex-1"> <p className="truncate text-sm font-semibold text-card-foreground">{props.sourceId}</p> </div> <Badge variant="secondary" className="text-xs"> GraphQL </Badge> </div> <SourceIdentityFields identity={identity} namespaceReadOnly /> <CardStack> <CardStackContent className="border-t-0"> <CardStackEntryField label="Endpoint"> <Input value={endpoint} onChange={(e) => { setEndpoint((e.target as HTMLInputElement).value); setDirty(true); }} placeholder="https://api.example.com/graphql" className="font-mono text-sm" /> </CardStackEntryField> </CardStackContent> </CardStack> <HttpCredentialsEditor credentials={credentials} onChange={handleCredentialsChange} existingSecrets={secretList} sourceName={identity.name} targetScope={scopeId} /> <section className="space-y-2.5"> <div className="flex items-center justify-between gap-3"> <span className="text-sm font-medium text-foreground">Authentication</span> <FilterTabs<AuthMode> tabs={[ { value: "none", label: "None" }, { value: "oauth2", label: "OAuth" }, ]} value={authMode} onChange={(value) => { setAuthMode(value); setDirty(true); }} /> </div> {authMode === "oauth2" && ( <p className="text-xs text-muted-foreground"> OAuth sign-in is available from the source header after saving. </p> )} </section> {error && ( <div className="rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2"> <p className="text-sm text-destructive">{error}</p> </div> )} <div className="flex items-center justify-between border-t border-border pt-4"> <Button variant="ghost" onClick={props.onSave}> Cancel </Button> <Button onClick={handleSave} disabled={(!dirty && !identityDirty) || saving}> {saving ? "Saving…" : "Save changes"} </Button> </div> </div> );} // ---------------------------------------------------------------------------// Main component// --------------------------------------------------------------------------- export default function EditGraphqlSource(props: { sourceId: string; onSave: () => void }) { const scopeId = useScope(); const sourceResult = useAtomValue(graphqlSourceAtom(scopeId, props.sourceId)); if (!AsyncResult.isSuccess(sourceResult) || !sourceResult.value) { return ( <div className="space-y-6"> <div> <h1 className="text-xl font-semibold text-foreground">Edit GraphQL Source</h1> <p className="mt-1 text-sm text-muted-foreground">Loading configuration…</p> </div> </div> ); } return ( <EditForm sourceId={props.sourceId} initial={sourceResult.value as EditableSource} onSave={props.onSave} /> );}
import { useState } from "react";
import { useAtomValue, useAtomSet } from "@effect/atom-react";
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
import { graphqlSourceAtom, updateGraphqlSource } from "./atoms";
import { useScope } from "@executor-js/react/api/scope-context";
import { sourceWriteKeys } from "@executor-js/react/api/reactivity-keys";
import { useSecretPickerSecrets } from "@executor-js/react/plugins/use-secret-picker-secrets";
import {
HttpCredentialsEditor,
httpCredentialsFromValues,
serializeHttpCredentials,
type HttpCredentialsState,
} from "@executor-js/react/plugins/http-credentials";
import {
SourceIdentityFields,
useSourceIdentity,
} from "@executor-js/react/plugins/source-identity";
import { Button } from "@executor-js/react/components/button";
import { FilterTabs } from "@executor-js/react/components/filter-tabs";
import {
CardStack,
CardStackContent,
CardStackEntryField,
} from "@executor-js/react/components/card-stack";
import { Input } from "@executor-js/react/components/input";
import { Badge } from "@executor-js/react/components/badge";
import type { HeaderValue } from "../sdk/types";
import type { StoredGraphqlSource } from "../sdk/store";
// UI only needs the fields the API exposes; `scope` on the SDK interface
// isn't part of the HTTP response.
type EditableSource = Omit<StoredGraphqlSource, "scope">;
type AuthMode = "none" | "oauth2";
const graphqlOAuthConnectionId = (namespaceSlug: string): string =>
`graphql-oauth2-${namespaceSlug || "default"}`;
// ---------------------------------------------------------------------------
// Edit form
// ---------------------------------------------------------------------------
function EditForm(props: { sourceId: string; initial: EditableSource; onSave: () => void }) {
const scopeId = useScope();
const doUpdate = useAtomSet(updateGraphqlSource, { mode: "promise" });
const secretList = useSecretPickerSecrets();
const identity = useSourceIdentity({
fallbackName: props.initial.name,
fallbackNamespace: props.initial.namespace,
});
const [endpoint, setEndpoint] = useState(props.initial.endpoint);
const [credentials, setCredentials] = useState<HttpCredentialsState>(() =>
httpCredentialsFromValues({
headers: props.initial.headers,
queryParams: props.initial.queryParams,
}),
);
const [authMode, setAuthMode] = useState<AuthMode>(props.initial.auth.kind);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [dirty, setDirty] = useState(false);
const identityDirty = identity.name.trim() !== props.initial.name.trim();
const handleCredentialsChange = (next: HttpCredentialsState) => {
setCredentials(next);
setDirty(true);
};
const handleSave = async () => {
setSaving(true);
setError(null);
const { headers, queryParams } = serializeHttpCredentials(credentials);
try {
await doUpdate({
params: { scopeId, namespace: props.sourceId },
payload: {
name: identity.name.trim() || undefined,
endpoint: endpoint.trim() || undefined,
headers,
queryParams: queryParams as Record<string, HeaderValue>,
auth:
authMode === "oauth2"
? {
kind: "oauth2",
connectionId:
props.initial.auth.kind === "oauth2"
? props.initial.auth.connectionId
: graphqlOAuthConnectionId(props.initial.namespace),
}
: { kind: "none" },
},
reactivityKeys: sourceWriteKeys,
});
setDirty(false);
props.onSave();
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to update source");
} finally {
setSaving(false);
}
};
return (
<div className="space-y-6">
<div>
<h1 className="text-xl font-semibold text-foreground">Edit GraphQL Source</h1>
<p className="mt-1 text-sm text-muted-foreground">
Update the endpoint and authentication headers for this source.
</p>
</div>
<div className="flex items-center gap-3 rounded-lg border border-border bg-card px-4 py-3">
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-semibold text-card-foreground">{props.sourceId}</p>
</div>
<Badge variant="secondary" className="text-xs">
GraphQL
</Badge>
</div>
<SourceIdentityFields identity={identity} namespaceReadOnly />
<CardStack>
<CardStackContent className="border-t-0">
<CardStackEntryField label="Endpoint">
<Input
value={endpoint}
onChange={(e) => {
setEndpoint((e.target as HTMLInputElement).value);
setDirty(true);
}}
placeholder="https://api.example.com/graphql"
className="font-mono text-sm"
/>
</CardStackEntryField>
</CardStackContent>
</CardStack>
<HttpCredentialsEditor
credentials={credentials}
onChange={handleCredentialsChange}
existingSecrets={secretList}
sourceName={identity.name}
targetScope={scopeId}
/>
<section className="space-y-2.5">
<div className="flex items-center justify-between gap-3">
<span className="text-sm font-medium text-foreground">Authentication</span>
<FilterTabs<AuthMode>
tabs={[
{ value: "none", label: "None" },
{ value: "oauth2", label: "OAuth" },
]}
value={authMode}
onChange={(value) => {
setAuthMode(value);
setDirty(true);
}}
/>
</div>
{authMode === "oauth2" && (
<p className="text-xs text-muted-foreground">
OAuth sign-in is available from the source header after saving.
</p>
)}
</section>
{error && (
<div className="rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2">
<p className="text-sm text-destructive">{error}</p>
</div>
)}
<div className="flex items-center justify-between border-t border-border pt-4">
<Button variant="ghost" onClick={props.onSave}>
Cancel
</Button>
<Button onClick={handleSave} disabled={(!dirty && !identityDirty) || saving}>
{saving ? "Saving…" : "Save changes"}
</Button>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Main component
// ---------------------------------------------------------------------------
export default function EditGraphqlSource(props: { sourceId: string; onSave: () => void }) {
const scopeId = useScope();
const sourceResult = useAtomValue(graphqlSourceAtom(scopeId, props.sourceId));
if (!AsyncResult.isSuccess(sourceResult) || !sourceResult.value) {
return (
<div className="space-y-6">
<div>
<h1 className="text-xl font-semibold text-foreground">Edit GraphQL Source</h1>
<p className="mt-1 text-sm text-muted-foreground">Loading configuration…</p>
</div>
</div>
);
}
return (
<EditForm
sourceId={props.sourceId}
initial={sourceResult.value as EditableSource}
onSave={props.onSave}
/>
);
}