Skip to content

Commit

Permalink
feat(source-connection-string): Postgres connection string configurat…
Browse files Browse the repository at this point in the history
…ion (#27559)

Co-authored-by: Eric Duong <[email protected]>
  • Loading branch information
phixMe and EDsCODE authored Jan 16, 2025
1 parent 4ba47b8 commit a2eda38
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 19 deletions.
61 changes: 53 additions & 8 deletions frontend/src/scenes/data-warehouse/external/forms/SourceForm.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,60 @@
import { LemonFileInput, LemonInput, LemonSelect, LemonSwitch, LemonTextArea } from '@posthog/lemon-ui'
import { LemonDivider, LemonFileInput, LemonInput, LemonSelect, LemonSwitch, LemonTextArea } from '@posthog/lemon-ui'
import { Form, Group } from 'kea-forms'
import { LemonField } from 'lib/lemon-ui/LemonField'

import { SourceConfig, SourceFieldConfig } from '~/types'

import { SOURCE_DETAILS, sourceWizardLogic } from '../../new/sourceWizardLogic'
import { DataWarehouseIntegrationChoice } from './DataWarehouseIntegrationChoice'
import { parseConnectionString } from './parseConnectionString'

export interface SourceFormProps {
sourceConfig: SourceConfig
showPrefix?: boolean
jobInputs?: Record<string, any>
}

const CONNECTION_STRING_DEFAULT_PORT = {
Postgres: 5432,
}

const sourceFieldToElement = (field: SourceFieldConfig, sourceConfig: SourceConfig, lastValue?: any): JSX.Element => {
if (field.type === 'text' && field.name === 'connection_string') {
return (
<>
<LemonField key={field.name} name={field.name} label={field.label}>
{({ onChange }) => (
<LemonInput
key={field.name}
className="ph-connection-string"
data-attr={field.name}
placeholder={field.placeholder}
type="text"
onChange={(updatedConnectionString) => {
onChange(updatedConnectionString)
const { host, port, database, user, password, isValid } =
parseConnectionString(updatedConnectionString)

if (isValid) {
sourceWizardLogic.actions.setSourceConnectionDetailsValues({
payload: {
dbname: database || '',
host: host || '',
user: user || '',
port: port || CONNECTION_STRING_DEFAULT_PORT[sourceConfig.name],
password: password || '',
},
})
}
}}
/>
)}
</LemonField>
<LemonDivider />
</>
)
}

if (field.type === 'switch-group') {
return (
<LemonField key={field.name} name={[field.name, 'enabled']} label={field.label}>
Expand Down Expand Up @@ -113,13 +154,17 @@ const sourceFieldToElement = (field: SourceFieldConfig, sourceConfig: SourceConf

return (
<LemonField key={field.name} name={field.name} label={field.label}>
<LemonInput
className="ph-ignore-input"
data-attr={field.name}
placeholder={field.placeholder}
type={field.type}
defaultValue={lastValue}
/>
{({ value, onChange }) => (
<LemonInput
className="ph-ignore-input"
data-attr={field.name}
placeholder={field.placeholder}
type={field.type as 'text'}
defaultValue={lastValue}
value={value ?? ''}
onChange={onChange}
/>
)}
</LemonField>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//Parse method copied from https://github.com/brianc/node-postgres/tree/master/packages/pg-connection-string
//Copyright (c) 2010-2014 Brian Carlson ([email protected])
//Adapted & Repurposed for TypeScript by Peter Hicks ([email protected])
//MIT License

interface Config {
host?: string
database?: string | null
port?: string | null
user?: string | null
password?: string | null
client_encoding?: string | null
isValid?: boolean
[key: string]: any
}

export function parseConnectionString(str: string): Config {
const config: Config = {}
let result: URL
let dummyHost = false

// Allow "postgres://" and "postgresql://"
if (str.startsWith('postgres://')) {
str = 'postgresql://' + str.substring('postgres://'.length)
}

if (/ |%[^a-f0-9]|%[a-f0-9][^a-f0-9]/i.test(str)) {
str = encodeURI(str).replace(/%25(\d\d)/g, '%$1') // Encode spaces as %20
}

try {
result = new URL(str, 'postgresql://base')
} catch {
// Invalid URL, attempt with dummy host
result = new URL(str.replace('@/', '@___DUMMY___/'), 'postgresql://base')
dummyHost = true
}

// Parse search parameters
for (const [key, value] of result.searchParams.entries()) {
config[key] = value
}

config.user = config.user || decodeURIComponent(result.username || '')
config.password = config.password || decodeURIComponent(result.password || '')

if (result.protocol === 'socket:') {
config.host = decodeURI(result.pathname)
config.database = result.searchParams.get('db')
config.client_encoding = result.searchParams.get('encoding')
return config
}

const hostname = dummyHost ? '' : result.hostname
if (!config.host) {
config.host = decodeURIComponent(hostname)
} else if (hostname && /^%2f/i.test(hostname)) {
result.pathname = hostname + result.pathname
}

config.port = config.port || result.port
const pathname = result.pathname.slice(1) || null
config.database = pathname ? decodeURIComponent(pathname) : null

config.isValid = !!(config.user && config.database && config.host)

return config
}
10 changes: 3 additions & 7 deletions frontend/src/scenes/data-warehouse/new/NewSourceWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,17 +138,13 @@ function FirstStep(): JSX.Element {
onNext()
}

const onManualLinkClick = (manulLinkSource: ManualLinkSourceType): void => {
const onManualLinkClick = (manualLinkSource: ManualLinkSourceType): void => {
toggleManualLinkFormVisible(true)
setManualLinkingProvider(manulLinkSource)
setManualLinkingProvider(manualLinkSource)
}

const filteredConnectors = connectors.filter((n) => {
if (n.name === 'BigQuery' && !featureFlags[FEATURE_FLAGS.BIGQUERY_DWH]) {
return false
}

return true
return !(n.name === 'BigQuery' && !featureFlags[FEATURE_FLAGS.BIGQUERY_DWH])
})

return (
Expand Down
13 changes: 9 additions & 4 deletions frontend/src/scenes/data-warehouse/new/sourceWizardLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const SOURCE_DETAILS: Record<ExternalDataSourceType, SourceConfig> = {
Hubspot: {
name: 'Hubspot',
fields: [],
caption: 'Succesfully authenticated with Hubspot. Please continue here to complete the source setup',
caption: 'Successfully authenticated with Hubspot. Please continue here to complete the source setup',
oauthPayload: ['code'],
},
Postgres: {
Expand All @@ -80,6 +80,13 @@ export const SOURCE_DETAILS: Record<ExternalDataSourceType, SourceConfig> = {
</>
),
fields: [
{
name: 'connection_string',
label: 'Connection String (optional)',
type: 'text',
required: false,
placeholder: 'postgresql://user:password@localhost:5432/database',
},
{
name: 'host',
label: 'Host',
Expand Down Expand Up @@ -754,7 +761,7 @@ export const buildKeaFormDefaultFromSourceDetails = (
}

const sourceDetailsKeys = Object.keys(sourceDetails)
const formDefault = sourceDetailsKeys.reduce(
return sourceDetailsKeys.reduce(
(defaults, cur) => {
const fields = sourceDetails[cur].fields
fields.forEach((f) => fieldDefaults(f, defaults['payload']))
Expand All @@ -763,8 +770,6 @@ export const buildKeaFormDefaultFromSourceDetails = (
},
{ prefix: '', payload: {} } as Record<string, any>
)

return formDefault
}

const manualLinkSourceMap: Record<ManualLinkSourceType, string> = {
Expand Down

0 comments on commit a2eda38

Please sign in to comment.