Skip to content

Commit

Permalink
feat(data-warehouse): add salesforce integration (#23212)
Browse files Browse the repository at this point in the history
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
EDsCODE and github-actions[bot] authored Aug 13, 2024
1 parent fdc2ac7 commit 7131400
Show file tree
Hide file tree
Showing 25 changed files with 692 additions and 70 deletions.
25 changes: 25 additions & 0 deletions frontend/public/salesforce-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 21 additions & 6 deletions frontend/src/scenes/data-warehouse/external/forms/SourceForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LemonInput, LemonSelect, LemonSwitch, LemonTextArea } from '@posthog/lemon-ui'
import { useValues } from 'kea'
import { Form, Group } from 'kea-forms'
import { LemonField } from 'lib/lemon-ui/LemonField'

Expand All @@ -8,6 +9,8 @@ import { SOURCE_DETAILS, sourceWizardLogic } from '../../new/sourceWizardLogic'

interface SourceFormProps {
sourceConfig: SourceConfig
showPrefix?: boolean
showSourceFields?: boolean
}

const sourceFieldToElement = (field: SourceFieldConfig): JSX.Element => {
Expand Down Expand Up @@ -73,14 +76,26 @@ const sourceFieldToElement = (field: SourceFieldConfig): JSX.Element => {
}

export default function SourceForm({ sourceConfig }: SourceFormProps): JSX.Element {
const { source } = useValues(sourceWizardLogic)
const showSourceFields = SOURCE_DETAILS[sourceConfig.name].showSourceForm
? SOURCE_DETAILS[sourceConfig.name].showSourceForm?.(source.payload)
: true
const showPrefix = SOURCE_DETAILS[sourceConfig.name].showPrefix
? SOURCE_DETAILS[sourceConfig.name].showPrefix?.(source.payload)
: true

return (
<Form logic={sourceWizardLogic} formKey="sourceConnectionDetails" className="space-y-4" enableFormOnSubmit>
<Group name="payload">
{SOURCE_DETAILS[sourceConfig.name].fields.map((field) => sourceFieldToElement(field))}
</Group>
<LemonField name="prefix" label="Table Prefix (optional)">
<LemonInput className="ph-ignore-input" data-attr="prefix" placeholder="internal_" />
</LemonField>
{showSourceFields && (
<Group name="payload">
{SOURCE_DETAILS[sourceConfig.name].fields.map((field) => sourceFieldToElement(field))}
</Group>
)}
{showPrefix && (
<LemonField name="prefix" label="Table Prefix (optional)">
<LemonInput className="ph-ignore-input" data-attr="prefix" placeholder="internal_" />
</LemonField>
)}
</Form>
)
}
115 changes: 103 additions & 12 deletions frontend/src/scenes/data-warehouse/new/sourceWizardLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const Caption = (): JSX.Element => (
)

export const getHubspotRedirectUri = (): string => `${window.location.origin}/data-warehouse/hubspot/redirect`
export const getSalesforceRedirectUri = (): string => `${window.location.origin}/data-warehouse/salesforce/redirect`

export const SOURCE_DETAILS: Record<ExternalDataSourceType, SourceConfig> = {
Stripe: {
Expand All @@ -67,6 +68,7 @@ export const SOURCE_DETAILS: Record<ExternalDataSourceType, SourceConfig> = {
name: 'Hubspot',
fields: [],
caption: 'Succesfully authenticated with Hubspot. Please continue here to complete the source setup',
oauthPayload: ['code'],
},
Postgres: {
name: 'Postgres',
Expand Down Expand Up @@ -422,6 +424,22 @@ export const SOURCE_DETAILS: Record<ExternalDataSourceType, SourceConfig> = {
},
],
},
Salesforce: {
name: 'Salesforce',
fields: [
{
name: 'subdomain',
label: 'Salesforce subdomain',
type: 'text',
required: true,
placeholder: '',
},
],
caption: 'Succesfully authenticated with Salesforce. Please continue here to complete the source setup',
showPrefix: (payload) => !!payload.code,
showSourceForm: (payload) => !payload.code,
oauthPayload: ['code', 'subdomain'],
},
}

export const buildKeaFormDefaultFromSourceDetails = (
Expand Down Expand Up @@ -748,6 +766,27 @@ export const sourceWizardLogic = kea<sourceWizardLogicType>([
}
},
],
addToSalesforceButtonUrl: [
(s) => [s.preflight],
(preflight) => {
return (subdomain: string) => {
const clientId = preflight?.data_warehouse_integrations?.salesforce.client_id

if (!clientId) {
return null
}

const params = new URLSearchParams()
params.set('client_id', clientId)
params.set('redirect_uri', `${window.location.origin}/data-warehouse/salesforce/redirect`)
params.set('response_type', 'code')
params.set('scope', 'refresh_token api')
params.set('state', subdomain)

return `https://${subdomain}.my.salesforce.com/services/oauth2/authorize?${params.toString()}`
}
},
],
modalTitle: [
(s) => [s.currentStep],
(currentStep) => {
Expand Down Expand Up @@ -885,6 +924,17 @@ export const sourceWizardLogic = kea<sourceWizardLogicType>([
})
return
}
case 'salesforce': {
actions.updateSource({
source_type: 'Salesforce',
payload: {
code: searchParams.code,
subdomain: searchParams.subdomain,
redirect_uri: getSalesforceRedirectUri(),
},
})
break
}
default:
lemonToast.error(`Something went wrong.`)
}
Expand Down Expand Up @@ -925,6 +975,13 @@ export const sourceWizardLogic = kea<sourceWizardLogicType>([
if (kind === 'hubspot') {
router.actions.push(urls.dataWarehouseTable(), { kind, code: searchParams.code })
}
if (kind === 'salesforce') {
router.actions.push(urls.dataWarehouseTable(), {
kind,
code: searchParams.code,
subdomain: searchParams.state,
})
}
},
'/data-warehouse/new': (_, searchParams) => {
if (searchParams.kind == 'hubspot' && searchParams.code) {
Expand All @@ -934,16 +991,41 @@ export const sourceWizardLogic = kea<sourceWizardLogicType>([
})
actions.setStep(2)
}
if (searchParams.kind == 'salesforce' && searchParams.code) {
actions.selectConnector(SOURCE_DETAILS['Salesforce'])
actions.handleRedirect(searchParams.kind, {
code: searchParams.code,
subdomain: searchParams.subdomain,
})
actions.setStep(2)
}
},
})),
forms(({ actions, values }) => ({
sourceConnectionDetails: {
defaults: buildKeaFormDefaultFromSourceDetails(SOURCE_DETAILS),
errors: (sourceValues) => {
if (
values.selectedConnector &&
SOURCE_DETAILS[values.selectedConnector?.name].oauthPayload &&
SOURCE_DETAILS[values.selectedConnector.name].oauthPayload?.every(
(element) => values.source.payload[element]
)
) {
return {}
}
return getErrorsForFields(values.selectedConnector?.fields ?? [], sourceValues as any)
},
submit: async (sourceValues) => {
if (values.selectedConnector) {
if (
values.selectedConnector.name === 'Salesforce' &&
(!values.source.payload.code || !values.source.payload.subdomain)
) {
window.open(values.addToSalesforceButtonUrl(sourceValues.payload.subdomain) as string)
return
}

const payload = {
...sourceValues,
source_type: values.selectedConnector.name,
Expand All @@ -953,19 +1035,28 @@ export const sourceWizardLogic = kea<sourceWizardLogicType>([
try {
await api.externalDataSources.source_prefix(payload.source_type, sourceValues.prefix)

const payloadKeys = (values.selectedConnector?.fields ?? []).map((n) => n.name)
// salesforce doesn't need to store the payload. The relevant fielsd will already be handled from the URL
// only update the prefix
if (values.selectedConnector.name === 'Salesforce') {
actions.updateSource({
...values.source,
prefix: sourceValues.prefix,
})
} else {
const payloadKeys = (values.selectedConnector?.fields ?? []).map((n) => n.name)

// Only store the keys of the source type we're using
actions.updateSource({
...payload,
payload: {
source_type: values.selectedConnector.name,
...payloadKeys.reduce((acc, cur) => {
acc[cur] = payload['payload'][cur]
return acc
}, {} as Record<string, any>),
},
})
// Only store the keys of the source type we're using
actions.updateSource({
...payload,
payload: {
source_type: values.selectedConnector.name,
...payloadKeys.reduce((acc, cur) => {
acc[cur] = payload['payload'][cur]
return acc
}, {} as Record<string, any>),
},
})
}

actions.setIsLoading(false)
} catch (e: any) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import IconGoogleCloudStorage from 'public/services/google-cloud-storage.png'
import IconHubspot from 'public/services/hubspot.png'
import IconMySQL from 'public/services/mysql.png'
import IconPostgres from 'public/services/postgres.png'
import IconSalesforce from 'public/services/salesforce.png'
import IconSnowflake from 'public/services/snowflake.png'
import IconStripe from 'public/services/stripe.png'
import IconZendesk from 'public/services/zendesk.png'
Expand Down Expand Up @@ -183,6 +184,7 @@ export function RenderDataWarehouseSourceIcon({
'google-cloud': IconGoogleCloudStorage,
'cloudflare-r2': IconCloudflare,
azure: Iconazure,
Salesforce: IconSalesforce,
}[type]

return (
Expand Down
16 changes: 15 additions & 1 deletion frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2966,6 +2966,9 @@ export interface PreflightStatus {
hubspot: {
client_id?: string
}
salesforce: {
client_id?: string
}
}
/** Whether PostHog is running in DEBUG mode. */
is_debug?: boolean
Expand Down Expand Up @@ -3857,7 +3860,15 @@ export enum DataWarehouseSettingsTab {
SelfManaged = 'self-managed',
}

export const externalDataSources = ['Stripe', 'Hubspot', 'Postgres', 'MySQL', 'Zendesk', 'Snowflake'] as const
export const externalDataSources = [
'Stripe',
'Hubspot',
'Postgres',
'MySQL',
'Zendesk',
'Snowflake',
'Salesforce',
] as const

export type ExternalDataSourceType = (typeof externalDataSources)[number]

Expand Down Expand Up @@ -4234,6 +4245,9 @@ export interface SourceConfig {
caption: string | React.ReactNode
fields: SourceFieldConfig[]
disabledReason?: string | null
showPrefix?: (payload: Record<string, any>) => boolean
showSourceForm?: (payload: Record<string, any>) => boolean
oauthPayload?: string[]
}

export interface ProductPricingTierSubrows {
Expand Down
Loading

0 comments on commit 7131400

Please sign in to comment.