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
15 changes: 14 additions & 1 deletion src/modules/integration-picker/components/IntegrationFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,25 @@ export const IntegrationForm: React.FC<IntegrationFieldsProps> = ({ fields, onCh
}));
};

const errorJson = () => {
if (!error) {
return null;
}
try {
return <CodeBlock json={JSON.parse(error.provider_response)} />;
} catch (_e) {
if (error?.provider_response && error?.provider_response.length > 0) {
return <CodeBlock code={error?.provider_response} />;
}
return null;
}
};
return (
<Padded vertical="large" horizontal="medium">
<Spacer direction="vertical" size={8} fullWidth>
{error && (
<Alert type="error" message={error.message} hasMargin={false}>
<CodeBlock json={JSON.parse(error.provider_response)} />
{errorJson()}
</Alert>
)}
<Spacer direction="vertical" size={20} fullWidth>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -43,6 +44,9 @@ const IntegrationRow: React.FC<IntegrationRowProps> = ({ integration }) => {
/>
<Typography.Text>{integration.name ?? 'N/A'}</Typography.Text>
</Flex>
{isFalconVersion(integration.version) && (
<Typography.SecondaryText>{integration.version}</Typography.SecondaryText>
)}
<Typography.SecondaryText>
{
CATEGORIES_WITH_LABELS.find((category) => category.value === integration.type)
Expand Down Expand Up @@ -123,7 +127,7 @@ export const IntegrationList: React.FC<{
</Padded>
<ButtonList
buttons={availableIntegrations.map((integration) => ({
key: integration.provider,
key: `${integration.provider}@${integration.version}`,
children: <IntegrationRow integration={integration} />,
onClick: () => onSelect(integration),
}))}
Expand Down
121 changes: 113 additions & 8 deletions src/modules/integration-picker/hooks/useIntegrationPicker.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
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';
import {
ConnectorConfigField,
Integration,
isFalconConnectorConfig,
isLegacyConnectorConfig,
} from '../types';

const DUMMY_VALUE = 'totally-fake-value';

Expand Down Expand Up @@ -137,10 +144,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;
},
Expand All @@ -153,6 +176,79 @@ export const useIntegrationPicker = ({
return { fields };
}

if (isFalconConnectorConfig(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];
Expand Down Expand Up @@ -228,9 +324,12 @@ export const useIntegrationPicker = ({
if (!connectorData || !selectedIntegration) {
return null;
}
return connectorData.config.authentication?.[
selectedIntegration.authentication_config_key
]?.[selectedIntegration.environment];
if (isLegacyConnectorConfig(connectorData.config)) {
return connectorData.config.authentication?.[
selectedIntegration.authentication_config_key
]?.[selectedIntegration.environment];
}
return connectorData.config;
}, [connectorData, selectedIntegration]);

const handleConnect = useCallback(async () => {
Expand Down Expand Up @@ -318,7 +417,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 });
Expand Down
25 changes: 22 additions & 3 deletions src/modules/integration-picker/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HubConnectorConfig>({
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<HubConnectorConfig>({
url: `${baseUrl}/hub/connectors/falcon/${encodeURIComponent(connectorKey)}`,
headers: {
'Content-Type': 'application/json',
'x-hub-session-token': token,
Expand All @@ -32,6 +50,7 @@ export const connectAccount = async (
baseUrl: string,
token: string,
provider: string,
version: string,
credentials: Record<string, unknown>,
) => {
return await postRequest<ConnectorConfig>({
Expand All @@ -41,7 +60,7 @@ export const connectAccount = async (
'x-hub-session-token': token,
},
body: {
provider,
provider: `${provider}@${version}`,
credentials,
},
});
Expand Down
25 changes: 24 additions & 1 deletion src/modules/integration-picker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface ConnectorConfigField {
};
}

export interface ConnectorConfig {
export interface LegacyConnectorConfig {
key: string;
name: string;
authentication: {
Expand All @@ -55,6 +55,19 @@ export interface ConnectorConfig {
};
}

export interface FalconConnectorConfig {
key: string;
name: string;
type: 'oauth2' | 'custom';
configFields: Array<ConnectorConfigField>;
support: {
link: string;
description: string;
};
}

export type ConnectorConfig = LegacyConnectorConfig | FalconConnectorConfig;

export interface HubConnectorConfig {
config: ConnectorConfig;
hub_settings: {
Expand All @@ -63,8 +76,18 @@ export interface HubConnectorConfig {
};
}

// 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;
setupInformation: Record<string, string>;
version: string;
}
3 changes: 3 additions & 0 deletions src/shared/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const isFalconVersion = (version: string) => {
return version != null && version != '1' && version != '2';
Comment on lines +1 to +2

Copilot AI Aug 26, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function logic assumes any version that isn't null, '1', or '2' is a Falcon version. This could incorrectly classify unexpected version values. Consider explicitly checking for known Falcon version patterns or adding validation for expected version formats.

Suggested change
export const isFalconVersion = (version: string) => {
return version != null && version != '1' && version != '2';
// Falcon versions are expected to match the pattern 'falcon-x' or 'falcon-x.y', where x and y are numbers
if (typeof version !== 'string') return false;
const falconVersionPattern = /^falcon-\d+(\.\d+)?$/;
return falconVersionPattern.test(version);

Copilot uses AI. Check for mistakes.
};