@workos-inc/widgets 1.16.0-next.1785954585478 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { Callout, Card, Flex, Text } from "@radix-ui/themes";
4
- import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
5
  import * as CardList from "./card-list.js";
6
6
  import { DataIntegrationIcon } from "./data-integration-icon.js";
7
7
  import { ProviderIcon } from "./provider-icon.js";
@@ -29,7 +29,8 @@ import {
29
29
  useAuthorizeDataIntegrationHook,
30
30
  useDeleteDataInstallation,
31
31
  useSettings,
32
- useUpsertDataIntegrationApiKey
32
+ useUpsertDataIntegrationApiKey,
33
+ useUpsertDataIntegrationClientCredentials
33
34
  } from "../api/endpoint.js";
34
35
  import { useQueryClient } from "@tanstack/react-query";
35
36
  import { GenericError } from "./generic-error.js";
@@ -37,8 +38,14 @@ import { getDomProps } from "./utils.js";
37
38
  import { Translation } from "./i18n/translation.js";
38
39
  import { useTranslation } from "./i18n/use-translation.js";
39
40
  import { ApiError } from "../api/errors.js";
41
+ import { buildRedactedSecret, isRedactedSecret } from "./redacted-secret.js";
40
42
  const HANDOFF_INTERVAL_MS = 100;
41
43
  const HANDOFF_MAX_ATTEMPTS = 100;
44
+ const CONNECT_AUTH_METHODS = [
45
+ "oauth",
46
+ "client_credentials",
47
+ "api_key"
48
+ ];
42
49
  const useOpenDataIntegrationAuthorizeUrl = (integration) => {
43
50
  const authorizeDataIntegration = useAuthorizeDataIntegrationHook();
44
51
  const settings = useSettings();
@@ -127,6 +134,140 @@ function getUnpinnedInstallationFields(integration) {
127
134
  (field) => field.scope === "installation" && !integration.config?.[field.key]
128
135
  ).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
129
136
  }
