-
Notifications
You must be signed in to change notification settings - Fork 154
Expand file tree
/
Copy pathAddGraphqlSource.tsx
More file actions
164 lines (143 loc) · 6.08 KB
/
Copy pathAddGraphqlSource.tsx
File metadata and controls
164 lines (143 loc) · 6.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
import { useCallback, useMemo, useState } from "react";
import { useAtomSet } from "@effect/atom-react";
import * as Exit from "effect/Exit";
import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys";
import {
integrationDisplayNameFromUrl,
slugifyNamespace,
useIntegrationIdentity,
} from "@executor-js/react/plugins/integration-identity";
import { Button } from "@executor-js/react/components/button";
import {
AuthMethodListEditor,
useAuthMethodList,
type AuthMethodRow,
type AuthMethodSeed,
} from "@executor-js/react/components/auth-method-list-editor";
import { FloatActions } from "@executor-js/react/components/float-actions";
import {
addIntegrationErrorMessage,
FormErrorAlert,
SlugCollisionAlert,
useSlugAlreadyExists,
} from "@executor-js/react/lib/integration-add";
import { addGraphqlIntegrationOptimistic } from "./atoms";
import { GraphqlSourceFields } from "./GraphqlSourceFields";
import { graphqlAuthMethodInputsFromPlacements } from "./auth-method-config";
import type { GraphqlAuthMethodInput } from "../sdk/types";
// v2 GraphQL add flow: register the integration with its declared auth-method
// LIST (the shared `AuthMethodListEditor` — GraphQL stays header/query apiKey;
// OAuth is hidden), then route to the integration's detail hub. Connection
// creation is no longer part of the add flow — accounts are added from the hub
// (P6: add without auth, connect later).
// GraphQL has no add-time detection, so the list starts empty (module constant
// — a fresh [] every render would re-seed the list each render).
const NO_SEEDS: readonly AuthMethodSeed[] = [];
export default function AddGraphqlSource(props: {
onComplete: (slug?: string) => void;
onCancel: () => void;
initialUrl?: string;
}) {
const [endpoint, setEndpoint] = useState(props.initialUrl ?? "");
const [description, setDescription] = useState("");
const identity = useIntegrationIdentity({
fallbackName: integrationDisplayNameFromUrl(endpoint, "GraphQL") ?? "",
});
const authMethodList = useAuthMethodList(NO_SEEDS);
const [adding, setAdding] = useState(false);
const [addError, setAddError] = useState<string | null>(null);
const doAddIntegration = useAtomSet(addGraphqlIntegrationOptimistic, {
mode: "promiseExit",
});
// The methods to register: each apikey row declares ONE method carrying
// every named placement (header + query mix in a single method). Inputs
// omit slugs — the backend assigns carrier-derived ones. `none` rows
// register nothing.
const authenticationTemplate = useMemo<readonly GraphqlAuthMethodInput[]>(
() =>
authMethodList.rows.flatMap((row: AuthMethodRow) =>
row.value.kind === "apikey"
? graphqlAuthMethodInputsFromPlacements(row.value.placements)
: [],
),
[authMethodList.rows],
);
// Every apikey row needs at least one named placement; `none` rows are
// always valid.
const apiKeyComplete = authMethodList.rows.every(
(row: AuthMethodRow) =>
row.value.kind !== "apikey" ||
row.value.placements.some((placement) => placement.name.trim().length > 0),
);
const resolvedSlug = useMemo(
() =>
slugifyNamespace(identity.namespace) ||
slugifyNamespace(integrationDisplayNameFromUrl(endpoint.trim(), "GraphQL") ?? "") ||
"graphql",
[endpoint, identity.namespace],
);
// Pre-empt the API's `IntegrationAlreadyExistsError`: adding an integration
// whose slug already exists clobbers the existing one's connections/policies,
// so the API blocks it. Surface that here from the tenant-scoped catalog list.
const slugAlreadyExists = useSlugAlreadyExists(resolvedSlug);
const canAdd = endpoint.trim().length > 0 && apiKeyComplete && !adding && !slugAlreadyExists;
const sourceIdentity = useCallback(() => {
const trimmedEndpoint = endpoint.trim();
const slug = resolvedSlug;
const displayName =
identity.name.trim() || integrationDisplayNameFromUrl(trimmedEndpoint, "GraphQL") || slug;
return { trimmedEndpoint, slug, displayName };
}, [endpoint, identity.name, resolvedSlug]);
const handleAdd = async (): Promise<void> => {
setAdding(true);
setAddError(null);
const { trimmedEndpoint, slug, displayName } = sourceIdentity();
const integrationExit = await doAddIntegration({
payload: {
endpoint: trimmedEndpoint,
slug,
name: displayName,
...(description.trim().length > 0 ? { description: description.trim() } : {}),
...(authenticationTemplate.length > 0
? { authenticationTemplate: [...authenticationTemplate] }
: {}),
},
reactivityKeys: integrationWriteKeys,
});
if (Exit.isFailure(integrationExit)) {
setAddError(addIntegrationErrorMessage(integrationExit, slug, "Failed to add source"));
setAdding(false);
return;
}
const registeredSlug = integrationExit.value.slug;
props.onComplete(String(registeredSlug));
};
return (
<div className="flex flex-1 flex-col gap-6">
<h1 className="text-xl font-semibold text-foreground">Add GraphQL integration</h1>
<GraphqlSourceFields
endpoint={endpoint}
onEndpointChange={setEndpoint}
identity={identity}
description={description}
onDescriptionChange={setDescription}
/>
<AuthMethodListEditor
list={authMethodList}
allowedKinds={["none", "apikey"]}
emptyHint="No authentication declared. Add a method, or add the integration without auth and connect an account from the integration page later."
footerHint="Every method here is registered with the integration. Connect an account from the integration page after adding."
/>
{slugAlreadyExists && !adding && <SlugCollisionAlert slug={resolvedSlug} />}
{addError && <FormErrorAlert message={addError} />}
<FloatActions>
<Button variant="ghost" onClick={() => props.onCancel()} disabled={adding}>
Cancel
</Button>
<Button onClick={() => void handleAdd()} disabled={!canAdd} loading={adding}>
Add integration
</Button>
</FloatActions>
</div>
);
}