From 0060543a9cd32477c4281d8420b811bd484f9b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20De=20Freitas?= <6485562+adefreitas@users.noreply.github.com> Date: Tue, 26 Aug 2025 16:53:19 +0100 Subject: [PATCH 1/2] feat: add falcon connector support --- .../components/IntegrationFields.tsx | 15 ++- .../components/IntegrationList.tsx | 6 +- .../hooks/useIntegrationPicker.ts | 114 ++++++++++++++++-- src/modules/integration-picker/queries.ts | 25 +++- src/modules/integration-picker/types.ts | 14 ++- src/shared/utils/utils.ts | 3 + 6 files changed, 164 insertions(+), 13 deletions(-) create mode 100644 src/shared/utils/utils.ts diff --git a/src/modules/integration-picker/components/IntegrationFields.tsx b/src/modules/integration-picker/components/IntegrationFields.tsx index c625f73..bb66e4c 100644 --- a/src/modules/integration-picker/components/IntegrationFields.tsx +++ b/src/modules/integration-picker/components/IntegrationFields.tsx @@ -55,12 +55,25 @@ export const IntegrationForm: React.FC = ({ fields, onCh })); }; + const errorJson = () => { + if (!error) { + return null; + } + try { + return ; + } catch (_e) { + if (error?.provider_response && error?.provider_response.length > 0) { + return ; + } + return null; + } + }; return ( {error && ( - + {errorJson()} )} diff --git a/src/modules/integration-picker/components/IntegrationList.tsx b/src/modules/integration-picker/components/IntegrationList.tsx index 0860a0b..c63638d 100644 --- a/src/modules/integration-picker/components/IntegrationList.tsx +++ b/src/modules/integration-picker/components/IntegrationList.tsx @@ -14,6 +14,7 @@ import { } from '@stackone/malachite'; import { useCallback, useMemo, useState } from 'react'; import { CATEGORIES_WITH_LABELS } from '../../../shared/categories'; +import { isFalconVersion } from '../../../shared/utils/utils'; import { Integration } from '../types'; interface IntegrationRowProps { @@ -43,6 +44,9 @@ const IntegrationRow: React.FC = ({ integration }) => { /> {integration.name ?? 'N/A'} + {isFalconVersion(integration.version) && ( + {integration.version} + )} { CATEGORIES_WITH_LABELS.find((category) => category.value === integration.type) @@ -123,7 +127,7 @@ export const IntegrationList: React.FC<{ ({ - key: integration.provider, + key: `${integration.provider}@${integration.version}`, children: , onClick: () => onSelect(integration), }))} diff --git a/src/modules/integration-picker/hooks/useIntegrationPicker.ts b/src/modules/integration-picker/hooks/useIntegrationPicker.ts index 99a593c..8682c2b 100644 --- a/src/modules/integration-picker/hooks/useIntegrationPicker.ts +++ b/src/modules/integration-picker/hooks/useIntegrationPicker.ts @@ -1,11 +1,13 @@ import { evaluate } from '@stackone/expressions'; import { useQuery } from '@tanstack/react-query'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { isFalconVersion } from '../../../shared/utils/utils'; import { connectAccount, getAccountData, - getConnectorConfig, + getFalconConnectorConfig, getHubData, + getLegacyConnectorConfig, updateAccount, } from '../queries'; import { ConnectorConfigField, Integration } from '../types'; @@ -137,10 +139,26 @@ export const useIntegrationPicker = ({ queryKey: ['connectorData', selectedIntegration?.provider, accountData?.provider], queryFn: async () => { if (selectedIntegration) { - return getConnectorConfig(baseUrl, token, selectedIntegration.provider); + if (isFalconVersion(selectedIntegration.version)) { + return getFalconConnectorConfig( + baseUrl, + token, + `${selectedIntegration.provider}@${selectedIntegration.version}`, + ); + } else { + return getLegacyConnectorConfig(baseUrl, token, selectedIntegration.provider); + } } if (accountData) { - return getConnectorConfig(baseUrl, token, accountData.provider); + if (isFalconVersion(accountData.version)) { + return getFalconConnectorConfig( + baseUrl, + token, + `${accountData.provider}@${accountData.version}`, + ); + } else { + return getLegacyConnectorConfig(baseUrl, token, accountData.provider); + } } return null; }, @@ -153,6 +171,79 @@ export const useIntegrationPicker = ({ return { fields }; } + if ('configFields' in connectorData.config) { + const fieldsWithPrefilledValues: ConnectorConfigField[] = + connectorData.config.configFields + .map((field) => { + const setupValue = accountData?.setupInformation?.[field.key]; + + if (accountData && (field.secret || field.type === 'password')) { + return { + ...field, + key: field.key, + value: DUMMY_VALUE, + }; + } + + if (field.key === 'external-trigger-token') { + return { + ...field, + key: field.key, + value: hubData?.external_trigger_token, + }; + } + + const evaluationContext = { + ...formData, + ...accountData?.setupInformation, + external_trigger_token: hubData?.external_trigger_token, + hub_settings: connectorData.hub_settings, + }; + + if (field.condition) { + const evaluated = evaluate(field.condition, evaluationContext); + + const shouldShow = evaluated != null && evaluated !== 'false'; + + if (!shouldShow) { + return; + } + } + + if (!field.value) { + return { + ...field, + key: field.key, + }; + } + + const valueToEvaluate = setupValue !== undefined ? setupValue : field.value; + let evaluatedValue = evaluate( + valueToEvaluate?.toString(), + evaluationContext, + ); + + if (typeof evaluatedValue === 'object' && evaluatedValue !== null) { + evaluatedValue = JSON.stringify(evaluatedValue); + } + + return { + ...field, + key: field.key, + value: evaluatedValue as string | number | undefined, + }; + }) + .filter((value) => value != null); + + return { + fields: fieldsWithPrefilledValues, + guide: { + supportLink: connectorData.config.support.link, + description: connectorData.config.support.description, + }, + }; + } + const authConfig = connectorData.config.authentication?.[selectedIntegration.authentication_config_key]; const authConfigForEnvironment = authConfig?.[selectedIntegration.environment]; @@ -228,9 +319,12 @@ export const useIntegrationPicker = ({ if (!connectorData || !selectedIntegration) { return null; } - return connectorData.config.authentication?.[ - selectedIntegration.authentication_config_key - ]?.[selectedIntegration.environment]; + if ('authentication' in connectorData.config) { + return connectorData.config.authentication?.[ + selectedIntegration.authentication_config_key + ]?.[selectedIntegration.environment]; + } + return connectorData.config; }, [connectorData, selectedIntegration]); const handleConnect = useCallback(async () => { @@ -318,7 +412,13 @@ export const useIntegrationPicker = ({ cleanedFormData, ); } else { - await connectAccount(baseUrl, token, selectedIntegration.provider, cleanedFormData); + await connectAccount( + baseUrl, + token, + selectedIntegration.provider, + selectedIntegration.version, + cleanedFormData, + ); } setConnectionState({ loading: false, success: true }); diff --git a/src/modules/integration-picker/queries.ts b/src/modules/integration-picker/queries.ts index b42df8f..504e46c 100644 --- a/src/modules/integration-picker/queries.ts +++ b/src/modules/integration-picker/queries.ts @@ -18,9 +18,27 @@ export const getHubData = async (token: string, baseUrl: string, provider?: stri }); }; -export const getConnectorConfig = async (baseUrl: string, token: string, connectorKey: string) => { +export const getLegacyConnectorConfig = async ( + baseUrl: string, + token: string, + connectorKey: string, +) => { return await getRequest({ - url: `${baseUrl}/hub/connectors/${connectorKey}`, + url: `${baseUrl}/hub/connectors/legacy/${connectorKey}`, + headers: { + 'Content-Type': 'application/json', + 'x-hub-session-token': token, + }, + }); +}; + +export const getFalconConnectorConfig = async ( + baseUrl: string, + token: string, + connectorKey: string, +) => { + return await getRequest({ + url: `${baseUrl}/hub/connectors/falcon/${encodeURIComponent(connectorKey)}`, headers: { 'Content-Type': 'application/json', 'x-hub-session-token': token, @@ -32,6 +50,7 @@ export const connectAccount = async ( baseUrl: string, token: string, provider: string, + version: string, credentials: Record, ) => { return await postRequest({ @@ -41,7 +60,7 @@ export const connectAccount = async ( 'x-hub-session-token': token, }, body: { - provider, + provider: `${provider}@${version}`, credentials, }, }); diff --git a/src/modules/integration-picker/types.ts b/src/modules/integration-picker/types.ts index 9f5cf98..6fe9e19 100644 --- a/src/modules/integration-picker/types.ts +++ b/src/modules/integration-picker/types.ts @@ -55,8 +55,19 @@ export interface ConnectorConfig { }; } +export interface FalconConnectorConfig { + key: string; + name: string; + type: 'oauth2' | 'custom'; + configFields: Array; + support: { + link: string; + description: string; + }; +} + export interface HubConnectorConfig { - config: ConnectorConfig; + config: ConnectorConfig | FalconConnectorConfig; hub_settings: { configured_webhook_events: Record>; project_settings: Record; @@ -67,4 +78,5 @@ export interface AccountData { account_id: string; provider: string; setupInformation: Record; + version: string; } diff --git a/src/shared/utils/utils.ts b/src/shared/utils/utils.ts new file mode 100644 index 0000000..33f87d5 --- /dev/null +++ b/src/shared/utils/utils.ts @@ -0,0 +1,3 @@ +export const isFalconVersion = (version: string) => { + return version != null && version != '1' && version != '2'; +}; From 7bb51a8077a37d3f8ac7a92ae2744517368cfd17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20De=20Freitas?= <6485562+adefreitas@users.noreply.github.com> Date: Tue, 26 Aug 2025 17:03:22 +0100 Subject: [PATCH 2/2] chore: address copilot comments --- .../hooks/useIntegrationPicker.ts | 11 ++++++++--- src/modules/integration-picker/types.ts | 15 +++++++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/modules/integration-picker/hooks/useIntegrationPicker.ts b/src/modules/integration-picker/hooks/useIntegrationPicker.ts index 8682c2b..1b4c4aa 100644 --- a/src/modules/integration-picker/hooks/useIntegrationPicker.ts +++ b/src/modules/integration-picker/hooks/useIntegrationPicker.ts @@ -10,7 +10,12 @@ import { getLegacyConnectorConfig, updateAccount, } from '../queries'; -import { ConnectorConfigField, Integration } from '../types'; +import { + ConnectorConfigField, + Integration, + isFalconConnectorConfig, + isLegacyConnectorConfig, +} from '../types'; const DUMMY_VALUE = 'totally-fake-value'; @@ -171,7 +176,7 @@ export const useIntegrationPicker = ({ return { fields }; } - if ('configFields' in connectorData.config) { + if (isFalconConnectorConfig(connectorData.config)) { const fieldsWithPrefilledValues: ConnectorConfigField[] = connectorData.config.configFields .map((field) => { @@ -319,7 +324,7 @@ export const useIntegrationPicker = ({ if (!connectorData || !selectedIntegration) { return null; } - if ('authentication' in connectorData.config) { + if (isLegacyConnectorConfig(connectorData.config)) { return connectorData.config.authentication?.[ selectedIntegration.authentication_config_key ]?.[selectedIntegration.environment]; diff --git a/src/modules/integration-picker/types.ts b/src/modules/integration-picker/types.ts index 6fe9e19..322a5be 100644 --- a/src/modules/integration-picker/types.ts +++ b/src/modules/integration-picker/types.ts @@ -38,7 +38,7 @@ export interface ConnectorConfigField { }; } -export interface ConnectorConfig { +export interface LegacyConnectorConfig { key: string; name: string; authentication: { @@ -66,14 +66,25 @@ export interface FalconConnectorConfig { }; } +export type ConnectorConfig = LegacyConnectorConfig | FalconConnectorConfig; + export interface HubConnectorConfig { - config: ConnectorConfig | FalconConnectorConfig; + config: ConnectorConfig; hub_settings: { configured_webhook_events: Record>; project_settings: Record; }; } +// Type guards for safe type checking - using structural properties instead of explicit type field +export function isLegacyConnectorConfig(config: ConnectorConfig): config is LegacyConnectorConfig { + return 'authentication' in config && !('configFields' in config); +} + +export function isFalconConnectorConfig(config: ConnectorConfig): config is FalconConnectorConfig { + return 'configFields' in config && !('authentication' in config); +} + export interface AccountData { account_id: string; provider: string;