Skip to content
2 changes: 1 addition & 1 deletion frontend/common/hooks/useCollapsibleHeight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default function useCollapsibleHeight(open: boolean) {

const style = {
height: height !== undefined ? `${height}px` : 'auto',
overflow: 'hidden' as const,
overflow: (height === undefined ? 'visible' : 'hidden') as const,
transition: 'height 0.3s ease',
}

Expand Down
46 changes: 46 additions & 0 deletions frontend/common/services/useWarehouseConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Res } from 'common/types/responses'
import { Req } from 'common/types/requests'
import { service } from 'common/service'

export const warehouseConnectionService = service
.enhanceEndpoints({ addTagTypes: ['WarehouseConnection'] })
.injectEndpoints({
endpoints: (builder) => ({
createWarehouseConnection: builder.mutation<
Res['warehouseConnections'][number],
Req['createWarehouseConnection']
>({
invalidatesTags: [{ id: 'LIST', type: 'WarehouseConnection' }],
query: ({ environmentId, ...body }) => ({
body,
method: 'POST',
url: `environments/${environmentId}/warehouse-connections/`,
}),
}),
deleteWarehouseConnection: builder.mutation<
void,
Req['deleteWarehouseConnection']
>({
invalidatesTags: [{ id: 'LIST', type: 'WarehouseConnection' }],
query: ({ environmentId, uuid }) => ({
method: 'DELETE',
url: `environments/${environmentId}/warehouse-connections/${uuid}/`,
}),
}),
getWarehouseConnections: builder.query<
Res['warehouseConnections'],
Req['getWarehouseConnections']
>({
providesTags: [{ id: 'LIST', type: 'WarehouseConnection' }],
query: ({ environmentId }) => ({
url: `environments/${environmentId}/warehouse-connections/`,
}),
}),
}),
})

export const {
useCreateWarehouseConnectionMutation,
useDeleteWarehouseConnectionMutation,
useGetWarehouseConnectionsQuery,
} = warehouseConnectionService
3 changes: 3 additions & 0 deletions frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -966,5 +966,8 @@ export type Req = {
project_id: number
gitlab_project_id: number
}>
getWarehouseConnections: { environmentId: string }
createWarehouseConnection: { environmentId: string; warehouse_type: string }
deleteWarehouseConnection: { environmentId: string; uuid: string }
// END OF TYPES
}
16 changes: 16 additions & 0 deletions frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,21 @@ export type ExperimentResults = {
statistics: ExperimentStatistics
}

export type WarehouseConnectionStatus =
| 'pending_connection'
| 'connected'
| 'errored'

export type WarehouseType = 'flagsmith' | 'snowflake' | 'clickhouse'

export type WarehouseConnection = {
uuid: string
warehouse_type: WarehouseType
status: WarehouseConnectionStatus
name: string
created_at: string
}

