Add stripe connection option (#5372)

- Refactor creation and edition form so it handles stripe integration
and not only postgres
- Add a hook `useIsSettingsIntegrationEnabled` to avoid checking feature
flags everywhere
- Add zod schema for stripe

<img width="250" alt="Capture d’écran 2024-05-13 à 12 41 52"
src="https://github.com/twentyhq/twenty/assets/22936103/a77e7278-5d79-4f95-bddb-ae9ddd1426eb">
<img width="250" alt="Capture d’écran 2024-05-13 à 12 41 59"
src="https://github.com/twentyhq/twenty/assets/22936103/d617dc6a-31a4-43c8-8192-dbfb7157de1c">
<img width="250" alt="Capture d’écran 2024-05-13 à 12 42 08"
src="https://github.com/twentyhq/twenty/assets/22936103/c4e2d0e4-f826-436d-89be-4d1679a27861">

---------

Co-authored-by: Thomas Trompette <thomast@twenty.com>
This commit is contained in:
Thomas Trompette
2024-05-13 18:00:13 +02:00
committed by GitHub
parent b9154f315e
commit de438b0171
19 changed files with 251 additions and 100 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -1,6 +1,5 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { ApolloClient, useApolloClient, useMutation } from '@apollo/client'; import { ApolloClient, useApolloClient, useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { SYNC_REMOTE_TABLE } from '@/databases/graphql/mutations/syncRemoteTable'; import { SYNC_REMOTE_TABLE } from '@/databases/graphql/mutations/syncRemoteTable';
import { GET_MANY_REMOTE_TABLES } from '@/databases/graphql/queries/findManyRemoteTables'; import { GET_MANY_REMOTE_TABLES } from '@/databases/graphql/queries/findManyRemoteTables';
@ -39,7 +38,16 @@ export const useSyncRemoteTable = () => {
input, input,
}, },
awaitRefetchQueries: true, awaitRefetchQueries: true,
refetchQueries: [getOperationName(GET_MANY_REMOTE_TABLES) ?? ''], refetchQueries: [
{
query: GET_MANY_REMOTE_TABLES,
variables: {
input: {
id: input.remoteServerId,
},
},
},
],
}); });
// TODO: we should return the tables with the columns and store in cache instead of refetching // TODO: we should return the tables with the columns and store in cache instead of refetching

View File

@ -1,6 +1,5 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { ApolloClient, useMutation } from '@apollo/client'; import { ApolloClient, useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { UNSYNC_REMOTE_TABLE } from '@/databases/graphql/mutations/unsyncRemoteTable'; import { UNSYNC_REMOTE_TABLE } from '@/databases/graphql/mutations/unsyncRemoteTable';
import { GET_MANY_REMOTE_TABLES } from '@/databases/graphql/queries/findManyRemoteTables'; import { GET_MANY_REMOTE_TABLES } from '@/databases/graphql/queries/findManyRemoteTables';
@ -31,7 +30,16 @@ export const useUnsyncRemoteTable = () => {
input, input,
}, },
awaitRefetchQueries: true, awaitRefetchQueries: true,
refetchQueries: [getOperationName(GET_MANY_REMOTE_TABLES) ?? ''], refetchQueries: [
{
query: GET_MANY_REMOTE_TABLES,
variables: {
input: {
id: input.remoteServerId,
},
},
},
],
}); });
await refetchObjectMetadataItems(); await refetchObjectMetadataItems();

View File

@ -2,6 +2,8 @@ export const getForeignDataWrapperType = (databaseKey: string) => {
switch (databaseKey) { switch (databaseKey) {
case 'postgresql': case 'postgresql':
return 'postgres_fdw'; return 'postgres_fdw';
case 'stripe':
return 'stripe_fdw';
default: default:
return null; return null;
} }

View File

@ -7,4 +7,8 @@ export const MOCK_REMOTE_DATABASES = [
name: 'postgresql', name: 'postgresql',
isActive: true, isActive: true,
}, },
{
name: 'stripe',
isActive: true,
},
]; ];

View File

