Skip to content
Open
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
8 changes: 6 additions & 2 deletions frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ export interface PipelineStageRequest {
actions: StageActionRequest[]
}

type WarehouseConfigValue = string | number | boolean

export type Req = {
getFeatureCodeReferences: {
projectId: number
Expand Down Expand Up @@ -1032,15 +1034,17 @@ export type Req = {
environmentId: string
warehouse_type: string
name?: string
config?: Record<string, string>
config?: Record<string, WarehouseConfigValue>
credentials?: { password: string }
}
deleteWarehouseConnection: { environmentId: string; id: number }
testWarehouseConnection: { environmentId: string; id: number }
updateWarehouseConnection: {
environmentId: string
id: number
name?: string
config?: Record<string, string>
config?: Record<string, WarehouseConfigValue>
credentials?: { password: string }
}
getExperiments: PagedRequest<{
environmentId: string
Expand Down
16 changes: 15 additions & 1 deletion frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1282,12 +1282,26 @@ export type SnowflakeConfig = {
user: string
}

export type ClickHouseConfig = {
host: string
port: number
database: string
username: string
secure: boolean
}

export type WarehouseConfigResponse =
| SnowflakeConfig
| ClickHouseConfig
| Record<string, never>

export type WarehouseConnection = {
id: number
warehouse_type: WarehouseType
status: WarehouseConnectionStatus
status_detail: string | null
name: string
config: SnowflakeConfig | Record<string, never>
config: WarehouseConfigResponse
created_at: string
total_events_received: number | null
unique_events_count: number | null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import React, { FC, useState } from 'react'
import Button from 'components/base/forms/Button'
import Input from 'components/base/forms/Input'
import Switch from 'components/Switch'
import ErrorMessage from 'components/ErrorMessage'
import { ClickHouseConfig } from 'common/types/responses'
import {
buildClickHousePayload,
CLICKHOUSE_DEFAULTS,
ClickHouseFormData,
ClickHouseFormState,
isClickHouseFormValid,
} from './clickhouseConfig'
import { getButtonLabel } from './warehouseFormUtils'
import './ConfigForm.scss'
Comment on lines +1 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Move into its own component folder and stop importing ConfigForm.scss directly.

This new component lives directly under warehouse-tab/ and imports ./ConfigForm.scss (Snowflake's stylesheet) rather than having its own co-located stylesheet. That silently couples ClickHouse styling to the Snowflake form file.

As per path instructions, "Each new component must live in its own folder with a barrel index.ts, a ComponentName/ComponentName.tsx file, co-located ComponentName.scss, any subcomponents, and imports should use the component folder path rather than the inner file."

♻️ Proposed structure
ClickHouseConfigForm/
  index.ts               // export { default } from './ClickHouseConfigForm'
  ClickHouseConfigForm.tsx
  ClickHouseConfigForm.scss   // own styles, or explicitly import the shared partial

Source: Path instructions


type ClickHouseConfigFormProps = {
onSave: (data: ClickHouseFormData) => Promise<unknown>
onCancel: () => void
isEdit?: boolean
initialConfig?: ClickHouseConfig
initialName?: string
}

const ClickHouseConfigForm: FC<ClickHouseConfigFormProps> = ({
initialConfig,
initialName = '',
isEdit = false,
onCancel,
onSave,
}) => {
const defaults = { ...CLICKHOUSE_DEFAULTS, ...initialConfig }
const [name, setName] = useState(initialName)
const [host, setHost] = useState(defaults.host)
const [port, setPort] = useState(String(defaults.port))
const [database, setDatabase] = useState(defaults.database)
const [username, setUsername] = useState(defaults.username)
const [password, setPassword] = useState('')
const [secure, setSecure] = useState(defaults.secure)
const [isSaving, setIsSaving] = useState(false)
const [error, setError] = useState(false)

const form: ClickHouseFormState = {
database,
host,
name,
password,
port,
secure,
username,
}
const isValid = isClickHouseFormValid(form, isEdit)

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!isValid) return

setIsSaving(true)
setError(false)
try {
await onSave(buildClickHousePayload(form))
} catch {
setError(true)
setIsSaving(false)
}
}

return (
<form onSubmit={handleSubmit} className='wh-config-form'>
<div className='wh-config-form__card'>
<div className='wh-config-form__field'>
<label className='wh-config-form__label'>Host</label>
<Input
value={host}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setHost(e.target.value)
}
placeholder='your-instance.clickhouse.cloud'
disabled={isEdit}
/>
<span className='wh-config-form__hint'>
{isEdit
? "Host can't be changed. To use a different ClickHouse instance, disconnect and create a new connection."
: 'The hostname of your ClickHouse instance, without protocol or port.'}
</span>
</div>

<div className='wh-config-form__field'>
<label className='wh-config-form__label'>Name</label>
<Input
value={name}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setName(e.target.value)
}
placeholder='e.g. Production ClickHouse'
/>
</div>

<div className='wh-config-form__row'>
<div className='wh-config-form__field'>
<label className='wh-config-form__label'>Port</label>
<Input
value={port}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setPort(e.target.value)
}
placeholder='9440'
/>
</div>
<div className='wh-config-form__field'>
<label className='wh-config-form__label'>Database</label>
<Input
value={database}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setDatabase(e.target.value)
}
placeholder='flagsmith'
/>
</div>
</div>

<div className='wh-config-form__field'>
<label className='wh-config-form__label'>Username</label>
<Input
value={username}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setUsername(e.target.value)
}
placeholder='default'
/>
</div>