export type Res = {
segments: PagedResponse<Segment>
segment: Segment
Expand Down Expand Up @@ -1309,5 +1324,6 @@ export type Res = {
gitlabProjects: PagedResponse<GitLabProject>
gitlabIssues: PagedResponse<GitLabIssue>
gitlabMergeRequests: PagedResponse<GitLabMergeRequest>
warehouseConnections: WarehouseConnection[]
// END OF TYPES
}
18 changes: 17 additions & 1 deletion frontend/common/utils/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type PaidFeature =
| 'CREATE_ADDITIONAL_PROJECT'
| '2FA'
| 'RELEASE_PIPELINES'
| 'WAREHOUSE'

export type AppFeature = PaidFeature | 'FEATURE_HEALTH'

Expand Down Expand Up @@ -221,7 +222,11 @@ const Utils = Object.assign({}, BaseUtils, {
flagsmithFeatureExists(flag: string) {
return Object.prototype.hasOwnProperty.call(flagsmith.getAllFlags(), flag)
},
getContentType(contentTypes: ContentType[] | undefined, model: string, type: string) {
getContentType(
contentTypes: ContentType[] | undefined,
model: string,
type: string,
) {
return contentTypes?.find((c: ContentType) => c[model] === type) || null
},
getCreateProjectPermission(organisation: Organisation) {
Expand Down Expand Up @@ -533,6 +538,17 @@ const Utils = Object.assign({}, BaseUtils, {
plan = 'enterprise'
break
}
case 'WAREHOUSE': {
const remotePlans: string[] = Utils.getFlagsmithJSONValue(
'experimentation_warehouse_connection',
[],
)
const allowedPlans = [...remotePlans, 'enterprise']
const planHierarchy: Plan[] = ['start-up', 'scale-up', 'enterprise']
plan =
planHierarchy.find((p) => allowedPlans.includes(p)) || 'enterprise'
break
}

case 'SCHEDULE_FLAGS':
case 'CREATE_ADDITIONAL_PROJECT':
Expand Down
5 changes: 5 additions & 0 deletions frontend/web/components/PlanBasedAccess.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ export const featureDescriptions: Record<PaidFeature, any> = {
description: 'Access all of your feature versions.',
title: 'Version History',
},
'WAREHOUSE': {
description:
'Connect a data warehouse to collect experimentation and analytics data from your environments.',
title: 'Warehouse Connections',
},
}

const PlanBasedBanner: FC<PlanBasedBannerType> = ({ children, ...props }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ import { useGetEnvironmentQuery } from 'common/services/useEnvironment'
import { useRouteContext } from 'components/providers/RouteContext'
import SettingTitle from 'components/SettingTitle'
import ChangeRequestsSetting from 'components/ChangeRequestsSetting'
import PlanBasedBanner from 'components/PlanBasedAccess'
import WarehouseTab from './tabs/warehouse-tab'

const showDisabledFlagOptions: { label: string; value: boolean | null }[] = [
{ label: 'Inherit from Project', value: null },
Expand Down Expand Up @@ -900,6 +902,19 @@ const EnvironmentSettingsPage: React.FC = () => {
)}
</FormGroup>
</TabItem>
<TabItem tabLabel='Warehouse'>
<PlanBasedBanner
className='mt-4'
feature='WAREHOUSE'
theme='page'
>
<div className='mt-4'>
<WarehouseTab
environmentId={match.params.environmentId}
/>
</div>
</PlanBasedBanner>
</TabItem>
{metadataEnable && environmentContentType?.id && (
<TabItem tabLabel='Custom Fields'>
<FormGroup className='mt-5 setting'>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './EnvironmentSettingsPage'
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, { FC, useState } from 'react'
import {
WarehouseConnection,
WarehouseConnectionStatus,
} from 'common/types/responses'
import ColorSwatch from 'components/ColorSwatch'
import Tooltip from 'components/Tooltip'
import Icon from 'components/icons/Icon'
import Button from 'components/base/forms/Button'
import WarehouseEventCodeHelp from './WarehouseEventCodeHelp'
import WarehouseStats from './WarehouseStats'
import useCollapsibleHeight from 'common/hooks/useCollapsibleHeight'

type WarehouseConnectionCardProps = {
connection: WarehouseConnection
onDelete: () => void
}

const STATUS_COLOUR: Record<WarehouseConnectionStatus, string> = {
connected: '#27AE60',
errored: '#EB5757',
pending_connection: '#F2C94C',
}

const STATUS_LABEL: Record<WarehouseConnectionStatus, string> = {
connected: 'Connected',
errored: 'Errored',
pending_connection: 'Pending Connection',
}

const WarehouseConnectionCard: FC<WarehouseConnectionCardProps> = ({
connection,
onDelete,
}) => {
const [open, setOpen] = useState(false)
const { contentRef, style: collapsibleStyle } = useCollapsibleHeight(open)

const handleDelete = (e: React.MouseEvent) => {
e.stopPropagation()
openConfirm({
body: 'Are you sure you want to remove this warehouse connection?',
onYes: onDelete,
title: 'Remove Warehouse Connection',
})
}

return (
<div className='d-flex flex-column px-3 py-3 accordion-card m-0'>
<div
className='d-flex flex-row align-items-center justify-content-between'
style={{ cursor: 'pointer' }}
onClick={() => setOpen(!open)}
>
<div className='d-flex flex-row align-items-center gap-2'>
<span className='font-weight-medium'>{connection.name}</span>
<Tooltip
title={
<ColorSwatch
color={STATUS_COLOUR[connection.status]}
size='sm'
shape='circle'
/>
}
place='top'
>
{STATUS_LABEL[connection.status]}
</Tooltip>
</div>
<div className='d-flex flex-row align-items-center gap-2'>
<button
type='button'
className='btn btn-with-icon'
onClick={handleDelete}
>
<Icon name='trash-2' width={20} fill='#656D7B' />
</button>
<span className='p-1' aria-label={open ? 'Collapse' : 'Expand'}>
<Icon name={open ? 'chevron-up' : 'chevron-down'} width={16} />
</span>
</div>
</div>
<div ref={contentRef} style={collapsibleStyle}>
<div className='mt-3 mb-2 d-flex flex-column gap-3'>
{connection.status === 'pending_connection' ? (
<>
<WarehouseEventCodeHelp />
<Button className='align-self-end' theme='primary'>
Send your first event
</Button>
</>
) : (
<WarehouseStats
errored={connection.status === 'errored'}
lastEventReceived='19 May 2026, 14:32'
totalEventsReceived={1247}
uniqueEventsCount={38}
/>
)}
</div>
</div>
</div>
)
}

WarehouseConnectionCard.displayName = 'WarehouseConnectionCard'
export default WarehouseConnectionCard
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React, { FC } from 'react'
import CodeHelp from 'components/CodeHelp'

type SnippetEntry = {
code: string
enabled: boolean
}

const helloWorldSnippets: Record<string, SnippetEntry> = {
'.NET': {
code: `using Flagsmith;

var client = new FlagsmithClient("YOUR_ENVIRONMENT_KEY");
Console.WriteLine("Hello, Flagsmith Warehouse!");`,
enabled: false,
},
'Flutter': {
code: `import 'package:flagsmith/flagsmith.dart';

final flagsmith = FlagsmithClient(apiKey: 'YOUR_ENVIRONMENT_KEY');
print('Hello, Flagsmith Warehouse!');`,
enabled: false,
},
'Go': {
code: `import (
flagsmith "github.com/Flagsmith/flagsmith-go-client/v5"
)

client := flagsmith.NewClient("YOUR_ENVIRONMENT_KEY")
fmt.Println("Hello, Flagsmith Warehouse!")`,
enabled: false,
},
'Java': {
code: `import com.flagsmith.FlagsmithClient;

FlagsmithClient client = FlagsmithClient
.newBuilder()
.setApiKey("YOUR_ENVIRONMENT_KEY")
.build();
System.out.println("Hello, Flagsmith Warehouse!");`,
enabled: false,
},
'JavaScript': {
code: `import flagsmith from 'flagsmith';

flagsmith.init({ environmentID: 'YOUR_ENVIRONMENT_KEY' });
console.log('Hello, Flagsmith Warehouse!');`,
enabled: true,
},
'Node JS': {
code: `import Flagsmith from 'flagsmith-nodejs';

const flagsmith = new Flagsmith({ environmentKey: 'YOUR_ENVIRONMENT_KEY' });
console.log('Hello, Flagsmith Warehouse!');`,
enabled: false,
},
'PHP': {
code: `use Flagsmith\\Flagsmith;

$flagsmith = new Flagsmith('YOUR_ENVIRONMENT_KEY');
echo "Hello, Flagsmith Warehouse!";`,
enabled: false,
},
'Python': {
code: `from flagsmith import Flagsmith

flagsmith = Flagsmith(environment_key="YOUR_ENVIRONMENT_KEY")
print("Hello, Flagsmith Warehouse!")`,
enabled: true,
},
'Ruby': {
code: `require "flagsmith"

flagsmith = Flagsmith::Client.new(environment_key: "YOUR_ENVIRONMENT_KEY")
puts "Hello, Flagsmith Warehouse!"`,
enabled: false,
},
'Rust': {
code: `use flagsmith::Flagsmith;

let flagsmith = Flagsmith::new("YOUR_ENVIRONMENT_KEY".to_string());
println!("Hello, Flagsmith Warehouse!");`,
enabled: false,
},
'iOS': {
code: `import FlagsmithClient

Flagsmith.shared.apiKey = "YOUR_ENVIRONMENT_KEY"
print("Hello, Flagsmith Warehouse!")`,
enabled: false,
},
}

const enabledSnippets = Object.fromEntries(
Object.entries(helloWorldSnippets)
.filter(([, entry]) => entry.enabled)
.map(([name, entry]) => [name, entry.code]),
)

const WarehouseEventCodeHelp: FC = () => (
<div>
<p className='text-center fst-italic text-muted'>
Verify your connection by sending your first custom event using one of our
SDKs
</p>
<CodeHelp
title='Send your first event'
snippets={enabledSnippets}
showInitially
hideHeader
/>
</div>
)

WarehouseEventCodeHelp.displayName = 'WarehouseEventCodeHelp'
export default WarehouseEventCodeHelp
Loading
Loading