137
+ function getInitialConfigValues(fields, storedConfig) {
138
+ const values = {};
139
+ for (const field of fields) {
140
+ const stored = storedConfig[field.key];
141
+ if (typeof stored === "string" || typeof stored === "number" || typeof stored === "boolean") {
142
+ values[field.key] = String(stored);
143
+ }
144
+ }
145
+ return values;
146
+ }
147
+ function useConfigFieldsForm(fields, initialValues = {}) {
148
+ const [values, setValues] = useState(initialValues);
149
+ const [errors, setErrors] = useState({});
150
+ const translate = useTranslation();
151
+ const requiredMessage = translate({
152
+ defaultMessage: "This field is required",
153
+ id: "B2HvfC",
154
+ description: "Validation error shown when a required configuration field is left empty"
155
+ });
156
+ const patternMessage = translate({
157
+ defaultMessage: "Enter a valid value.",
158
+ id: "2XWaGz",
159
+ description: "Validation error shown when a configuration field value does not match the expected format"
160
+ });
161
+ const setValue = (key, value) => {
162
+ setValues((prev) => ({ ...prev, [key]: value }));
163
+ setErrors((prev) => prev[key] ? { ...prev, [key]: null } : prev);
164
+ };
165
+ const reset = () => {
166
+ setValues(initialValues);
167
+ setErrors({});
168
+ };
169
+ const validate = () => {
170
+ const nextErrors = {};
171
+ let valid = true;
172
+ for (const field of fields) {
173
+ const value = (values[field.key] ?? "").trim();
174
+ if (field.required && !value) {
175
+ nextErrors[field.key] = requiredMessage;
176
+ valid = false;
177
+ continue;
178
+ }
179
+ if (value && field.pattern) {
180
+ let matches = true;
181
+ try {
182
+ matches = new RegExp(field.pattern).test(value);
183
+ } catch {
184
+ matches = true;
185
+ }
186
+ if (!matches) {
187
+ nextErrors[field.key] = patternMessage;
188
+ valid = false;
189
+ continue;
190
+ }
191
+ }
192
+ nextErrors[field.key] = null;
193
+ }
194
+ setErrors(nextErrors);
195
+ return valid;
196
+ };
197
+ const buildConfig = () => {
198
+ const config = {};
199
+ for (const field of fields) {
200
+ const value = (values[field.key] ?? "").trim();
201
+ if (value) {
202
+ config[field.key] = value;
203
+ }
204
+ }
205
+ return config;
206
+ };
207
+ const buildConfigPayload = (storedConfig) => {
208
+ const payload = { ...storedConfig };
209
+ for (const field of fields) {
210
+ const value = (values[field.key] ?? "").trim();
211
+ if (value) {
212
+ payload[field.key] = value;
213
+ } else {
214
+ delete payload[field.key];
215
+ }
216
+ }
217
+ return payload;
218
+ };
219
+ const hasMissingRequired = fields.some(
220
+ (field) => field.required && !(values[field.key] ?? "").trim()
221
+ );
222
+ const isDirty = fields.some(
223
+ (field) => (values[field.key] ?? "") !== (initialValues[field.key] ?? "")
224
+ );
225
+ return {
226
+ values,
227
+ errors,
228
+ setValue,
229
+ reset,
230
+ validate,
231
+ buildConfig,
232
+ buildConfigPayload,
233
+ hasMissingRequired,
234
+ isDirty
235
+ };
236
+ }
237
+ function ConfigFieldInputs({
238
+ idPrefix,
239
+ fields,
240
+ values,
241
+ errors,
242
+ onChange
243
+ }) {
244
+ return fields.map((field) => {
245
+ const fieldId = `${idPrefix}-${field.key}`;
246
+ const errorId = `${fieldId}-error`;
247
+ const descriptionId = `${fieldId}-description`;
248
+ const error = errors[field.key];
249
+ const Control = field.secret ? PasswordField : TextField;
250
+ const describedBy = [field.description ? descriptionId : null, error ? errorId : null].filter(Boolean).join(" ") || void 0;
251
+ return /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: "1", children: [
252
+ /* @__PURE__ */ jsx(Label, { htmlFor: fieldId, children: field.label }),
253
+ field.description ? /* @__PURE__ */ jsx(Text, { color: "gray", size: "2", id: descriptionId, mb: "1", children: field.description }) : null,
254
+ /* @__PURE__ */ jsx(
255
+ Control,
256
+ {
257
+ id: fieldId,
258
+ value: values[field.key] ?? "",
259
+ placeholder: field.example,
260
+ autoComplete: "off",
261
+ required: field.required || void 0,
262
+ "aria-invalid": !!error || void 0,
263
+ "aria-describedby": describedBy,
264
+ onChange: (event) => onChange(field.key, event.target.value)
265
+ }
266
+ ),
267
+ error ? /* @__PURE__ */ jsx(Text, { color: "red", size: "2", id: errorId, children: error }) : null
268
+ ] }, field.key);
269
+ });
270
+ }
130
271
  function LogoSeparator() {
131
272
  return /* @__PURE__ */ jsx(
132
273
  DotsHorizontalIcon,
@@ -239,22 +380,11 @@ function ConnectDialog({
239
380
  }) {
240
381
  const initialStep = fields.length > 0 ? "fields" : "review";
241
382
  const [step, setStep] = useState(initialStep);
242
- const [values, setValues] = useState({});
243
- const [errors, setErrors] = useState({});
244
383
  const [submitError, setSubmitError] = useState(null);
245
384
  const [isSubmitting, setIsSubmitting] = useState(false);
385
+ const configForm = useConfigFieldsForm(fields);
246
386
  const eventHandler = useOpenDataIntegrationAuthorizeUrl(integration);
247
387
  const translate = useTranslation();
248
- const requiredMessage = translate({
249
- defaultMessage: "This field is required",
250
- id: "B2HvfC",
251
- description: "Validation error shown when a required configuration field is left empty"
252
- });
253
- const patternMessage = translate({
254
- defaultMessage: "Enter a valid value.",
255
- id: "2XWaGz",
256
- description: "Validation error shown when a configuration field value does not match the expected format"
257
- });
258
388
  const genericConnectError = translate({
259
389
  defaultMessage: "We couldn't start the connection. Please try again.",
260
390
  id: "zGw1ma",
@@ -262,57 +392,15 @@ function ConnectDialog({
262
392
  });
263
393
  const reset = () => {
264
394
  setStep(initialStep);
265
- setValues({});
266
- setErrors({});
395
+ configForm.reset();
267
396
  setSubmitError(null);
268
397
  setIsSubmitting(false);
269
398
  };
270
- const validateFields = () => {
271
- const nextErrors = {};
272
- let valid = true;
273
- for (const field of fields) {
274
- const value = (values[field.key] ?? "").trim();
275
- if (field.required && !value) {
276
- nextErrors[field.key] = requiredMessage;
277
- valid = false;
278
- continue;
279
- }
280
- if (value && field.pattern) {
281
- let matches = true;
282
- try {
283
- matches = new RegExp(field.pattern).test(value);
284
- } catch {
285
- matches = true;
286
- }
287
- if (!matches) {
288
- nextErrors[field.key] = patternMessage;
289
- valid = false;
290
- continue;
291
- }
292
- }
293
- nextErrors[field.key] = null;
294
- }
295
- setErrors(nextErrors);
296
- return valid;
297
- };
298
- const buildConfig = () => {
299
- const config = {};
300
- for (const field of fields) {
301
- const value = (values[field.key] ?? "").trim();
302
- if (value) {
303
- config[field.key] = value;
304
- }
305
- }
306
- return config;
307
- };
308
- const hasMissingRequired = fields.some(
309
- (field) => field.required && !(values[field.key] ?? "").trim()
310
- );
311
399
  const handleConnect = async () => {
312
400
  setSubmitError(null);
313
401
  setIsSubmitting(true);
314
402
  try {
315
- await eventHandler(buildConfig());
403
+ await eventHandler(configForm.buildConfig());
316
404
  onOpenChange(false);
317
405
  } catch (error) {
318
406
  setSubmitError(
@@ -335,46 +423,16 @@ function ConnectDialog({
335
423
  children: /* @__PURE__ */ jsxs(Dialog.Content, { maxWidth: "430px", children: [
336
424
  /* @__PURE__ */ jsx(ConnectDialogHeader, { integration }),
337
425
  step === "fields" ? /* @__PURE__ */ jsxs(Fragment, { children: [
338
- /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(Flex, { direction: "column", gap: "4", children: fields.map((field) => {
339
- const fieldId = `pipes-config-${field.key}`;
340
- const errorId = `${fieldId}-error`;
341
- const descriptionId = `${fieldId}-description`;
342
- const error = errors[field.key];
343
- const Control = field.secret ? PasswordField : TextField;
344
- const describedBy = [
345
- field.description ? descriptionId : null,
346
- error ? errorId : null
347
- ].filter(Boolean).join(" ") || void 0;
348
- return /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: "1", children: [
349
- /* @__PURE__ */ jsx(Label, { htmlFor: fieldId, children: field.label }),
350
- field.description ? /* @__PURE__ */ jsx(Text, { color: "gray", size: "2", id: descriptionId, mb: "1", children: field.description }) : null,
351
- /* @__PURE__ */ jsx(
352
- Control,
353
- {
354
- id: fieldId,
355
- value: values[field.key] ?? "",
356
- placeholder: field.example,
357
- autoComplete: "off",
358
- required: field.required || void 0,
359
- "aria-invalid": !!error || void 0,
360
- "aria-describedby": describedBy,
361
- onChange: (event) => {
362
- setValues((prev) => ({
363
- ...prev,
364
- [field.key]: event.target.value
365
- }));
366
- if (error) {
367
- setErrors((prev) => ({
368
- ...prev,
369
- [field.key]: null
370
- }));
371
- }
372
- }
373
- }
374
- ),
375
- error ? /* @__PURE__ */ jsx(Text, { color: "red", size: "2", id: errorId, children: error }) : null
376
- ] }, field.key);
377
- }) }) }),
426
+ /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(Flex, { direction: "column", gap: "4", children: /* @__PURE__ */ jsx(
427
+ ConfigFieldInputs,
428
+ {
429
+ idPrefix: "pipes-config",
430
+ fields,
431
+ values: configForm.values,
432
+ errors: configForm.errors,
433
+ onChange: configForm.setValue
434
+ }
435
+ ) }) }),
378
436
  /* @__PURE__ */ jsxs(Flex, { justify: "end", gap: "3", mt: "5", children: [
379
437
  /* @__PURE__ */ jsx(Dialog.Close, { children: /* @__PURE__ */ jsx(Button, { variant: "secondary", children: /* @__PURE__ */ jsx(
380
438
  Translation,
@@ -388,9 +446,9 @@ function ConnectDialog({
388
446
  Button,
389
447
  {
390
448
  type: "button",
391
- disabled: hasMissingRequired,
449
+ disabled: configForm.hasMissingRequired,
392
450
  onClick: () => {
393
- if (validateFields()) {
451
+ if (configForm.validate()) {
394
452
  setStep("review");
395
453
  }
396
454
  },
@@ -654,11 +712,247 @@ function ApiKeyConnectButton({
654
712
  ) })
655
713
  ] });
656
714
  }
657
- function DualMethodConnectButton({
715
+ function ClientCredentialsDialog({
716
+ integration,
717
+ fields,
718
+ open,
719
+ onOpenChange
720
+ }) {
721
+ const queryClient = useQueryClient();
722
+ const installation = integration.installation;
723
+ const isUpdate = installation?.auth_method === "client_credentials";
724
+ const storedConfig = useMemo(
725
+ () => isUpdate ? installation?.config ?? {} : {},
726
+ [isUpdate, installation?.config]
727
+ );
728
+ const initialConfigValues = useMemo(
729
+ () => getInitialConfigValues(fields, storedConfig),
730
+ [fields, storedConfig]
731
+ );
732
+ const redactedSecret = buildRedactedSecret(
733
+ isUpdate ? installation?.client_secret_last_4 : null
734
+ );
735
+ const [clientId, setClientId] = useState(
736
+ (isUpdate ? installation?.client_id : null) ?? ""
737
+ );
738
+ const [clientSecret, setClientSecret] = useState(redactedSecret);
739
+ const configForm = useConfigFieldsForm(fields, initialConfigValues);
740
+ const translate = useTranslation();
741
+ const clientIdPlaceholder = translate({
742
+ defaultMessage: "Enter your client ID",
743
+ id: "5YTAFo",
744
+ description: "Placeholder for the client credentials client ID input"
745
+ });
746
+ const clientSecretPlaceholder = translate({
747
+ defaultMessage: "Enter your client secret",
748
+ id: "53f0B7",
749
+ description: "Placeholder for the client credentials client secret input"
750
+ });
751
+ const secretMissing = !clientSecret.trim() || isRedactedSecret(clientSecret);
752
+ const {
753
+ mutate: upsertClientCredentials,
754
+ error,
755
+ isPending
756
+ } = useUpsertDataIntegrationClientCredentials({
757
+ mutation: {
758
+ onSuccess: () => {
759
+ onOpenChange(false);
760
+ queryClient.invalidateQueries({
761
+ queryKey: getMyDataIntegrationsQueryKey()
762
+ });
763
+ }
764
+ }
765
+ });
766
+ const errorMessage = error instanceof ApiError && error.message ? error.message : null;
767
+ const handleSubmit = () => {
768
+ if (!configForm.validate()) {
769
+ return;
770
+ }
771
+ const config = configForm.isDirty ? configForm.buildConfigPayload(storedConfig) : void 0;
772
+ upsertClientCredentials({
773
+ slug: integration.slug,
774
+ data: {
775
+ client_id: clientId.trim(),
776
+ client_secret: clientSecret.trim(),
777
+ ...config ? { config } : {}
778
+ }
779
+ });
780
+ };
781
+ return /* @__PURE__ */ jsx(Dialog.Root, { open, onOpenChange, children: /* @__PURE__ */ jsxs(Dialog.Content, { maxWidth: "430px", children: [
782
+ /* @__PURE__ */ jsx(Dialog.Title, { size: "3", weight: "bold", children: isUpdate ? /* @__PURE__ */ jsx(
783
+ Translation,
784
+ {
785
+ defaultMessage: "Update client credentials for {integrationName}",
786
+ id: "/D85zF",
787
+ description: "Dialog title for updating stored client credentials",
788
+ values: { integrationName: integration.name }
789
+ }
790
+ ) : /* @__PURE__ */ jsx(
791
+ Translation,
792
+ {
793
+ defaultMessage: "Connect {integrationName} with client credentials",
794
+ id: "x+7tw3",
795
+ description: "Dialog title for connecting with client credentials",
796
+ values: { integrationName: integration.name }
797
+ }
798
+ ) }),
799
+ error && /* @__PURE__ */ jsx(Callout.Root, { my: "3", color: "red", role: "alert", children: /* @__PURE__ */ jsx(Callout.Text, { children: errorMessage ?? /* @__PURE__ */ jsx(
800
+ Translation,
801
+ {
802
+ defaultMessage: "An error occurred. Please check the value and try again.",
803
+ id: "bYLbLL",
804
+ description: "Generic error message for API key submission failure"
805
+ }
806
+ ) }) }),
807
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: "4", mt: "2", children: [
808
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: "1", children: [
809
+ /* @__PURE__ */ jsx(Label, { htmlFor: "pipes-client-credentials-client-id", children: /* @__PURE__ */ jsx(
810
+ Translation,
811
+ {
812
+ defaultMessage: "Client ID",
813
+ id: "/Cv5P1",
814
+ description: "Label for the client credentials client ID input"
815
+ }
816
+ ) }),
817
+ /* @__PURE__ */ jsx(
818
+ TextField,
819
+ {
820
+ id: "pipes-client-credentials-client-id",
821
+ value: clientId,
822
+ autoComplete: "off",
823
+ placeholder: clientIdPlaceholder,
824
+ onChange: (event) => setClientId(event.target.value)
825
+ }
826
+ )
827
+ ] }),
828
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: "1", children: [
829
+ /* @__PURE__ */ jsx(Label, { htmlFor: "pipes-client-credentials-client-secret", children: /* @__PURE__ */ jsx(
830
+ Translation,
831
+ {
832
+ defaultMessage: "Client secret",
833
+ id: "uScBDj",
834
+ description: "Label for the client credentials client secret input"
835
+ }
836
+ ) }),
837
+ isUpdate && /* @__PURE__ */ jsx(
838
+ Text,
839
+ {
840
+ color: "gray",
841
+ size: "2",
842
+ id: "pipes-client-credentials-client-secret-description",
843
+ mb: "1",
844
+ children: /* @__PURE__ */ jsx(
845
+ Translation,
846
+ {
847
+ defaultMessage: "Enter the client secret again to save any changes.",
848
+ id: "ZrNPCF",
849
+ description: "Help text explaining the client secret must be re-entered to save an update"
850
+ }
851
+ )
852
+ }
853
+ ),
854
+ /* @__PURE__ */ jsx(
855
+ PasswordField,
856
+ {
857
+ id: "pipes-client-credentials-client-secret",
858
+ value: clientSecret,
859
+ autoComplete: "off",
860
+ type: isRedactedSecret(clientSecret) ? "text" : void 0,
861
+ placeholder: clientSecretPlaceholder,
862
+ "aria-describedby": isUpdate ? "pipes-client-credentials-client-secret-description" : void 0,
863
+ onChange: (event) => setClientSecret(event.target.value),
864
+ onFocus: () => {
865
+ if (isRedactedSecret(clientSecret)) {
866
+ setClientSecret("");
867
+ }
868
+ },
869
+ onBlur: () => {
870
+ if (clientSecret === "") {
871
+ setClientSecret(redactedSecret);
872
+ }
873
+ }
874
+ }
875
+ )
876
+ ] }),
877
+ /* @__PURE__ */ jsx(
878
+ ConfigFieldInputs,
879
+ {
880
+ idPrefix: "pipes-client-credentials-config",
881
+ fields,
882
+ values: configForm.values,
883
+ errors: configForm.errors,
884
+ onChange: configForm.setValue
885
+ }
886
+ )
887
+ ] }),
888
+ /* @__PURE__ */ jsxs(Flex, { justify: "end", gap: "3", mt: "5", children: [
889
+ /* @__PURE__ */ jsx(Dialog.Close, { children: /* @__PURE__ */ jsx(Button, { variant: "secondary", children: /* @__PURE__ */ jsx(
890
+ Translation,
891
+ {
892
+ defaultMessage: "Cancel",
893
+ id: "hHNj31",
894
+ description: "Cancel button text"
895
+ }
896
+ ) }) }),
897
+ /* @__PURE__ */ jsx(
898
+ Button,
899
+ {
900
+ type: "button",
901
+ disabled: isPending || !clientId.trim() || secretMissing || configForm.hasMissingRequired,
902
+ loading: isPending,
903
+ onClick: handleSubmit,
904
+ children: isUpdate ? /* @__PURE__ */ jsx(
905
+ Translation,
906
+ {
907
+ defaultMessage: "Save",
908
+ id: "RT8KNi",
909
+ description: "Save button text"
910
+ }
911
+ ) : /* @__PURE__ */ jsx(
912
+ Translation,
913
+ {
914
+ defaultMessage: "Connect",
915
+ id: "quvdFp",
916
+ description: "Button to connect an integration"
917
+ }
918
+ )
919
+ }
920
+ )
921
+ ] })
922
+ ] }) });
923
+ }
924
+ function ClientCredentialsConnectButton({
658
925
  integration
926
+ }) {
927
+ const [open, setOpen] = useState(false);
928
+ const fields = getUnpinnedInstallationFields(integration);
929
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
930
+ open && /* @__PURE__ */ jsx(
931
+ ClientCredentialsDialog,
932
+ {
933
+ integration,
934
+ fields,
935
+ open,
936
+ onOpenChange: setOpen
937
+ }
938
+ ),
939
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => setOpen(true), children: /* @__PURE__ */ jsx(
940
+ Translation,
941
+ {
942
+ defaultMessage: "Connect",
943
+ id: "quvdFp",
944
+ description: "Button to connect an integration"
945
+ }
946
+ ) })
947
+ ] });
948
+ }
949
+ function MultiMethodConnectButton({
950
+ integration,
951
+ methods
659
952
  }) {
660
953
  const [apiKeyOpen, setApiKeyOpen] = useState(false);
661
954
  const [oauthOpen, setOauthOpen] = useState(false);
955
+ const [clientCredentialsOpen, setClientCredentialsOpen] = useState(false);
662
956
  const fields = getUnpinnedInstallationFields(integration);
663
957
  return /* @__PURE__ */ jsxs(Fragment, { children: [
664
958
  apiKeyOpen && /* @__PURE__ */ jsx(
@@ -669,7 +963,16 @@ function DualMethodConnectButton({
669
963
  onOpenChange: setApiKeyOpen
670
964
  }
671
965
  ),
672
- /* @__PURE__ */ jsx(
966
+ clientCredentialsOpen && /* @__PURE__ */ jsx(
967
+ ClientCredentialsDialog,
968
+ {
969
+ integration,
970
+ fields,
971
+ open: clientCredentialsOpen,
972
+ onOpenChange: setClientCredentialsOpen
973
+ }
974
+ ),
975
+ methods.includes("oauth") && /* @__PURE__ */ jsx(
673
976
  ConnectDialog,
674
977
  {
675
978
  integration,
@@ -688,7 +991,7 @@ function DualMethodConnectButton({
688
991
  }
689
992
  ) }) }),
690
993
  /* @__PURE__ */ jsxs(DropdownMenu.Content, { align: "end", children: [
691
- /* @__PURE__ */ jsx(DropdownMenu.Item, { onClick: () => setOauthOpen(true), children: /* @__PURE__ */ jsxs(Flex, { gap: "4", width: "100%", justify: "between", align: "center", children: [
994
+ methods.includes("oauth") && /* @__PURE__ */ jsx(DropdownMenu.Item, { onClick: () => setOauthOpen(true), children: /* @__PURE__ */ jsxs(Flex, { gap: "4", width: "100%", justify: "between", align: "center", children: [
692
995
  /* @__PURE__ */ jsx(
693
996
  Translation,
694
997
  {
@@ -700,7 +1003,15 @@ function DualMethodConnectButton({
700
1003
  ),
701
1004
  /* @__PURE__ */ jsx(ExternalLinkIcon, { "aria-hidden": true })
702
1005
  ] }) }),
703
- /* @__PURE__ */ jsx(DropdownMenu.Item, { onClick: () => setApiKeyOpen(true), children: /* @__PURE__ */ jsx(
1006
+ methods.includes("client_credentials") && /* @__PURE__ */ jsx(DropdownMenu.Item, { onClick: () => setClientCredentialsOpen(true), children: /* @__PURE__ */ jsx(
1007
+ Translation,
1008
+ {
1009
+ defaultMessage: "Enter client credentials",
1010
+ id: "Y/87Vt",
1011
+ description: "Menu option to connect via client credentials"
1012
+ }
1013
+ ) }),
1014
+ methods.includes("api_key") && /* @__PURE__ */ jsx(DropdownMenu.Item, { onClick: () => setApiKeyOpen(true), children: /* @__PURE__ */ jsx(
704
1015
  Translation,
705
1016
  {
706
1017
  defaultMessage: "Enter API key",
@@ -715,6 +1026,7 @@ function DualMethodConnectButton({
715
1026
  function ProviderStatus({ integration }) {
716
1027
  const [disconnectOpen, setDisconnectOpen] = useState(false);
717
1028
  const [apiKeyOpen, setApiKeyOpen] = useState(false);
1029
+ const [clientCredentialsOpen, setClientCredentialsOpen] = useState(false);
718
1030
  const [reauthConfigOpen, setReauthConfigOpen] = useState(false);
719
1031
  const authorizeEventHandler = useOpenDataIntegrationAuthorizeUrl(integration);
720
1032
  const translate = useTranslation();
@@ -727,6 +1039,7 @@ function ProviderStatus({ integration }) {
727
1039
  if (integration.installation) {
728
1040
  const connected = integration.installation.state === "connected";
729
1041
  const isApiKeyInstallation = integration.installation.auth_method === "api_key";
1042
+ const isClientCredentialsInstallation = integration.installation.auth_method === "client_credentials";
730
1043
  return /* @__PURE__ */ jsxs(Fragment, { children: [
731
1044
  /* @__PURE__ */ jsx(
732
1045
  DisconnectAccountDialog,
@@ -745,7 +1058,16 @@ function ProviderStatus({ integration }) {
745
1058
  onOpenChange: setApiKeyOpen
746
1059
  }
747
1060
  ),
748
- unpinnedFields.length > 0 && /* @__PURE__ */ jsx(
1061
+ clientCredentialsOpen && /* @__PURE__ */ jsx(
1062
+ ClientCredentialsDialog,
1063
+ {
1064
+ integration,
1065
+ fields: unpinnedFields,
1066
+ open: clientCredentialsOpen,
1067
+ onOpenChange: setClientCredentialsOpen
1068
+ }
1069
+ ),
1070
+ unpinnedFields.length > 0 && !isClientCredentialsInstallation && /* @__PURE__ */ jsx(
749
1071
  ConnectDialog,
750
1072
  {
751
1073
  integration,
@@ -770,7 +1092,40 @@ function ProviderStatus({ integration }) {
770
1092
  description: "Status text for integration needing reauthorization"
771
1093
  }
772
1094
  ) }),
773
- connected && isApiKeyInstallation ? /* @__PURE__ */ jsxs(DropdownMenu.Root, { children: [
1095
+ isClientCredentialsInstallation ? /* @__PURE__ */ jsxs(DropdownMenu.Root, { children: [
1096
+ /* @__PURE__ */ jsx(DropdownMenu.Trigger, { children: /* @__PURE__ */ jsx(IconButton, { title: pipeActionsTitle, children: /* @__PURE__ */ jsx(DotsHorizontalIcon, {}) }) }),
1097
+ /* @__PURE__ */ jsxs(DropdownMenu.Content, { align: "end", children: [
1098
+ /* @__PURE__ */ jsx(
1099
+ DropdownMenu.Item,
1100
+ {
1101
+ onClick: () => setClientCredentialsOpen(true),
1102
+ children: /* @__PURE__ */ jsx(
1103
+ Translation,
1104
+ {
1105
+ defaultMessage: "Update client credentials",
1106
+ id: "daHKWE",
1107
+ description: "Menu option to update stored client credentials"
1108
+ }
1109
+ )
1110
+ }
1111
+ ),
1112
+ /* @__PURE__ */ jsx(
1113
+ DropdownMenu.Item,
1114
+ {
1115
+ variant: "destructive",
1116
+ onClick: () => setDisconnectOpen(true),
1117
+ children: /* @__PURE__ */ jsx(
1118
+ Translation,
1119
+ {
1120
+ defaultMessage: "Disconnect account",
1121
+ id: "esZQrd",
1122
+ description: "Menu option to disconnect an account"
1123
+ }
1124
+ )
1125
+ }
1126
+ )
1127
+ ] })
1128
+ ] }) : connected && isApiKeyInstallation ? /* @__PURE__ */ jsxs(DropdownMenu.Root, { children: [
774
1129
  /* @__PURE__ */ jsx(DropdownMenu.Trigger, { children: /* @__PURE__ */ jsx(IconButton, { title: pipeActionsTitle, children: /* @__PURE__ */ jsx(DotsHorizontalIcon, {}) }) }),
775
1130
  /* @__PURE__ */ jsxs(DropdownMenu.Content, { align: "end", children: [
776
1131
  /* @__PURE__ */ jsx(DropdownMenu.Item, { onClick: () => setApiKeyOpen(true), children: /* @__PURE__ */ jsx(
@@ -882,34 +1237,9 @@ function ProviderStatus({ integration }) {
882
1237
  ] });
883
1238
  }
884
1239
  const authMethods = integration.authMethods ?? [];
885
- const supportsOAuth = authMethods.includes("oauth");
886
- const supportsApiKey = authMethods.includes("api_key");
887
- if (supportsOAuth && supportsApiKey) {
888
- return /* @__PURE__ */ jsxs(Flex, { align: "center", gap: "4", children: [
889
- /* @__PURE__ */ jsx(Status, { state: "neutral", children: /* @__PURE__ */ jsx(
890
- Translation,
891
- {
892
- defaultMessage: "Not connected",
893
- id: "J7bTZa",
894
- description: "Status text for an integration that is not connected"
895
- }
896
- ) }),
897
- /* @__PURE__ */ jsx(DualMethodConnectButton, { integration })
898
- ] });
899
- }
900
- if (supportsApiKey) {
901
- return /* @__PURE__ */ jsxs(Flex, { align: "center", gap: "4", children: [
902
- /* @__PURE__ */ jsx(Status, { state: "neutral", children: /* @__PURE__ */ jsx(
903
- Translation,
904
- {
905
- defaultMessage: "Not connected",
906
- id: "J7bTZa",
907
- description: "Status text for an integration that is not connected"
908
- }
909
- ) }),
910
- /* @__PURE__ */ jsx(ApiKeyConnectButton, { integration })
911
- ] });
912
- }
1240
+ const methods = CONNECT_AUTH_METHODS.filter(
1241
+ (method) => authMethods.includes(method)
1242
+ );
913
1243
  return /* @__PURE__ */ jsxs(Flex, { align: "center", gap: "4", children: [
914
1244
  /* @__PURE__ */ jsx(Status, { state: "neutral", children: /* @__PURE__ */ jsx(
915
1245
  Translation,
@@ -919,7 +1249,7 @@ function ProviderStatus({ integration }) {
919
1249
  description: "Status text for an integration that is not connected"
920
1250
  }
921
1251
  ) }),
922
- /* @__PURE__ */ jsx(ConnectIntegrationButton, { integration })
1252
+ methods.length > 1 ? /* @__PURE__ */ jsx(MultiMethodConnectButton, { integration, methods }) : methods.length === 1 && methods[0] === "api_key" ? /* @__PURE__ */ jsx(ApiKeyConnectButton, { integration }) : methods.length === 1 && methods[0] === "client_credentials" ? /* @__PURE__ */ jsx(ClientCredentialsConnectButton, { integration }) : /* @__PURE__ */ jsx(ConnectIntegrationButton, { integration })
923
1253
  ] });
924
1254
  }
925
1255
  function DisconnectAccountDialog({