@ -17,6 +17,14 @@ type SettingsIntegrationPostgreSQLConnectionFormValues = z.infer<
typeof settingsIntegrationPostgreSQLConnectionFormSchema typeof settingsIntegrationPostgreSQLConnectionFormSchema
>; >;
export const settingsIntegrationStripeConnectionFormSchema = z.object({
api_key: z.string().min(1),
});
type SettingsIntegrationStripeConnectionFormValues = z.infer<
typeof settingsIntegrationStripeConnectionFormSchema
>;
const StyledInputsContainer = styled.div` const StyledInputsContainer = styled.div`
display: grid; display: grid;
gap: ${({ theme }) => theme.spacing(2, 4)}; gap: ${({ theme }) => theme.spacing(2, 4)};
@ -31,19 +39,35 @@ const StyledInputsContainer = styled.div`
} }
`; `;
type SettingsIntegrationPostgreSQLConnectionFormProps = { type SettingsIntegrationDatabaseConnectionFormProps = {
databaseKey: string;
disabled?: boolean; disabled?: boolean;
}; };
export const SettingsIntegrationPostgreSQLConnectionForm = ({ type SettingsIntegrationConnectionFormValues =
disabled, | SettingsIntegrationPostgreSQLConnectionFormValues
}: SettingsIntegrationPostgreSQLConnectionFormProps) => { | SettingsIntegrationStripeConnectionFormValues;
const { control } =
useFormContext<SettingsIntegrationPostgreSQLConnectionFormValues>();
return ( const getFormFields = (
<StyledInputsContainer> databaseKey: string,
{[ ):
| {
name:
| 'dbname'
| 'host'
| 'port'
| 'user'
| 'password'
| 'schema'
| 'api_key';
label: string;
type?: string;
placeholder: string;
}[]
| null => {
switch (databaseKey) {
case 'postgresql':
return [
{ {
name: 'dbname' as const, name: 'dbname' as const,
label: 'Database Name', label: 'Database Name',
@ -63,7 +87,28 @@ export const SettingsIntegrationPostgreSQLConnectionForm = ({
placeholder: '••••••', placeholder: '••••••',
}, },
{ name: 'schema' as const, label: 'Schema', placeholder: 'public' }, { name: 'schema' as const, label: 'Schema', placeholder: 'public' },
].map(({ name, label, type, placeholder }) => ( ];
case 'stripe':
return [
{ name: 'api_key' as const, label: 'API Key', placeholder: 'API key' },
];
default:
return null;
}
};
export const SettingsIntegrationDatabaseConnectionForm = ({
databaseKey,
disabled,
}: SettingsIntegrationDatabaseConnectionFormProps) => {
const { control } = useFormContext<SettingsIntegrationConnectionFormValues>();
const formFields = getFormFields(databaseKey);
if (!formFields) return null;
return (
<StyledInputsContainer>
{formFields.map(({ name, label, type, placeholder }) => (
<Controller <Controller
key={name} key={name}
name={name} name={name}
@ -71,7 +116,7 @@ export const SettingsIntegrationPostgreSQLConnectionForm = ({
render={({ field: { onChange, value } }) => { render={({ field: { onChange, value } }) => {
return ( return (
<TextInput <TextInput
autoComplete="new-password" autoComplete="new-password" // Disable autocomplete
label={label} label={label}
value={value} value={value}
onChange={onChange} onChange={onChange}

View File

@ -8,7 +8,7 @@ import { z } from 'zod';
import { useUpdateOneDatabaseConnection } from '@/databases/hooks/useUpdateOneDatabaseConnection'; import { useUpdateOneDatabaseConnection } from '@/databases/hooks/useUpdateOneDatabaseConnection';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer'; import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsIntegrationPostgreSQLConnectionForm } from '@/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionForm'; import { SettingsIntegrationDatabaseConnectionForm } from '@/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionForm';
import { import {
formatValuesForUpdate, formatValuesForUpdate,
getEditionSchemaForForm, getEditionSchemaForForm,
@ -132,18 +132,17 @@ export const SettingsIntegrationEditDatabaseConnectionContent = ({
accent={'blue'} accent={'blue'}
/> />
)} )}
{databaseKey === 'postgresql' ? ( <Section>
<Section> <H2Title
<H2Title title="Edit Connection"
title="Edit PostgreSQL Connection" description="Edit the information to connect your database"
description="Edit the information to connect your PostgreSQL database" />
/>
<SettingsIntegrationPostgreSQLConnectionForm <SettingsIntegrationDatabaseConnectionForm
disabled={hasSyncedTables} databaseKey={databaseKey}
/> disabled={hasSyncedTables}
</Section> />
) : null} </Section>
</FormProvider> </FormProvider>
</> </>
); );

View File

@ -3,9 +3,9 @@ import { useNavigate, useParams } from 'react-router-dom';
import { useGetDatabaseConnection } from '@/databases/hooks/useGetDatabaseConnection'; import { useGetDatabaseConnection } from '@/databases/hooks/useGetDatabaseConnection';
import { useGetDatabaseConnectionTables } from '@/databases/hooks/useGetDatabaseConnectionTables'; import { useGetDatabaseConnectionTables } from '@/databases/hooks/useGetDatabaseConnectionTables';
import { useIsSettingsIntegrationEnabled } from '@/settings/integrations/hooks/useIsSettingsIntegrationEnabled';
import { useSettingsIntegrationCategories } from '@/settings/integrations/hooks/useSettingsIntegrationCategories'; import { useSettingsIntegrationCategories } from '@/settings/integrations/hooks/useSettingsIntegrationCategories';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
export const useDatabaseConnection = () => { export const useDatabaseConnection = () => {
const { databaseKey = '', connectionId = '' } = useParams(); const { databaseKey = '', connectionId = '' } = useParams();
@ -16,16 +16,9 @@ export const useDatabaseConnection = () => {
({ from: { key } }) => key === databaseKey, ({ from: { key } }) => key === databaseKey,
); );
const isAirtableIntegrationEnabled = useIsFeatureEnabled( const isIntegrationEnabled = useIsSettingsIntegrationEnabled(databaseKey);
'IS_AIRTABLE_INTEGRATION_ENABLED',
); const isIntegrationAvailable = !!integration && isIntegrationEnabled;
const isPostgresqlIntegrationEnabled = useIsFeatureEnabled(
'IS_POSTGRESQL_INTEGRATION_ENABLED',
);
const isIntegrationAvailable =
!!integration &&
((databaseKey === 'airtable' && isAirtableIntegrationEnabled) ||
(databaseKey === 'postgresql' && isPostgresqlIntegrationEnabled));
const { connection, loading } = useGetDatabaseConnection({ const { connection, loading } = useGetDatabaseConnection({
databaseKey, databaseKey,

View File

@ -3,7 +3,10 @@ import isEmpty from 'lodash.isempty';
import pickBy from 'lodash.pickby'; import pickBy from 'lodash.pickby';
import { z } from 'zod'; import { z } from 'zod';
import { settingsIntegrationPostgreSQLConnectionFormSchema } from '@/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionForm'; import {
settingsIntegrationPostgreSQLConnectionFormSchema,
settingsIntegrationStripeConnectionFormSchema,
} from '@/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionForm';
import { RemoteServer } from '~/generated-metadata/graphql'; import { RemoteServer } from '~/generated-metadata/graphql';
export const getEditionSchemaForForm = (databaseKey: string) => { export const getEditionSchemaForForm = (databaseKey: string) => {
@ -12,6 +15,8 @@ export const getEditionSchemaForForm = (databaseKey: string) => {
return settingsIntegrationPostgreSQLConnectionFormSchema.extend({ return settingsIntegrationPostgreSQLConnectionFormSchema.extend({
password: z.string().optional(), password: z.string().optional(),
}); });
case 'stripe':
return settingsIntegrationStripeConnectionFormSchema;
default: default:
throw new Error(`No schema found for database key: ${databaseKey}`); throw new Error(`No schema found for database key: ${databaseKey}`);
} }
@ -34,6 +39,10 @@ export const getFormDefaultValuesFromConnection = ({
schema: connection.schema || undefined, schema: connection.schema || undefined,
password: '', password: '',
}; };
case 'stripe':
return {
api_key: connection.foreignDataWrapperOptions.api_key,
};
default: default:
throw new Error( throw new Error(
`No default form values for database key: ${databaseKey}`, `No default form values for database key: ${databaseKey}`,
@ -71,6 +80,12 @@ export const formatValuesForUpdate = ({
return pickBy(formattedValues, (obj) => !isEmpty(obj)); return pickBy(formattedValues, (obj) => !isEmpty(obj));
} }
case 'stripe':
return {
foreignDataWrapperOptions: {
api_key: formValues.api_key,
},
};
default: default:
throw new Error(`Cannot format values for database key: ${databaseKey}`); throw new Error(`Cannot format values for database key: ${databaseKey}`);
} }

View File

@ -0,0 +1,21 @@
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
const getFeatureKey = (databaseKey: string) => {
switch (databaseKey) {
case 'airtable':
return 'IS_AIRTABLE_INTEGRATION_ENABLED';
case 'postgresql':
return 'IS_POSTGRESQL_INTEGRATION_ENABLED';
case 'stripe':
return 'IS_STRIPE_INTEGRATION_ENABLED';
default:
return null;
}
};
export const useIsSettingsIntegrationEnabled = (
databaseKey: string,
): boolean => {
const featureKey = getFeatureKey(databaseKey);
return useIsFeatureEnabled(featureKey);
};

View File

@ -14,6 +14,7 @@ export const useSettingsIntegrationCategories =
const isAirtableIntegrationActive = !!MOCK_REMOTE_DATABASES.find( const isAirtableIntegrationActive = !!MOCK_REMOTE_DATABASES.find(
({ name }) => name === 'airtable', ({ name }) => name === 'airtable',
)?.isActive; )?.isActive;
const isPostgresqlIntegrationEnabled = useIsFeatureEnabled( const isPostgresqlIntegrationEnabled = useIsFeatureEnabled(
'IS_POSTGRESQL_INTEGRATION_ENABLED', 'IS_POSTGRESQL_INTEGRATION_ENABLED',
); );
@ -21,12 +22,21 @@ export const useSettingsIntegrationCategories =
({ name }) => name === 'postgresql', ({ name }) => name === 'postgresql',
)?.isActive; )?.isActive;
const isStripeIntegrationEnabled = useIsFeatureEnabled(
'IS_STRIPE_INTEGRATION_ENABLED',
);
const isStripeIntegrationActive = !!MOCK_REMOTE_DATABASES.find(
({ name }) => name === 'stripe',
)?.isActive;
return [ return [
getSettingsIntegrationAll({ getSettingsIntegrationAll({
isAirtableIntegrationEnabled, isAirtableIntegrationEnabled,
isAirtableIntegrationActive, isAirtableIntegrationActive,
isPostgresqlIntegrationEnabled, isPostgresqlIntegrationEnabled,
isPostgresqlIntegrationActive, isPostgresqlIntegrationActive,
isStripeIntegrationEnabled,
isStripeIntegrationActive,
}), }),
SETTINGS_INTEGRATION_ZAPIER_CATEGORY, SETTINGS_INTEGRATION_ZAPIER_CATEGORY,
SETTINGS_INTEGRATION_WINDMILL_CATEGORY, SETTINGS_INTEGRATION_WINDMILL_CATEGORY,

View File

@ -9,7 +9,13 @@ type GetConnectionDbNameParams = {
export const getConnectionDbName = ({ export const getConnectionDbName = ({
integration, integration,
connection, connection,
}: GetConnectionDbNameParams) => }: GetConnectionDbNameParams) => {
integration.from.key === 'postgresql' switch (integration.from.key) {
? connection.foreignDataWrapperOptions?.dbname case 'postgresql':
: ''; return connection.foreignDataWrapperOptions?.dbname;
case 'stripe':
return connection.id;
default:
return '';
}
};

View File

@ -5,11 +5,15 @@ export const getSettingsIntegrationAll = ({
isAirtableIntegrationActive, isAirtableIntegrationActive,
isPostgresqlIntegrationEnabled, isPostgresqlIntegrationEnabled,
isPostgresqlIntegrationActive, isPostgresqlIntegrationActive,
isStripeIntegrationEnabled,
isStripeIntegrationActive,
}: { }: {
isAirtableIntegrationEnabled: boolean; isAirtableIntegrationEnabled: boolean;
isAirtableIntegrationActive: boolean; isAirtableIntegrationActive: boolean;
isPostgresqlIntegrationEnabled: boolean; isPostgresqlIntegrationEnabled: boolean;
isPostgresqlIntegrationActive: boolean; isPostgresqlIntegrationActive: boolean;
isStripeIntegrationEnabled: boolean;
isStripeIntegrationActive: boolean;
}): SettingsIntegrationCategory => ({ }): SettingsIntegrationCategory => ({
key: 'all', key: 'all',
title: 'All', title: 'All',
@ -40,5 +44,18 @@ export const getSettingsIntegrationAll = ({
text: 'PostgreSQL', text: 'PostgreSQL',
link: '/settings/integrations/postgresql', link: '/settings/integrations/postgresql',
}, },
{
from: {
key: 'stripe',
image: '/images/integrations/stripe-logo.png',
},
type: !isStripeIntegrationEnabled
? 'Soon'
: isStripeIntegrationActive
? 'Active'
: 'Add',
text: 'Stripe',
link: '/settings/integrations/stripe',
},
], ],
}); });

View File

@ -3,9 +3,13 @@ import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { FeatureFlagKey } from '@/workspace/types/FeatureFlagKey'; import { FeatureFlagKey } from '@/workspace/types/FeatureFlagKey';
export const useIsFeatureEnabled = (featureKey: FeatureFlagKey) => { export const useIsFeatureEnabled = (featureKey: FeatureFlagKey | null) => {
const currentWorkspace = useRecoilValue(currentWorkspaceState); const currentWorkspace = useRecoilValue(currentWorkspaceState);
if (!featureKey) {
return false;
}
const featureFlag = currentWorkspace?.featureFlags?.find( const featureFlag = currentWorkspace?.featureFlags?.find(
(flag) => flag.key === featureKey, (flag) => flag.key === featureKey,
); );

View File

@ -3,4 +3,5 @@ export type FeatureFlagKey =
| 'IS_QUICK_ACTIONS_ENABLED' | 'IS_QUICK_ACTIONS_ENABLED'
| 'IS_EVENT_OBJECT_ENABLED' | 'IS_EVENT_OBJECT_ENABLED'
| 'IS_AIRTABLE_INTEGRATION_ENABLED' | 'IS_AIRTABLE_INTEGRATION_ENABLED'
| 'IS_POSTGRESQL_INTEGRATION_ENABLED'; | 'IS_POSTGRESQL_INTEGRATION_ENABLED'
| 'IS_STRIPE_INTEGRATION_ENABLED';

View File

@ -6,6 +6,7 @@ import { useGetDatabaseConnections } from '@/databases/hooks/useGetDatabaseConne
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsIntegrationPreview } from '@/settings/integrations/components/SettingsIntegrationPreview'; import { SettingsIntegrationPreview } from '@/settings/integrations/components/SettingsIntegrationPreview';
import { SettingsIntegrationDatabaseConnectionsListCard } from '@/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionsListCard'; import { SettingsIntegrationDatabaseConnectionsListCard } from '@/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionsListCard';
import { useIsSettingsIntegrationEnabled } from '@/settings/integrations/hooks/useIsSettingsIntegrationEnabled';
import { useSettingsIntegrationCategories } from '@/settings/integrations/hooks/useSettingsIntegrationCategories'; import { useSettingsIntegrationCategories } from '@/settings/integrations/hooks/useSettingsIntegrationCategories';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
@ -14,7 +15,6 @@ import { H2Title } from '@/ui/display/typography/components/H2Title';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section'; import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
export const SettingsIntegrationDatabase = () => { export const SettingsIntegrationDatabase = () => {
const { databaseKey = '' } = useParams(); const { databaseKey = '' } = useParams();
@ -25,16 +25,9 @@ export const SettingsIntegrationDatabase = () => {
({ from: { key } }) => key === databaseKey, ({ from: { key } }) => key === databaseKey,
); );
const isAirtableIntegrationEnabled = useIsFeatureEnabled( const isIntegrationEnabled = useIsSettingsIntegrationEnabled(databaseKey);
'IS_AIRTABLE_INTEGRATION_ENABLED',
); const isIntegrationAvailable = !!integration && isIntegrationEnabled;
const isPostgresqlIntegrationEnabled = useIsFeatureEnabled(
'IS_POSTGRESQL_INTEGRATION_ENABLED',
);
const isIntegrationAvailable =
!!integration &&
((databaseKey === 'airtable' && isAirtableIntegrationEnabled) ||
(databaseKey === 'postgresql' && isPostgresqlIntegrationEnabled));
useEffect(() => { useEffect(() => {
if (!isIntegrationAvailable) { if (!isIntegrationAvailable) {

View File

@ -11,9 +11,11 @@ import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer'; import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { import {
SettingsIntegrationPostgreSQLConnectionForm, SettingsIntegrationDatabaseConnectionForm,
settingsIntegrationPostgreSQLConnectionFormSchema, settingsIntegrationPostgreSQLConnectionFormSchema,
settingsIntegrationStripeConnectionFormSchema,
} from '@/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionForm'; } from '@/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionForm';
import { useIsSettingsIntegrationEnabled } from '@/settings/integrations/hooks/useIsSettingsIntegrationEnabled';
import { useSettingsIntegrationCategories } from '@/settings/integrations/hooks/useSettingsIntegrationCategories'; import { useSettingsIntegrationCategories } from '@/settings/integrations/hooks/useSettingsIntegrationCategories';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
@ -23,33 +25,47 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section'; import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { CreateRemoteServerInput } from '~/generated-metadata/graphql'; import { CreateRemoteServerInput } from '~/generated-metadata/graphql';
const newConnectionSchema = settingsIntegrationPostgreSQLConnectionFormSchema; const createRemoteServerInputPostgresSchema =
settingsIntegrationPostgreSQLConnectionFormSchema.transform<CreateRemoteServerInput>(
(values) => ({
foreignDataWrapperType: 'postgres_fdw',
foreignDataWrapperOptions: {
dbname: values.dbname,
host: values.host,
port: values.port,
},
userMappingOptions: {
password: values.password,
user: values.user,
},
schema: values.schema,
}),
);
const createRemoteServerInputSchema = newConnectionSchema type SettingsIntegrationNewConnectionPostgresFormValues = z.infer<
.extend({ typeof createRemoteServerInputPostgresSchema
foreignDataWrapperType: z.string().min(1),
})
.transform<CreateRemoteServerInput>((values) => ({
foreignDataWrapperType: values.foreignDataWrapperType,
foreignDataWrapperOptions: {
dbname: values.dbname,
host: values.host,
port: values.port,
},
userMappingOptions: {
password: values.password,
user: values.user,
},
schema: values.schema,
}));
type SettingsIntegrationNewConnectionFormValues = z.infer<
typeof newConnectionSchema
>; >;
const createRemoteServerInputStripeSchema =
settingsIntegrationStripeConnectionFormSchema.transform<CreateRemoteServerInput>(
(values) => ({
foreignDataWrapperType: 'stripe_fdw',
foreignDataWrapperOptions: {
api_key: values.api_key,
},
}),
);
type SettingsIntegrationNewConnectionStripeFormValues = z.infer<
typeof createRemoteServerInputStripeSchema
>;
type SettingsIntegrationNewConnectionFormValues =
| SettingsIntegrationNewConnectionPostgresFormValues
| SettingsIntegrationNewConnectionStripeFormValues;
export const SettingsIntegrationNewDatabaseConnection = () => { export const SettingsIntegrationNewDatabaseConnection = () => {
const { databaseKey = '' } = useParams(); const { databaseKey = '' } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
@ -62,16 +78,9 @@ export const SettingsIntegrationNewDatabaseConnection = () => {
const { createOneDatabaseConnection } = useCreateOneDatabaseConnection(); const { createOneDatabaseConnection } = useCreateOneDatabaseConnection();
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const isAirtableIntegrationEnabled = useIsFeatureEnabled( const isIntegrationEnabled = useIsSettingsIntegrationEnabled(databaseKey);
'IS_AIRTABLE_INTEGRATION_ENABLED',
); const isIntegrationAvailable = !!integration && isIntegrationEnabled;
const isPostgresqlIntegrationEnabled = useIsFeatureEnabled(
'IS_POSTGRESQL_INTEGRATION_ENABLED',
);
const isIntegrationAvailable =
!!integration &&
((databaseKey === 'airtable' && isAirtableIntegrationEnabled) ||
(databaseKey === 'postgresql' && isPostgresqlIntegrationEnabled));
useEffect(() => { useEffect(() => {
if (!isIntegrationAvailable) { if (!isIntegrationAvailable) {
@ -79,6 +88,11 @@ export const SettingsIntegrationNewDatabaseConnection = () => {
} }
}, [integration, databaseKey, navigate, isIntegrationAvailable]); }, [integration, databaseKey, navigate, isIntegrationAvailable]);
const newConnectionSchema =
databaseKey === 'postgresql'
? createRemoteServerInputPostgresSchema
: createRemoteServerInputStripeSchema;
const formConfig = useForm<SettingsIntegrationNewConnectionFormValues>({ const formConfig = useForm<SettingsIntegrationNewConnectionFormValues>({
mode: 'onTouched', mode: 'onTouched',
resolver: zodResolver(newConnectionSchema), resolver: zodResolver(newConnectionSchema),
@ -97,7 +111,7 @@ export const SettingsIntegrationNewDatabaseConnection = () => {
try { try {
const createdConnection = await createOneDatabaseConnection( const createdConnection = await createOneDatabaseConnection(
createRemoteServerInputSchema.parse({ newConnectionSchema.parse({
...formValues, ...formValues,
foreignDataWrapperType: getForeignDataWrapperType(databaseKey), foreignDataWrapperType: getForeignDataWrapperType(databaseKey),
}), }),
@ -144,15 +158,15 @@ export const SettingsIntegrationNewDatabaseConnection = () => {
onSave={handleSave} onSave={handleSave}
/> />
</SettingsHeaderContainer> </SettingsHeaderContainer>
{databaseKey === 'postgresql' ? ( <Section>
<Section> <H2Title
<H2Title title="Connect a new database"
title="Connect a new database" description="Provide the information to connect your database"
description="Provide the information to connect your PostgreSQL database" />
/> <SettingsIntegrationDatabaseConnectionForm
<SettingsIntegrationPostgreSQLConnectionForm /> databaseKey={databaseKey}
</Section> />
) : null} </Section>
</FormProvider> </FormProvider>
</SettingsPageContainer> </SettingsPageContainer>
</SubMenuTopBarContainer> </SubMenuTopBarContainer>

View File

@ -35,6 +35,6 @@ export const Default: Story = {
const canvas = within(canvasElement); const canvas = within(canvasElement);
sleep(100); sleep(100);
await canvas.findByText('Edit PostgreSQL Connection'); await canvas.findByText('Edit Connection');
}, },
}; };

View File

@ -17,4 +17,15 @@ export const mockedRemoteServers = [
updatedAt: '2024-04-30T13:41:25.858Z', updatedAt: '2024-04-30T13:41:25.858Z',
schema: 'public', schema: 'public',
}, },
{
__typename: 'RemoteServer',
id: 'ddc3b641-2142-4b4e-8fba-976afbc3b2bc',
createdAt: '2024-04-30T13:41:25.584Z',
foreignDataWrapperId: 'dqfdsqf-2142-4b4e-8fba-976afbc3b2bc',
foreignDataWrapperOptions: {
api_key: 'sk_test_51',
},
foreignDataWrapperType: 'stripe_fdw',
updatedAt: '2024-04-30T13:41:25.858Z',
},
]; ];