Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions packages/core/sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,6 @@ export interface SourcePlugin {
readonly variant?: "badge" | "panel";
readonly onAction?: () => void;
}>;
readonly signIn?: ComponentType<{
readonly sourceId: string;
}>;
readonly presets?: readonly SourcePreset[];
/** Trigger early download of the plugin's lazy component chunks (add/edit/etc.).
* Call from the host on intent (hover/focus) so the chunks land before the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Badge } from "@executor-js/react/components/badge";
import { Button } from "@executor-js/react/components/button";

import { googleDiscoverySourceAtom } from "./atoms";
import GoogleDiscoverySignInButton from "./GoogleDiscoverySignInButton";

export default function EditGoogleDiscoverySource({
sourceId,
Expand Down Expand Up @@ -63,9 +64,12 @@ export default function EditGoogleDiscoverySource({
<p className="text-xs uppercase tracking-wider text-muted-foreground mb-1">
Authentication
</p>
<p className="text-sm font-medium text-foreground capitalize">
{authKind === "oauth2" ? "OAuth 2.0" : authKind}
</p>
<div className="flex min-h-9 items-center justify-between gap-3">
<p className="text-sm font-medium text-foreground capitalize">
{authKind === "oauth2" ? "OAuth 2.0" : authKind}
</p>
{authKind === "oauth2" && <GoogleDiscoverySignInButton sourceId={sourceId} />}
</div>
</div>
</div>
)}
Expand Down
3 changes: 0 additions & 3 deletions packages/plugins/google-discovery/src/react/source-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,17 @@ import { googleDiscoveryPresets } from "../sdk/presets";
const importAdd = () => import("./AddGoogleDiscoverySource");
const importEdit = () => import("./EditGoogleDiscoverySource");
const importSummary = () => import("./GoogleDiscoverySourceSummary");
const importSignIn = () => import("./GoogleDiscoverySignInButton");

export const googleDiscoverySourcePlugin: SourcePlugin = {
key: "googleDiscovery",
label: "Google Discovery",
add: lazy(importAdd),
edit: lazy(importEdit),
summary: lazy(importSummary),
signIn: lazy(importSignIn),
presets: googleDiscoveryPresets,
preload: () => {
void importAdd();
void importEdit();
void importSummary();
void importSignIn();
},
};
25 changes: 2 additions & 23 deletions packages/plugins/graphql/src/react/AddGraphqlSource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
import {
sourceDisplayNameFromUrl,
slugifyNamespace,
SourceIdentityFieldRows,
useSourceIdentity,
} from "@executor-js/react/plugins/source-identity";
import {
Expand All @@ -33,16 +32,11 @@ import {
import { useSecretPickerSecrets } from "@executor-js/react/plugins/use-secret-picker-secrets";
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 { FloatActions } from "@executor-js/react/components/float-actions";
import { Input } from "@executor-js/react/components/input";
import { Spinner } from "@executor-js/react/components/spinner";
import { addGraphqlSourceOptimistic } from "./atoms";
import { initialGraphqlCredentials } from "./defaults";
import { GraphqlSourceFields } from "./GraphqlSourceFields";
import type { GraphqlCredentialInput } from "../sdk/types";

const ErrorMessage = Schema.Struct({ message: Schema.String });
Expand Down Expand Up @@ -181,22 +175,7 @@ export default function AddGraphqlSource(props: {
<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>
<SourceIdentityFieldRows identity={identity} namePlaceholder="e.g. Shopify API" />
</CardStackContent>
</CardStack>
<GraphqlSourceFields endpoint={endpoint} onEndpointChange={setEndpoint} identity={identity} />

<HttpCredentialsEditor
credentials={credentials}
Expand Down
130 changes: 83 additions & 47 deletions packages/plugins/graphql/src/react/EditGraphqlSource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,39 @@ import { useState } from "react";
import { useAtomValue, useAtomSet } from "@effect/atom-react";
import * as Exit from "effect/Exit";
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
import { graphqlSourceAtom, graphqlSourceBindingsAtom, updateGraphqlSource } from "./atoms";
import { useScope } from "@executor-js/react/api/scope-context";
import { sourceWriteKeys } from "@executor-js/react/api/reactivity-keys";
import {
graphqlSourceAtom,
graphqlSourceBindingsAtom,
setGraphqlSourceBinding,
updateGraphqlSource,
} from "./atoms";
import { connectionsAtom } from "@executor-js/react/api/atoms";
import { useScope, useScopeStack } from "@executor-js/react/api/scope-context";
import { connectionWriteKeys, sourceWriteKeys } from "@executor-js/react/api/reactivity-keys";
import { useSecretPickerSecrets } from "@executor-js/react/plugins/use-secret-picker-secrets";
import {
HttpCredentialsEditor,
serializeHttpCredentials,
serializeScopedHttpCredentials,
type HttpCredentialsState,
} from "@executor-js/react/plugins/http-credentials";
import {
effectiveCredentialBindingForScope,
httpCredentialsFromConfiguredCredentialBindings,
initialCredentialTargetScope,
} from "@executor-js/react/plugins/credential-bindings";
import {
SourceIdentityFields,
useSourceIdentity,
} from "@executor-js/react/plugins/source-identity";
import {
CredentialTargetScopeSelector,
useCredentialTargetScope,
} from "@executor-js/react/plugins/credential-target-scope";
import { slugifyNamespace, useSourceIdentity } from "@executor-js/react/plugins/source-identity";
import { useCredentialTargetScope } from "@executor-js/react/plugins/credential-target-scope";
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 { SourceOAuthConnectionControl } from "@executor-js/react/plugins/source-oauth-connection";
import { Badge } from "@executor-js/react/components/badge";
import { ScopeId } from "@executor-js/sdk/core";
import { GraphqlSourceFields } from "./GraphqlSourceFields";
import {
GRAPHQL_OAUTH_CONNECTION_SLOT,
type GraphqlCredentialInput,
GraphqlSourceBindingInput,
type GraphqlSourceBindingRef,
} from "../sdk/types";
import type { StoredGraphqlSource } from "../sdk/store";
Expand All @@ -54,14 +53,23 @@ function EditForm(props: {
onSave: () => void;
}) {
const displayScope = useScope();
const scopeStack = useScopeStack();
const sourceScope = ScopeId.make(props.initial.scope);
const { credentialTargetScope, setCredentialTargetScope, credentialScopeOptions } =
useCredentialTargetScope({
sourceScope,
initialTargetScope: initialCredentialTargetScope(sourceScope, props.bindings),
});
const { credentialTargetScope, credentialScopeOptions } = useCredentialTargetScope({
sourceScope,
initialTargetScope: initialCredentialTargetScope(sourceScope, props.bindings),
});
const {
credentialTargetScope: oauthCredentialTargetScope,
setCredentialTargetScope: setOAuthCredentialTargetScope,
} = useCredentialTargetScope({
sourceScope,
initialTargetScope: initialCredentialTargetScope(sourceScope, props.bindings),
});
const doUpdate = useAtomSet(updateGraphqlSource, { mode: "promiseExit" });
const setBinding = useAtomSet(setGraphqlSourceBinding, { mode: "promise" });
const secretList = useSecretPickerSecrets();
const connectionsResult = useAtomValue(connectionsAtom(displayScope));

const identity = useSourceIdentity({
fallbackName: props.initial.name,
Expand All @@ -84,6 +92,23 @@ function EditForm(props: {
const identityDirty = identity.name.trim() !== props.initial.name.trim();
const metadataDirty = identityDirty || endpoint.trim() !== props.initial.endpoint.trim();
const dirty = metadataDirty || credentialsDirty || authDirty;
const oauth2 = props.initial.auth.kind === "oauth2" ? props.initial.auth : null;
const connections = AsyncResult.isSuccess(connectionsResult) ? connectionsResult.value : [];
const scopeRanks = new Map(scopeStack.map((scope, index) => [scope.id, index] as const));
const connectionBinding = oauth2
? effectiveCredentialBindingForScope(
props.bindings,
oauth2.connectionSlot,
oauthCredentialTargetScope,
scopeRanks,
)
: null;
const boundConnectionId =
connectionBinding?.value.kind === "connection" ? connectionBinding.value.connectionId : null;
const isConnected =
boundConnectionId !== null &&
connections.some((connection) => connection.id === boundConnectionId);
const oauthRequestCredentials = serializeHttpCredentials(credentials);

const handleCredentialsChange = (next: HttpCredentialsState) => {
setCredentials(next);
Expand Down Expand Up @@ -164,31 +189,11 @@ function EditForm(props: {
</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);
}}
placeholder="https://api.example.com/graphql"
className="font-mono text-sm"
/>
</CardStackEntryField>
</CardStackContent>
</CardStack>

<CredentialTargetScopeSelector
value={credentialTargetScope}
options={credentialScopeOptions}
onChange={(targetScope) => {
setCredentialTargetScope(targetScope);
setCredentialsDirty(true);
}}
description="Choose where updated GraphQL credentials are saved."
<GraphqlSourceFields
endpoint={endpoint}
onEndpointChange={setEndpoint}
identity={identity}
namespaceReadOnly
/>

<HttpCredentialsEditor
Expand Down Expand Up @@ -224,6 +229,37 @@ function EditForm(props: {
)}
</section>

{oauth2 && (
<SourceOAuthConnectionControl
popupName="graphql-oauth"
pluginId="graphql"
namespace={slugifyNamespace(props.initial.namespace) || "graphql"}
fallbackNamespace="graphql"
endpoint={endpoint.trim()}
tokenScope={oauthCredentialTargetScope}
onTokenScopeChange={setOAuthCredentialTargetScope}
credentialScopeOptions={credentialScopeOptions}
connectionId={boundConnectionId}
sourceLabel={`${identity.name.trim() || props.initial.namespace || "GraphQL"} OAuth`}
headers={oauthRequestCredentials.headers}
queryParams={oauthRequestCredentials.queryParams}
isConnected={isConnected}
onConnected={async (connectionId) => {
await setBinding({
params: { scopeId: oauthCredentialTargetScope },
payload: new GraphqlSourceBindingInput({
sourceId: props.sourceId,
sourceScope,
scope: oauthCredentialTargetScope,
slot: oauth2.connectionSlot,
value: { kind: "connection", connectionId },
}),
reactivityKeys: [...sourceWriteKeys, ...connectionWriteKeys],
});
}}
/>
)}

{error && (
<div className="rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2">
<p className="text-sm text-destructive">{error}</p>
Expand Down
42 changes: 42 additions & 0 deletions packages/plugins/graphql/src/react/GraphqlSourceFields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
CardStack,
CardStackContent,
CardStackEntryField,
} from "@executor-js/react/components/card-stack";
import { Input } from "@executor-js/react/components/input";
import {
SourceIdentityFieldRows,
type SourceIdentity,
} from "@executor-js/react/plugins/source-identity";

export function GraphqlSourceFields(props: {
readonly endpoint: string;
readonly onEndpointChange: (endpoint: string) => void;
readonly identity: SourceIdentity;
readonly endpointDisabled?: boolean;
readonly namespaceReadOnly?: boolean;
}) {
return (
<CardStack>
<CardStackContent className="border-t-0">
<CardStackEntryField
label="Endpoint"
hint="The endpoint will be introspected to discover available queries and mutations."
>
<Input
value={props.endpoint}
onChange={(e) => props.onEndpointChange((e.target as HTMLInputElement).value)}
placeholder="https://api.example.com/graphql"
className="font-mono text-sm"
disabled={props.endpointDisabled}
/>
</CardStackEntryField>
<SourceIdentityFieldRows
identity={props.identity}
namePlaceholder="e.g. Shopify API"
namespaceReadOnly={props.namespaceReadOnly}
/>
</CardStackContent>
</CardStack>
);
}
Loading
Loading