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
2 changes: 2 additions & 0 deletions src/modules/integration-picker/IntegrationPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const IntegrationPicker: React.FC<IntegrationPickerProps> = ({
connectorData,
selectedIntegration,
fields,
notices,
guide,

// State
Expand Down Expand Up @@ -152,6 +153,7 @@ export const IntegrationPicker: React.FC<IntegrationPickerProps> = ({
connectorData={connectorData?.config ?? null}
hubData={hubData ?? null}
fields={fields}
notices={notices}
errorHubData={(errorHubData as Error) ?? null}
errorConnectorData={(errorConnectorData as Error) ?? null}
onSelect={setSelectedIntegration}
Expand Down
85 changes: 62 additions & 23 deletions src/modules/integration-picker/components/IntegrationFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import {
TextArea,
Typography,
} from '@stackone/malachite';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useMemo } from 'react';
import { FieldErrors, UseFormSetValue } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import useDeepCompareEffect from 'use-deep-compare-effect';
import { ConnectorConfigField } from '../types';
import { AuthenticationNotice, ConnectorConfigField } from '../types';
import { partitionNotices } from '../utils/partitionNotices';
import { formatSecretPlaceholder, isSecretPlaceholder } from '../utils/secretPlaceholder';
import { createFormSchema } from '../utils/zodSchema';

Expand Down Expand Up @@ -215,6 +216,7 @@ const ErrorBlock = ({ error }: { error?: { message: string; provider_response: s
};
interface IntegrationFieldsProps {
fields: Array<ConnectorConfigField>;
notices?: Array<AuthenticationNotice>;
error?: {
message: string;
provider_response: string;
Expand All @@ -229,7 +231,9 @@ interface IntegrationFieldsProps {
const NoFieldsView: React.FC<{
integrationName: string;
error?: { message: string; provider_response: string };
}> = ({ integrationName, error }) => {
notices?: AuthenticationNotice[];
}> = ({ integrationName, error, notices = [] }) => {
const topNotices = notices.filter((n) => !n.position || n.position === 'top');
return (
<Padded vertical="large" horizontal="medium" overflow="auto" fullHeight>
{error && (
Expand All @@ -241,6 +245,9 @@ const NoFieldsView: React.FC<{
<ErrorBlock error={error} />
</Alert>
)}
{topNotices.map((n) => (
<Alert key={n.key} type={n.type} message={n.description} hasMargin={false} />
))}
<Flex
direction={FlexDirection.Vertical}
gapSize={FlexGapSize.Small}
Expand All @@ -265,13 +272,19 @@ const NoFieldsView: React.FC<{

export const IntegrationForm: React.FC<IntegrationFieldsProps> = ({
fields,
notices = [],
onChange,
error,
onValidationChange,
integrationName,
editingSecrets,
setEditingSecrets,
}) => {
const displayedFields = fields.filter((f) => f.display !== false);
const fieldKeys = displayedFields.map((f) =>
typeof f.key === 'object' ? JSON.stringify(f.key) : String(f.key),
);
const { noticesBefore, noticesAfter } = partitionNotices(notices, fieldKeys);
const schema = useMemo(() => createFormSchema(fields), [fields]);

const defaultValues = useMemo(() => {
Expand Down Expand Up @@ -308,8 +321,8 @@ export const IntegrationForm: React.FC<IntegrationFieldsProps> = ({
onValidationChange?.(isValid);
}, [isValid, onValidationChange]);

if (fields.length === 0) {
return <NoFieldsView integrationName={integrationName} error={error} />;
if (displayedFields.length === 0) {
return <NoFieldsView integrationName={integrationName} error={error} notices={notices} />;
}

return (
Expand All @@ -321,25 +334,51 @@ export const IntegrationForm: React.FC<IntegrationFieldsProps> = ({
</Alert>
)}
<Form>
{fields
.filter((field) => field.display !== false)
.map((field) => {
const key =
typeof field.key === 'object'
? JSON.stringify(field.key)
: String(field.key);
return (
<div key={key} style={{ width: '100%' }}>
<FieldRenderer
field={field}
errors={errors}
setValue={setValue}
editingSecrets={editingSecrets}
setEditingSecrets={setEditingSecrets}
{displayedFields.map((field) => {
const key =
typeof field.key === 'object'
? JSON.stringify(field.key)
: String(field.key);
const hasNotices =
noticesBefore(key).length > 0 || noticesAfter(key).length > 0;
return (
<div
key={key}
style={{
...(hasNotices && {
display: 'flex',
flexDirection: 'column',
gap: '8px',
}),
width: '100%',
}}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This wrapper used to be <div style={{ width: '100%' }}> and is now flex column with gap: 8px applied unconditionally. That changes vertical spacing for every field, even ones with no notices attached. Worth a visual QA pass across existing integrations to make sure nothing regresses — or only apply the flex/gap styles when there are notices to render.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated.

>
{noticesBefore(key).map((n) => (
<Alert
key={n.key}
type={n.type}
message={n.description}
hasMargin={false}
/>
))}
<FieldRenderer
field={field}
errors={errors}
setValue={setValue}
editingSecrets={editingSecrets}
setEditingSecrets={setEditingSecrets}
/>
{noticesAfter(key).map((n) => (
<Alert
key={n.key}
type={n.type}
message={n.description}
hasMargin={false}
/>
</div>
);
})}
))}
</div>
);
})}
</Form>
</Spacer>
</Padded>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import React from 'react';
import { ConnectorConfig, ConnectorConfigField, HubData, Integration } from '../types';
import {
AuthenticationNotice,
ConnectorConfig,
ConnectorConfigField,
HubData,
Integration,
} from '../types';
import { ErrorView } from './views/ErrorView';
import { IntegrationFormView } from './views/IntegrationFormView';
import { IntegrationListView } from './views/IntegrationListView';
Expand All @@ -24,6 +30,7 @@ interface IntegrationPickerContentProps {
connectorData: ConnectorConfig | null;
hubData: HubData | null;
fields: ConnectorConfigField[];
notices?: AuthenticationNotice[];
selectedCategory: string | null;
search: string;

Expand All @@ -48,6 +55,7 @@ export const IntegrationPickerContent: React.FC<IntegrationPickerContentProps> =
connectorData,
hubData,
fields,
notices,
selectedCategory,
search,
errorHubData,
Expand Down Expand Up @@ -113,6 +121,7 @@ export const IntegrationPickerContent: React.FC<IntegrationPickerContentProps> =
return (
<IntegrationFormView
fields={fields}
notices={notices}
error={connectionState.error}
onChange={onChange}
onValidationChange={onValidationChange}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from 'react';
import { ConnectorConfigField } from '../../types';
import { AuthenticationNotice, ConnectorConfigField } from '../../types';
import { IntegrationForm } from '../IntegrationFields';

interface IntegrationFormViewProps {
fields: ConnectorConfigField[];
notices?: AuthenticationNotice[];
error?: {
message: string;
provider_response: string;
Expand All @@ -17,6 +18,7 @@ interface IntegrationFormViewProps {

export const IntegrationFormView: React.FC<IntegrationFormViewProps> = ({
fields,
notices,
error,
onChange,
onValidationChange,
Expand All @@ -27,6 +29,7 @@ export const IntegrationFormView: React.FC<IntegrationFormViewProps> = ({
return (
<IntegrationForm
fields={fields}
notices={notices}
error={error}
onChange={onChange}
onValidationChange={onValidationChange}
Expand Down
113 changes: 58 additions & 55 deletions src/modules/integration-picker/hooks/useIntegrationPicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
updateAccount,
} from '../queries';
import {
AuthenticationNotice,
ConnectorConfigField,
Integration,
isFalconConnectorConfig,
Expand Down Expand Up @@ -331,88 +332,88 @@ export const useIntegrationPicker = ({
...RETRY_CONFIG,
});

const { fields, guide } = useMemo(() => {
const { fields, guide, notices } = useMemo(() => {
if (!connectorData || !selectedIntegration) {
const fields: ConnectorConfigField[] = [];
return { fields };
const notices: AuthenticationNotice[] = [];
return { fields, notices };
}

if (isFalconConnectorConfig(connectorData.config)) {
const fieldsWithPrefilledValues: ConnectorConfigField[] =
connectorData.config.configFields
.map((field) => {
const setupValue = accountData?.setupInformation?.[field.key];
const fieldsWithPrefilledValues: ConnectorConfigField[] = (
connectorData.config.configFields ?? []
)
.map((field) => {
const setupValue = accountData?.setupInformation?.[field.key];

if (field.key === 'external-trigger-token') {
return {
...field,
key: field.key,
value: hubData?.external_trigger_token,
};
}
if (field.key === 'external-trigger-token') {
return {
...field,
key: field.key,
value: hubData?.external_trigger_token,
};
}

if (accountData && (field.secret !== false || field.type === 'password')) {
const secretValue = accountData.secrets?.[field.key];
if (secretValue) {
return {
...field,
key: field.key,
value: secretValue,
};
}
if (accountData && (field.secret !== false || field.type === 'password')) {
const secretValue = accountData.secrets?.[field.key];
if (secretValue) {
return {
...field,
key: field.key,
value: '',
value: secretValue,
};
}

const evaluationContext = {
...formData,
...accountData?.setupInformation,
external_trigger_token: hubData?.external_trigger_token,
webhooks_url: hubData?.webhooks_url,
events_encoded_context: hubData?.events_encoded_context,
hub_settings: connectorData.hub_settings,
return {
...field,
key: field.key,
value: '',
};
}

if (field.condition) {
const evaluated = evaluate(field.condition, evaluationContext);
const evaluationContext = {
...formData,
...accountData?.setupInformation,
external_trigger_token: hubData?.external_trigger_token,
webhooks_url: hubData?.webhooks_url,
events_encoded_context: hubData?.events_encoded_context,
hub_settings: connectorData.hub_settings,
};

const shouldShow = evaluated != null && evaluated !== 'false';
if (field.condition) {
const evaluated = evaluate(field.condition, evaluationContext);

if (!shouldShow) {
return;
}
}
const shouldShow = evaluated != null && evaluated !== 'false';

const valueToEvaluate = setupValue !== undefined ? setupValue : field.value;

if (!valueToEvaluate) {
return {
...field,
key: field.key,
};
if (!shouldShow) {
return;
}
let evaluatedValue = evaluate(
valueToEvaluate?.toString(),
evaluationContext,
);
}

if (typeof evaluatedValue === 'object' && evaluatedValue !== null) {
evaluatedValue = JSON.stringify(evaluatedValue);
}
const valueToEvaluate = setupValue !== undefined ? setupValue : field.value;

if (!valueToEvaluate) {
return {
...field,
key: field.key,
value: evaluatedValue as string | number | undefined,
};
})
.filter((value) => value != null);
}
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,
notices: connectorData.config.configNotices ?? [],
guide: {
supportLink: connectorData.config.support?.link,
description: connectorData.config.support?.description ?? '',
Expand Down Expand Up @@ -507,6 +508,7 @@ export const useIntegrationPicker = ({

return {
fields: fieldsWithPrefilledValues,
notices: [],
guide: authConfigForEnvironment?.guide,
};
}, [connectorData, selectedIntegration, accountData, formData, hubData]);
Expand Down Expand Up @@ -901,6 +903,7 @@ export const useIntegrationPicker = ({
connectorData,
selectedIntegration,
fields,
notices,
guide,

// State
Expand Down
Loading
Loading