Skip to content

Commit f63b33e

Browse files
committed
Share HTTP source credential state
1 parent 6dec2e1 commit f63b33e

8 files changed

Lines changed: 241 additions & 198 deletions

File tree

packages/plugins/graphql/src/react/AddGraphqlSource.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { sourceWriteKeys } from "@executor-js/react/api/reactivity-keys";
99
import {
1010
HttpCredentials,
1111
httpCredentialsValid,
12+
nonEmptyHttpCredentialFields,
1213
serializeScopedHttpCredentials,
1314
serializeHttpCredentials,
1415
type HttpCredentialsState,
@@ -102,12 +103,11 @@ export default function AddGraphqlSource(props: {
102103
if (!endpoint.trim() || !httpCredentialsValid(credentials)) return;
103104
setAddError(null);
104105
const { trimmedEndpoint, namespace, displayName } = sourceIdentity();
105-
const { headers, queryParams } = serializeHttpCredentials(credentials);
106+
const requestCredentials = nonEmptyHttpCredentialFields(serializeHttpCredentials(credentials));
106107
await oauth.start({
107108
payload: {
108109
endpoint: trimmedEndpoint,
109-
...(Object.keys(headers).length > 0 ? { headers } : {}),
110-
...(Object.keys(queryParams).length > 0 ? { queryParams } : {}),
110+
...requestCredentials,
111111
redirectUrl: oauthCallbackUrl(),
112112
connectionId: oauthConnectionId({ pluginId: "graphql", namespace }),
113113
tokenScope: oauthCredentialTargetScope,
@@ -133,6 +133,10 @@ export default function AddGraphqlSource(props: {
133133
credentials,
134134
requestCredentialTargetScope,
135135
);
136+
const requestCredentials = nonEmptyHttpCredentialFields({
137+
headers: headerMap as Record<string, GraphqlCredentialInput>,
138+
queryParams: queryParams as Record<string, GraphqlCredentialInput>,
139+
});
136140

137141
const { trimmedEndpoint, namespace, displayName } = sourceIdentity();
138142
const exit = await doAdd({
@@ -142,12 +146,7 @@ export default function AddGraphqlSource(props: {
142146
endpoint: trimmedEndpoint,
143147
name: displayName,
144148
namespace,
145-
...(Object.keys(headerMap).length > 0 ? { headers: headerMap } : {}),
146-
...(Object.keys(queryParams).length > 0
147-
? {
148-
queryParams: queryParams as Record<string, GraphqlCredentialInput>,
149-
}
150-
: {}),
149+
...requestCredentials,
151150
credentialTargetScope:
152151
authMode === "oauth2" && tokens
153152
? oauthCredentialTargetScope

packages/plugins/graphql/src/react/EditGraphqlSource.tsx

Lines changed: 41 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,24 @@ import { useAtomValue, useAtomSet } from "@effect/atom-react";
33
import * as Exit from "effect/Exit";
44
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
55
import { graphqlSourceAtom, updateGraphqlSource } from "./atoms";
6-
import {
7-
connectionsAtom,
8-
setSourceCredentialBinding,
9-
sourceCredentialBindingsAtom,
10-
} from "@executor-js/react/api/atoms";
11-
import { useScope, useScopeStack } from "@executor-js/react/api/scope-context";
12-
import { connectionWriteKeys, sourceWriteKeys } from "@executor-js/react/api/reactivity-keys";
6+
import { sourceCredentialBindingsAtom } from "@executor-js/react/api/atoms";
7+
import { useScope } from "@executor-js/react/api/scope-context";
8+
import { sourceWriteKeys } from "@executor-js/react/api/reactivity-keys";
139
import { useSecretPickerSecrets } from "@executor-js/react/plugins/use-secret-picker-secrets";
14-
import {
15-
HttpCredentials,
16-
serializeHttpCredentials,
17-
serializeScopedHttpCredentials,
18-
type HttpCredentialsState,
19-
} from "@executor-js/react/plugins/http-credentials";
20-
import {
21-
effectiveCredentialBindingForScope,
22-
httpCredentialsFromConfiguredCredentialBindings,
23-
initialCredentialTargetScope,
24-
} from "@executor-js/react/plugins/credential-bindings";
10+
import { HttpCredentials } from "@executor-js/react/plugins/http-credentials";
11+
import { useHttpCredentialEditor } from "@executor-js/react/plugins/http-credential-state";
12+
import { initialCredentialTargetScope } from "@executor-js/react/plugins/credential-bindings";
2513
import { slugifyNamespace, useSourceIdentity } from "@executor-js/react/plugins/source-identity";
2614
import { useCredentialTargetScope } from "@executor-js/react/plugins/credential-target-scope";
2715
import { Button } from "@executor-js/react/components/button";
2816
import { FilterTabs } from "@executor-js/react/components/filter-tabs";
29-
import { SourceOAuthConnectionControl } from "@executor-js/react/plugins/source-oauth-connection";
17+
import {
18+
SourceOAuthConnectionControl,
19+
useSourceOAuthConnectionBinding,
20+
} from "@executor-js/react/plugins/source-oauth-connection";
3021
import { Badge } from "@executor-js/react/components/badge";
3122
import { ScopeId } from "@executor-js/sdk/core";
32-
import type { CredentialBindingRef } from "@executor-js/sdk/core";
23+
import type { SourceCredentialBindingRef } from "@executor-js/react/plugins/credential-bindings";
3324
import { GraphqlSourceFields } from "./GraphqlSourceFields";
3425
import { GRAPHQL_OAUTH_CONNECTION_SLOT, type GraphqlCredentialInput } from "../sdk/types";
3526
import type { StoredGraphqlSource } from "../sdk/store";
@@ -44,15 +35,16 @@ type AuthMode = "none" | "oauth2";
4435
function EditForm(props: {
4536
sourceId: string;
4637
initial: EditableSource;
47-
bindings: readonly CredentialBindingRef[];
38+
bindings: readonly SourceCredentialBindingRef[];
4839
onSave: () => void;
4940
}) {
5041
const displayScope = useScope();
51-
const scopeStack = useScopeStack();
5242
const sourceScope = ScopeId.make(props.initial.scope);
53-
const { credentialTargetScope, credentialScopeOptions } = useCredentialTargetScope({
43+
const credentialEditor = useHttpCredentialEditor({
5444
sourceScope,
55-
initialTargetScope: initialCredentialTargetScope(sourceScope, props.bindings),
45+
headers: props.initial.headers,
46+
queryParams: props.initial.queryParams,
47+
bindings: props.bindings,
5648
});
5749
const {
5850
credentialTargetScope: oauthCredentialTargetScope,
@@ -62,61 +54,35 @@ function EditForm(props: {
6254
initialTargetScope: initialCredentialTargetScope(sourceScope, props.bindings),
6355
});
6456
const doUpdate = useAtomSet(updateGraphqlSource, { mode: "promiseExit" });
65-
const setBinding = useAtomSet(setSourceCredentialBinding, { mode: "promise" });
6657
const secretList = useSecretPickerSecrets();
67-
const connectionsResult = useAtomValue(connectionsAtom(displayScope));
6858

6959
const identity = useSourceIdentity({
7060
fallbackName: props.initial.name,
7161
fallbackNamespace: props.initial.namespace,
7262
});
7363
const [endpoint, setEndpoint] = useState(props.initial.endpoint);
74-
const [credentials, setCredentials] = useState<HttpCredentialsState>(() =>
75-
httpCredentialsFromConfiguredCredentialBindings({
76-
headers: props.initial.headers,
77-
queryParams: props.initial.queryParams,
78-
bindings: props.bindings,
79-
}),
80-
);
8164
const [authMode, setAuthMode] = useState<AuthMode>(props.initial.auth.kind);
8265
const [saving, setSaving] = useState(false);
8366
const [error, setError] = useState<string | null>(null);
84-
const [credentialsDirty, setCredentialsDirty] = useState(false);
8567
const [authDirty, setAuthDirty] = useState(false);
8668

8769
const identityDirty = identity.name.trim() !== props.initial.name.trim();
8870
const metadataDirty = identityDirty || endpoint.trim() !== props.initial.endpoint.trim();
89-
const dirty = metadataDirty || credentialsDirty || authDirty;
71+
const dirty = metadataDirty || credentialEditor.credentialsDirty || authDirty;
9072
const oauth2 = props.initial.auth.kind === "oauth2" ? props.initial.auth : null;
91-
const connections = AsyncResult.isSuccess(connectionsResult) ? connectionsResult.value : [];
92-
const scopeRanks = new Map(scopeStack.map((scope, index) => [scope.id, index] as const));
93-
const connectionBinding = oauth2
94-
? effectiveCredentialBindingForScope(
95-
props.bindings,
96-
oauth2.connectionSlot,
97-
oauthCredentialTargetScope,
98-
scopeRanks,
99-
)
100-
: null;
101-
const boundConnectionId =
102-
connectionBinding?.value.kind === "connection" ? connectionBinding.value.connectionId : null;
103-
const isConnected =
104-
boundConnectionId !== null &&
105-
connections.some((connection) => connection.id === boundConnectionId);
106-
const oauthRequestCredentials = serializeHttpCredentials(credentials);
107-
108-
const handleCredentialsChange = (next: HttpCredentialsState) => {
109-
setCredentials(next);
110-
setCredentialsDirty(true);
111-
};
73+
const oauthConnection = useSourceOAuthConnectionBinding({
74+
pluginId: "graphql",
75+
sourceId: props.sourceId,
76+
sourceScope,
77+
slotKey: oauth2?.connectionSlot ?? GRAPHQL_OAUTH_CONNECTION_SLOT,
78+
targetScope: oauthCredentialTargetScope,
79+
bindings: props.bindings,
80+
});
11281

11382
const handleSave = async () => {
11483
setSaving(true);
11584
setError(null);
116-
const { headers, queryParams } = serializeScopedHttpCredentials(
117-
credentials,
118-
credentialTargetScope,
119-
);
85+
const { headers, queryParams } = credentialEditor.serializeScopedCredentials();
12086
const payload: {
12187
sourceScope: ScopeId;
12288
name?: string;
@@ -130,10 +96,10 @@ function EditForm(props: {
13096
name: metadataDirty ? identity.name.trim() || undefined : undefined,
13197
endpoint: metadataDirty ? endpoint.trim() || undefined : undefined,
13298
};
133-
if (credentialsDirty) {
99+
if (credentialEditor.credentialsDirty) {
134100
payload.headers = headers;
135101
payload.queryParams = queryParams as Record<string, GraphqlCredentialInput>;
136-
payload.credentialTargetScope = credentialTargetScope;
102+
payload.credentialTargetScope = credentialEditor.credentialTargetScope;
137103
}
138104
if (authDirty) {
139105
payload.auth =
@@ -146,7 +112,7 @@ function EditForm(props: {
146112
: GRAPHQL_OAUTH_CONNECTION_SLOT,
147113
}
148114
: { kind: "none" };
149-
payload.credentialTargetScope = credentialTargetScope;
115+
payload.credentialTargetScope = credentialEditor.credentialTargetScope;
150116
}
151117
const exit = await doUpdate({
152118
params: { scopeId: displayScope, namespace: props.sourceId },
@@ -160,7 +126,7 @@ function EditForm(props: {
160126
return;
161127
}
162128

163-
setCredentialsDirty(false);
129+
credentialEditor.resetCredentialsDirty();
164130
setAuthDirty(false);
165131
props.onSave();
166132
setSaving(false);
@@ -192,13 +158,13 @@ function EditForm(props: {
192158
/>
193159

194160
<HttpCredentials.Root
195-
credentials={credentials}
196-
onChange={handleCredentialsChange}
161+
credentials={credentialEditor.credentials}
162+
onChange={credentialEditor.onCredentialsChange}
197163
existingSecrets={secretList}
198164
sourceName={identity.name}
199-
targetScope={credentialTargetScope}
200-
credentialScopeOptions={credentialScopeOptions}
201-
bindingScopeOptions={credentialScopeOptions}
165+
targetScope={credentialEditor.credentialTargetScope}
166+
credentialScopeOptions={credentialEditor.credentialScopeOptions}
167+
bindingScopeOptions={credentialEditor.credentialScopeOptions}
202168
>
203169
<HttpCredentials.Headers />
204170
<HttpCredentials.QueryParams />
@@ -236,26 +202,13 @@ function EditForm(props: {
236202
endpoint={endpoint.trim()}
237203
tokenScope={oauthCredentialTargetScope}
238204
onTokenScopeChange={setOAuthCredentialTargetScope}
239-
credentialScopeOptions={credentialScopeOptions}
240-
connectionId={boundConnectionId}
205+
credentialScopeOptions={credentialEditor.credentialScopeOptions}
206+
connectionId={oauthConnection.connectionId}
241207
sourceLabel={`${identity.name.trim() || props.initial.namespace || "GraphQL"} OAuth`}
242-
headers={oauthRequestCredentials.headers}
243-
queryParams={oauthRequestCredentials.queryParams}
244-
isConnected={isConnected}
245-
onConnected={async (connectionId) => {
246-
await setBinding({
247-
params: { scopeId: oauthCredentialTargetScope },
248-
payload: {
249-
targetScope: oauthCredentialTargetScope,
250-
pluginId: "graphql",
251-
sourceId: props.sourceId,
252-
sourceScope,
253-
slotKey: oauth2.connectionSlot,
254-
value: { kind: "connection", connectionId },
255-
},
256-
reactivityKeys: [...sourceWriteKeys, ...connectionWriteKeys],
257-
});
258-
}}
208+
headers={credentialEditor.requestCredentials.headers}
209+
queryParams={credentialEditor.requestCredentials.queryParams}
210+
isConnected={oauthConnection.isConnected}
211+
onConnected={oauthConnection.onConnected}
259212
/>
260213
)}
261214

packages/plugins/mcp/src/react/AddMcpSource.tsx

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
emptyHttpCredentials,
2323
HttpCredentials,
2424
httpCredentialsValid,
25+
nonEmptyHttpCredentialFields,
2526
serializeScopedHttpCredentials,
2627
serializeHttpCredentials,
2728
} from "@executor-js/react/plugins/http-credentials";
@@ -323,13 +324,14 @@ export default function AddMcpSource(props: {
323324

324325
const handleProbe = useCallback(async () => {
325326
dispatch({ type: "probe-start" });
326-
const { headers, queryParams } = serializeHttpCredentials(remoteCredentials);
327+
const requestCredentials = nonEmptyHttpCredentialFields(
328+
serializeHttpCredentials(remoteCredentials),
329+
);
327330
const exit = await doProbe({
328331
params: { scopeId },
329332
payload: {
330333
endpoint: state.url.trim(),
331-
...(Object.keys(headers).length > 0 ? { headers } : {}),
332-
...(Object.keys(queryParams).length > 0 ? { queryParams } : {}),
334+
...requestCredentials,
333335
},
334336
});
335337
if (Exit.isFailure(exit)) {
@@ -371,12 +373,13 @@ export default function AddMcpSource(props: {
371373
slugifyNamespace(remoteIdentity.namespace) ||
372374
slugifyNamespace(probe?.namespace ?? "") ||
373375
"mcp";
374-
const { headers, queryParams } = serializeHttpCredentials(remoteCredentials);
376+
const requestCredentials = nonEmptyHttpCredentialFields(
377+
serializeHttpCredentials(remoteCredentials),
378+
);
375379
await oauth.start({
376380
payload: {
377381
endpoint: state.url.trim(),
378-
...(Object.keys(headers).length > 0 ? { headers } : {}),
379-
...(Object.keys(queryParams).length > 0 ? { queryParams } : {}),
382+
...requestCredentials,
380383
redirectUrl: oauthCallbackUrl(),
381384
connectionId: oauthConnectionId({
382385
pluginId: "mcp",
@@ -427,6 +430,10 @@ export default function AddMcpSource(props: {
427430
remoteCredentials,
428431
requestCredentialTargetScope,
429432
);
433+
const requestCredentials = nonEmptyHttpCredentialFields({
434+
headers: credentials.headers as Record<string, McpCredentialInput>,
435+
queryParams: credentials.queryParams as Record<string, McpCredentialInput>,
436+
});
430437
const displayName = remoteIdentity.name.trim() || probe.serverName || probe.name;
431438
const slugNamespace = slugifyNamespace(remoteIdentity.namespace);
432439
const exit = await doAdd({
@@ -442,12 +449,7 @@ export default function AddMcpSource(props: {
442449
remoteAuthMode === "oauth2" && tokens
443450
? oauthCredentialTargetScope
444451
: requestCredentialTargetScope,
445-
...(Object.keys(credentials.headers).length > 0
446-
? { headers: credentials.headers as Record<string, McpCredentialInput> }
447-
: {}),
448-
...(Object.keys(credentials.queryParams).length > 0
449-
? { queryParams: credentials.queryParams }
450-
: {}),
452+
...requestCredentials,
451453
},
452454
reactivityKeys: sourceWriteKeys,
453455
});

0 commit comments

Comments
 (0)