Skip to content

Commit

Permalink
feat(data-warehouse): hubspot integration (#19529)
Browse files Browse the repository at this point in the history
* initial api

* urls

* refactor source selector frontend

* refactor source selector frontend

* field config

* http working

* add hubspot dlt helpers

* remove products endpoint and add token refresh

* reformat

* add limiting

* Update UI snapshots for `chromium` (1)

* Update UI snapshots for `chromium` (1)

* typing and migration

* typing

* update latest migration

* add hubspot logo

* Update UI snapshots for `chromium` (1)

* add prefix flow

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
EDsCODE and github-actions[bot] authored Jan 2, 2024
1 parent b8b90c1 commit 1d6ba3c
Show file tree
Hide file tree
Showing 27 changed files with 915 additions and 117 deletions.
Binary file added frontend/public/hubspot-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 2 additions & 4 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ import {
EventType,
Experiment,
ExportedAssetType,
ExternalDataSourceCreatePayload,
ExternalDataSourceSchema,
ExternalDataStripeSource,
ExternalDataStripeSourceCreatePayload,
FeatureFlagAssociatedRoleType,
FeatureFlagType,
Group,
Expand Down Expand Up @@ -1756,9 +1756,7 @@ const api = {
async list(): Promise<PaginatedResponse<ExternalDataStripeSource>> {
return await new ApiRequest().externalDataSources().get()
},
async create(
data: Partial<ExternalDataStripeSourceCreatePayload>
): Promise<ExternalDataStripeSourceCreatePayload> {
async create(data: Partial<ExternalDataSourceCreatePayload>): Promise<ExternalDataSourceCreatePayload> {
return await new ApiRequest().externalDataSources().create({ data })
},
async delete(sourceId: ExternalDataStripeSource['id']): Promise<void> {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/scenes/appScenes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const appScenes: Record<Scene, () => any> = {
[Scene.DataWarehouseExternal]: () => import('./data-warehouse/external/DataWarehouseExternalScene'),
[Scene.DataWarehouseSavedQueries]: () => import('./data-warehouse/saved_queries/DataWarehouseSavedQueriesScene'),
[Scene.DataWarehouseSettings]: () => import('./data-warehouse/settings/DataWarehouseSettingsScene'),
[Scene.DataWarehouseRedirect]: () => import('./data-warehouse/redirect/DataWarehouseRedirectScene'),
[Scene.OrganizationCreateFirst]: () => import('./organization/Create'),
[Scene.OrganizationCreationConfirm]: () => import('./organization/ConfirmOrganization/ConfirmOrganization'),
[Scene.ProjectHomepage]: () => import('./project-homepage/ProjectHomepage'),
Expand Down
118 changes: 70 additions & 48 deletions frontend/src/scenes/data-warehouse/external/SourceModal.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,47 @@
import { LemonButton, LemonDivider, LemonInput, LemonModal, LemonModalProps } from '@posthog/lemon-ui'
import { LemonButton, LemonDivider, LemonInput, LemonModal, LemonModalProps, Link } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { Form } from 'kea-forms'
import { Field } from 'lib/forms/Field'
import hubspotLogo from 'public/hubspot-logo.png'
import stripeLogo from 'public/stripe-logo.svg'

import { ExternalDataSourceType } from '~/types'

import { DatawarehouseTableForm } from '../new_table/DataWarehouseTableForm'
import { SOURCE_DETAILS, sourceFormLogic } from './sourceFormLogic'
import { ConnectorConfigType, sourceModalLogic } from './sourceModalLogic'

interface SourceModalProps extends LemonModalProps {}

export default function SourceModal(props: SourceModalProps): JSX.Element {
const { tableLoading, isExternalDataSourceSubmitting, selectedConnector, isManualLinkFormVisible, connectors } =
const { tableLoading, selectedConnector, isManualLinkFormVisible, connectors, addToHubspotButtonUrl } =
useValues(sourceModalLogic)
const { selectConnector, toggleManualLinkFormVisible, resetExternalDataSource, resetTable } =
useActions(sourceModalLogic)
const { selectConnector, toggleManualLinkFormVisible, onClear } = useActions(sourceModalLogic)

const MenuButton = (config: ConnectorConfigType): JSX.Element => {
const onClick = (): void => {
selectConnector(config)
}

return (
<LemonButton onClick={onClick} className="w-100" center type="secondary">
<img src={stripeLogo} alt={`stripe logo`} height={50} />
</LemonButton>
)
}
if (config.name === 'Stripe') {
return (
<LemonButton onClick={onClick} className="w-100" center type="secondary">
<img src={stripeLogo} alt={`stripe logo`} height={50} />
</LemonButton>
)
}
if (config.name === 'Hubspot') {
return (
<Link to={addToHubspotButtonUrl() || ''}>
<LemonButton className="w-100" center type="secondary">
<img src={hubspotLogo} alt={`hubspot logo`} height={50} />
Hubspot
</LemonButton>
</Link>
)
}

const onClear = (): void => {
selectConnector(null)
toggleManualLinkFormVisible(false)
resetExternalDataSource()
resetTable()
return <></>
}

const onManualLinkClick = (): void => {
Expand All @@ -40,39 +50,7 @@ export default function SourceModal(props: SourceModalProps): JSX.Element {

const formToShow = (): JSX.Element => {
if (selectedConnector) {
return (
<Form logic={sourceModalLogic} formKey={'externalDataSource'} className="space-y-4" enableFormOnSubmit>
<Field name="prefix" label="Table Prefix">
<LemonInput className="ph-ignore-input" autoFocus data-attr="prefix" placeholder="internal_" />
</Field>
<Field name="account_id" label="Stripe Account ID">
<LemonInput className="ph-ignore-input" autoFocus data-attr="account-id" placeholder="acct_" />
</Field>
<Field name="client_secret" label="Stripe Client Secret">
<LemonInput
className="ph-ignore-input"
autoFocus
data-attr="client-secret"
placeholder="sklive"
/>
</Field>
<LemonDivider className="mt-4" />
<div className="mt-2 flex flex-row justify-end gap-2">
<LemonButton type="secondary" center data-attr="source-modal-back-button" onClick={onClear}>
Back
</LemonButton>
<LemonButton
type="primary"
center
htmlType="submit"
data-attr="source-link"
loading={isExternalDataSourceSubmitting}
>
Link
</LemonButton>
</div>
</Form>
)
return <SourceForm sourceType={selectedConnector.name} />
}

if (isManualLinkFormVisible) {
Expand Down Expand Up @@ -131,3 +109,47 @@ export default function SourceModal(props: SourceModalProps): JSX.Element {
</LemonModal>
)
}

interface SourceFormProps {
sourceType: ExternalDataSourceType
}

function SourceForm({ sourceType }: SourceFormProps): JSX.Element {
const logic = sourceFormLogic({ sourceType })
const { isExternalDataSourceSubmitting } = useValues(logic)
const { onBack } = useActions(logic)

return (
<Form
logic={sourceFormLogic}
props={{ sourceType }}
formKey={'externalDataSource'}
className="space-y-4"
enableFormOnSubmit
>
<Field name="prefix" label="Table Prefix">
<LemonInput className="ph-ignore-input" autoFocus data-attr="prefix" placeholder="internal_" />
</Field>
{SOURCE_DETAILS[sourceType].fields.map((field) => (
<Field key={field.name} name={['payload', field.name]} label={field.label}>
<LemonInput className="ph-ignore-input" data-attr={field.name} />
</Field>
))}
<LemonDivider className="mt-4" />
<div className="mt-2 flex flex-row justify-end gap-2">
<LemonButton type="secondary" center data-attr="source-modal-back-button" onClick={onBack}>
Back
</LemonButton>
<LemonButton
type="primary"
center
htmlType="submit"
data-attr="source-link"
loading={isExternalDataSourceSubmitting}
>
Link
</LemonButton>
</div>
</Form>
)
}
135 changes: 135 additions & 0 deletions frontend/src/scenes/data-warehouse/external/sourceFormLogic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { lemonToast } from '@posthog/lemon-ui'
import { actions, connect, kea, listeners, path, props } from 'kea'
import { forms } from 'kea-forms'
import { router, urlToAction } from 'kea-router'
import api from 'lib/api'
import { urls } from 'scenes/urls'

import { ExternalDataSourceCreatePayload, ExternalDataSourceType } from '~/types'

import type { sourceFormLogicType } from './sourceFormLogicType'
import { getHubspotRedirectUri, sourceModalLogic } from './sourceModalLogic'

export interface SourceFormProps {
sourceType: ExternalDataSourceType
}

interface SourceConfig {
name: string
caption: string
fields: FieldConfig[]
}
interface FieldConfig {
name: string
label: string
type: string
required: boolean
}

export const SOURCE_DETAILS: Record<string, SourceConfig> = {
Stripe: {
name: 'Stripe',
caption: 'Enter your Stripe credentials to link your Stripe to PostHog',
fields: [
{
name: 'account_id',
label: 'Account ID',
type: 'text',
required: true,
},
{
name: 'client_secret',
label: 'Client Secret',
type: 'text',
required: true,
},
],
},
}

const getPayloadDefaults = (sourceType: string): Record<string, any> => {
switch (sourceType) {
case 'Stripe':
return {
account_id: '',
client_secret: '',
}
default:
return {}
}
}

const getErrorsDefaults = (sourceType: string): ((args: Record<string, any>) => Record<string, any>) => {
switch (sourceType) {
case 'Stripe':
return ({ payload }) => ({
payload: {
account_id: !payload.account_id && 'Please enter an account id.',
client_secret: !payload.client_secret && 'Please enter a client secret.',
},
})
default:
return () => ({})
}
}

export const sourceFormLogic = kea<sourceFormLogicType>([
path(['scenes', 'data-warehouse', 'external', 'sourceFormLogic']),
props({} as SourceFormProps),
connect({
actions: [sourceModalLogic, ['onClear', 'toggleSourceModal', 'loadSources']],
}),
actions({
onBack: true,
handleRedirect: (kind: string, searchParams: any) => ({ kind, searchParams }),
}),
listeners(({ actions }) => ({
onBack: () => {
actions.resetExternalDataSource()
actions.onClear()
},
submitExternalDataSourceSuccess: () => {
lemonToast.success('New Data Resource Created')
actions.toggleSourceModal(false)
actions.resetExternalDataSource()
actions.loadSources()
router.actions.push(urls.dataWarehouseSettings())
},
submitExternalDataSourceFailure: ({ error }) => {
lemonToast.error(error?.message || 'Something went wrong')
},
handleRedirect: async ({ kind, searchParams }) => {
switch (kind) {
case 'hubspot': {
actions.setExternalDataSourceValue('payload', {
code: searchParams.code,
redirect_uri: getHubspotRedirectUri(),
})
actions.setExternalDataSourceValue('source_type', 'Hubspot')
return
}
default:
lemonToast.error(`Something went wrong.`)
}
},
})),
urlToAction(({ actions }) => ({
'/data-warehouse/:kind/redirect': ({ kind = '' }, searchParams) => {
actions.handleRedirect(kind, searchParams)
},
})),
forms(({ props }) => ({
externalDataSource: {
defaults: {
prefix: '',
source_type: props.sourceType,
payload: getPayloadDefaults(props.sourceType),
} as ExternalDataSourceCreatePayload,
errors: getErrorsDefaults(props.sourceType),
submit: async (payload: ExternalDataSourceCreatePayload) => {
const newResource = await api.externalDataSources.create(payload)
return newResource
},
},
})),
])
Loading

0 comments on commit 1d6ba3c

Please sign in to comment.