<div className='wh-config-form__field'>
<label className='wh-config-form__label'>Password</label>
<Input
value={password}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setPassword(e.target.value)
}
type='password'
placeholder={isEdit ? '••••••••' : 'Password'}
/>
{isEdit && (
<span className='wh-config-form__hint'>
Leave blank to keep the current password.
</span>
)}
</div>

<div className='wh-config-form__field'>
<div className='d-flex flex-row align-items-center gap-2'>
<Switch checked={secure} onChange={setSecure} />
<label className='wh-config-form__label mb-0'>
Secure connection (TLS)
</label>
</div>
</div>

{error && (
<ErrorMessage
error={`Failed to ${
isEdit ? 'update' : 'create'
} warehouse connection. Please try again.`}
/>
)}

<div className='wh-config-form__actions'>
<Button
id='warehouse-config-cancel'
theme='outline'
size='small'
onClick={onCancel}
type='button'
>
Cancel
</Button>
<Button
id='warehouse-config-save'
theme='primary'
size='small'
type='submit'
disabled={isSaving || !isValid}
>
{getButtonLabel(isEdit, isSaving)}
</Button>
</div>
</div>
</form>
)
}

ClickHouseConfigForm.displayName = 'ClickHouseConfigForm'
export default ClickHouseConfigForm
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Button from 'components/base/forms/Button'
import Input from 'components/base/forms/Input'
import ErrorMessage from 'components/ErrorMessage'
import { SnowflakeConfig } from 'common/types/responses'
import { getButtonLabel } from './warehouseFormUtils'
import './ConfigForm.scss'

export type ConfigFormData = SnowflakeConfig & { name: string }
Expand All @@ -15,11 +16,6 @@ type ConfigFormProps = {
initialName?: string
}

const getButtonLabel = (isEdit: boolean, isSaving: boolean): string => {
if (isSaving) return isEdit ? 'Saving...' : 'Creating...'
return isEdit ? 'Save changes' : 'Save and continue'
}

const DEFAULTS: SnowflakeConfig = {
account_identifier: '',
database: 'FLAGSMITH',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type WarehouseConnectionCardProps = {
onDelete: () => void
onEdit?: () => void
onSendTestEvent: () => void
onTestConnection?: () => void
isSendingTestEvent: boolean
isLoadingStats?: boolean
}
Expand Down Expand Up @@ -46,6 +47,7 @@ const WarehouseConnectionCard: FC<WarehouseConnectionCardProps> = ({
onDelete,
onEdit,
onSendTestEvent,
onTestConnection,
}) => {
const typeLabel =
connection.warehouse_type !== 'flagsmith'
Expand Down Expand Up @@ -80,7 +82,11 @@ const WarehouseConnectionCard: FC<WarehouseConnectionCardProps> = ({
}
place='top'
>
{STATUS_LABEL[connection.status]}
{connection.status === 'errored' && connection.status_detail
? `${STATUS_LABEL[connection.status]}: ${
connection.status_detail
}`
: STATUS_LABEL[connection.status]}
</Tooltip>
</div>
{typeLabel && (
Expand All @@ -90,6 +96,10 @@ const WarehouseConnectionCard: FC<WarehouseConnectionCardProps> = ({
'account_identifier' in connection.config &&
connection.config.account_identifier &&
`: ${connection.config.account_identifier}`}
{connection.config &&
'host' in connection.config &&
connection.config.host &&
`: ${connection.config.host}`}
</span>
)}
</div>
Expand Down Expand Up @@ -134,14 +144,15 @@ const WarehouseConnectionCard: FC<WarehouseConnectionCardProps> = ({
)}
<WarehouseEventCodeHelp />
<div className='d-flex justify-content-end mt-3'>
{!isFlagsmith && (
{onTestConnection && (
<Button
id='warehouse-connection-test'
theme='outline'
size='small'
disabled
onClick={onTestConnection}
disabled={isSendingTestEvent}
>
Test connection
{isSendingTestEvent ? 'Testing...' : 'Test connection'}
</Button>
)}
{isFlagsmith && !isPending && !isConnected && (
Expand Down
Loading
Loading