From b548c249c2e5ab9411b37b173506e4bce2380b09 Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 19 May 2026 15:46:28 +0200 Subject: [PATCH 1/9] feat: add-warehouse-connection-types-and-rtk-query-service --- .../common/services/useWarehouseConnection.ts | 46 +++++++++++++++++++ frontend/common/types/requests.ts | 3 ++ frontend/common/types/responses.ts | 16 +++++++ 3 files changed, 65 insertions(+) create mode 100644 frontend/common/services/useWarehouseConnection.ts diff --git a/frontend/common/services/useWarehouseConnection.ts b/frontend/common/services/useWarehouseConnection.ts new file mode 100644 index 000000000000..1022c6ce1fdc --- /dev/null +++ b/frontend/common/services/useWarehouseConnection.ts @@ -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['warehouseConnection'], + Req['createWarehouseConnection'] + >({ + invalidatesTags: [{ id: 'SINGLETON', type: 'WarehouseConnection' }], + query: ({ environmentId, ...body }) => ({ + body, + method: 'POST', + url: `environments/${environmentId}/warehouse-connection/`, + }), + }), + deleteWarehouseConnection: builder.mutation< + void, + Req['deleteWarehouseConnection'] + >({ + invalidatesTags: [{ id: 'SINGLETON', type: 'WarehouseConnection' }], + query: ({ environmentId }) => ({ + method: 'DELETE', + url: `environments/${environmentId}/warehouse-connection/`, + }), + }), + getWarehouseConnection: builder.query< + Res['warehouseConnection'], + Req['getWarehouseConnection'] + >({ + providesTags: [{ id: 'SINGLETON', type: 'WarehouseConnection' }], + query: ({ environmentId }) => ({ + url: `environments/${environmentId}/warehouse-connection/`, + }), + }), + }), + }) + +export const { + useCreateWarehouseConnectionMutation, + useDeleteWarehouseConnectionMutation, + useGetWarehouseConnectionQuery, +} = warehouseConnectionService diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index 09cc47986326..2817d8d78d5a 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -966,5 +966,8 @@ export type Req = { project_id: number gitlab_project_id: number }> + getWarehouseConnection: { environmentId: string } + createWarehouseConnection: { environmentId: string; warehouse_type: string } + deleteWarehouseConnection: { environmentId: string } // END OF TYPES } diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index b765b948b4a1..d3d1fdbb2560 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -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 @@ -1309,5 +1324,6 @@ export type Res = { gitlabProjects: PagedResponse gitlabIssues: PagedResponse gitlabMergeRequests: PagedResponse + warehouseConnection: WarehouseConnection // END OF TYPES } From 59dc25cf29f6d2d6ec23d2ffd62fa396928917d2 Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 19 May 2026 15:46:48 +0200 Subject: [PATCH 2/9] feat: add-warehouse-tab-components-for-environment-settings --- .../components/WarehouseConnectionCard.tsx | 96 +++++++++++++++++++ .../web/components/WarehouseEventCodeHelp.tsx | 72 ++++++++++++++ frontend/web/components/WarehouseTab.tsx | 81 ++++++++++++++++ 3 files changed, 249 insertions(+) create mode 100644 frontend/web/components/WarehouseConnectionCard.tsx create mode 100644 frontend/web/components/WarehouseEventCodeHelp.tsx create mode 100644 frontend/web/components/WarehouseTab.tsx diff --git a/frontend/web/components/WarehouseConnectionCard.tsx b/frontend/web/components/WarehouseConnectionCard.tsx new file mode 100644 index 000000000000..8de6e183f3c8 --- /dev/null +++ b/frontend/web/components/WarehouseConnectionCard.tsx @@ -0,0 +1,96 @@ +import React, { FC, useState } from 'react' +import { + WarehouseConnection, + WarehouseConnectionStatus, +} from 'common/types/responses' +import ColorSwatch from './ColorSwatch' +import Tooltip from './Tooltip' +import Icon from './icons/Icon' +import Button from './base/forms/Button' +import WarehouseEventCodeHelp from './WarehouseEventCodeHelp' +import useCollapsibleHeight from 'common/hooks/useCollapsibleHeight' + +type WarehouseConnectionCardProps = { + connection: WarehouseConnection + onDelete: () => void +} + +const STATUS_COLOUR: Record = { + connected: '#27AE60', + errored: '#EB5757', + pending_connection: '#F2C94C', +} + +const STATUS_LABEL: Record = { + connected: 'Connected', + errored: 'Errored', + pending_connection: 'Pending Connection', +} + +const WarehouseConnectionCard: FC = ({ + 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 ( +
+
setOpen(!open)} + > +
+ {connection.name} + + } + place='top' + > + {STATUS_LABEL[connection.status]} + +
+
+ + + + +
+
+
+
+ + {connection.status === 'pending_connection' && ( + + )} +
+
+
+ ) +} + +WarehouseConnectionCard.displayName = 'WarehouseConnectionCard' +export default WarehouseConnectionCard diff --git a/frontend/web/components/WarehouseEventCodeHelp.tsx b/frontend/web/components/WarehouseEventCodeHelp.tsx new file mode 100644 index 000000000000..37fe2481be89 --- /dev/null +++ b/frontend/web/components/WarehouseEventCodeHelp.tsx @@ -0,0 +1,72 @@ +import React, { FC } from 'react' +import CodeHelp from './CodeHelp' + +const helloWorldSnippets = { + '.NET': `using Flagsmith; + +var client = new FlagsmithClient("YOUR_ENVIRONMENT_KEY"); +Console.WriteLine("Hello, Flagsmith Warehouse!");`, + 'Flutter': `import 'package:flagsmith/flagsmith.dart'; + +final flagsmith = FlagsmithClient(apiKey: 'YOUR_ENVIRONMENT_KEY'); +print('Hello, Flagsmith Warehouse!');`, + 'Go': `import ( + flagsmith "github.com/Flagsmith/flagsmith-go-client/v5" +) + +client := flagsmith.NewClient("YOUR_ENVIRONMENT_KEY") +fmt.Println("Hello, Flagsmith Warehouse!")`, + 'Java': `import com.flagsmith.FlagsmithClient; + +FlagsmithClient client = FlagsmithClient + .newBuilder() + .setApiKey("YOUR_ENVIRONMENT_KEY") + .build(); +System.out.println("Hello, Flagsmith Warehouse!");`, + 'JavaScript': `import flagsmith from 'flagsmith'; + +flagsmith.init({ environmentID: 'YOUR_ENVIRONMENT_KEY' }); +console.log('Hello, Flagsmith Warehouse!');`, + 'Node JS': `import Flagsmith from 'flagsmith-nodejs'; + +const flagsmith = new Flagsmith({ environmentKey: 'YOUR_ENVIRONMENT_KEY' }); +console.log('Hello, Flagsmith Warehouse!');`, + 'PHP': `use Flagsmith\\Flagsmith; + +$flagsmith = new Flagsmith('YOUR_ENVIRONMENT_KEY'); +echo "Hello, Flagsmith Warehouse!";`, + 'Python': `from flagsmith import Flagsmith + +flagsmith = Flagsmith(environment_key="YOUR_ENVIRONMENT_KEY") +print("Hello, Flagsmith Warehouse!")`, + 'Ruby': `require "flagsmith" + +flagsmith = Flagsmith::Client.new(environment_key: "YOUR_ENVIRONMENT_KEY") +puts "Hello, Flagsmith Warehouse!"`, + 'Rust': `use flagsmith::Flagsmith; + +let flagsmith = Flagsmith::new("YOUR_ENVIRONMENT_KEY".to_string()); +println!("Hello, Flagsmith Warehouse!");`, + 'iOS': `import FlagsmithClient + +Flagsmith.shared.apiKey = "YOUR_ENVIRONMENT_KEY" +print("Hello, Flagsmith Warehouse!")`, +} + +const WarehouseEventCodeHelp: FC = () => ( +
+

+ Verify your connection by sending your first custom event using one of our + SDKs +

+ +
+) + +WarehouseEventCodeHelp.displayName = 'WarehouseEventCodeHelp' +export default WarehouseEventCodeHelp diff --git a/frontend/web/components/WarehouseTab.tsx b/frontend/web/components/WarehouseTab.tsx new file mode 100644 index 000000000000..4d08466c7cc9 --- /dev/null +++ b/frontend/web/components/WarehouseTab.tsx @@ -0,0 +1,81 @@ +import React, { FC } from 'react' +import { + useCreateWarehouseConnectionMutation, + useDeleteWarehouseConnectionMutation, + useGetWarehouseConnectionQuery, +} from 'common/services/useWarehouseConnection' +import Setting from './Setting' +import WarehouseConnectionCard from './WarehouseConnectionCard' + +type WarehouseTabProps = { + environmentId: string +} + +const WarehouseTab: FC = ({ environmentId }) => { + const { + data: connection, + error, + isLoading, + } = useGetWarehouseConnectionQuery( + { environmentId }, + { skip: !environmentId }, + ) + const [createConnection, { isLoading: isCreating }] = + useCreateWarehouseConnectionMutation() + const [deleteConnection] = useDeleteWarehouseConnectionMutation() + + const hasNoConnection = + !connection && !isLoading && (error as { status?: number })?.status === 404 + + if (hasNoConnection || (!connection && !isLoading)) { + return ( +
+ + openConfirm({ + body: 'This will enable a Flagsmith Warehouse connection for this environment. Are you sure you want to proceed?', + onYes: () => + createConnection({ + environmentId, + warehouse_type: 'flagsmith', + }) + .unwrap() + .then(() => toast('Warehouse connection created')) + .catch(() => + toast('Failed to create warehouse connection', 'danger'), + ), + title: 'Connect Flagsmith Warehouse', + }) + } + /> +
+ ) + } + + if (!connection) { + return null + } + + return ( +
+ + deleteConnection({ environmentId }) + .unwrap() + .then(() => toast('Warehouse connection removed')) + .catch(() => + toast('Failed to remove warehouse connection', 'danger'), + ) + } + /> +
+ ) +} + +WarehouseTab.displayName = 'WarehouseTab' +export default WarehouseTab From 0bfec194010023b30e04ea7f6d2cd389bc1b36ea Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 19 May 2026 15:46:59 +0200 Subject: [PATCH 3/9] feat: gate-warehouse-tab-with-plan-based-permission-and-remote-flag-value --- frontend/common/utils/utils.tsx | 18 +++++++++++++++++- .../pages/EnvironmentSettingsPage.tsx | 10 ++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/frontend/common/utils/utils.tsx b/frontend/common/utils/utils.tsx index 7e6df844397a..4ff3a748ddc2 100644 --- a/frontend/common/utils/utils.tsx +++ b/frontend/common/utils/utils.tsx @@ -54,6 +54,7 @@ export type PaidFeature = | 'CREATE_ADDITIONAL_PROJECT' | '2FA' | 'RELEASE_PIPELINES' + | 'WAREHOUSE' export type AppFeature = PaidFeature | 'FEATURE_HEALTH' @@ -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) { @@ -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': diff --git a/frontend/web/components/pages/EnvironmentSettingsPage.tsx b/frontend/web/components/pages/EnvironmentSettingsPage.tsx index c7a465892b48..4c3e3a12129b 100644 --- a/frontend/web/components/pages/EnvironmentSettingsPage.tsx +++ b/frontend/web/components/pages/EnvironmentSettingsPage.tsx @@ -46,6 +46,7 @@ import { useGetEnvironmentQuery } from 'common/services/useEnvironment' import { useRouteContext } from 'components/providers/RouteContext' import SettingTitle from 'components/SettingTitle' import ChangeRequestsSetting from 'components/ChangeRequestsSetting' +import WarehouseTab from 'components/WarehouseTab' const showDisabledFlagOptions: { label: string; value: boolean | null }[] = [ { label: 'Inherit from Project', value: null }, @@ -900,6 +901,15 @@ const EnvironmentSettingsPage: React.FC = () => { )} + {Utils.getPlansPermission('WAREHOUSE') && ( + +
+ +
+
+ )} {metadataEnable && environmentContentType?.id && ( From 6e83c9d7cf975a82b57cd6304087e7183596c0ec Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 19 May 2026 15:47:09 +0200 Subject: [PATCH 4/9] fix: allow-dropdown-overflow-in-collapsible-sections-once-fully-expanded --- frontend/common/hooks/useCollapsibleHeight.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/common/hooks/useCollapsibleHeight.ts b/frontend/common/hooks/useCollapsibleHeight.ts index a568443e1448..db20df27dbad 100644 --- a/frontend/common/hooks/useCollapsibleHeight.ts +++ b/frontend/common/hooks/useCollapsibleHeight.ts @@ -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', } From 76703f531a57cc4634d94e45e63764231609e63e Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 19 May 2026 17:39:13 +0200 Subject: [PATCH 5/9] feat: update frontend for list-based warehouse connections endpoint --- .../common/services/useWarehouseConnection.ts | 22 +++--- frontend/common/types/requests.ts | 2 +- frontend/common/types/responses.ts | 2 +- .../web/components/WarehouseEventCodeHelp.tsx | 70 +++++++++++++++---- frontend/web/components/WarehouseTab.tsx | 39 +++++------ 5 files changed, 89 insertions(+), 46 deletions(-) diff --git a/frontend/common/services/useWarehouseConnection.ts b/frontend/common/services/useWarehouseConnection.ts index 1022c6ce1fdc..a7521310df57 100644 --- a/frontend/common/services/useWarehouseConnection.ts +++ b/frontend/common/services/useWarehouseConnection.ts @@ -7,33 +7,33 @@ export const warehouseConnectionService = service .injectEndpoints({ endpoints: (builder) => ({ createWarehouseConnection: builder.mutation< - Res['warehouseConnection'], + Res['warehouseConnections'][number], Req['createWarehouseConnection'] >({ - invalidatesTags: [{ id: 'SINGLETON', type: 'WarehouseConnection' }], + invalidatesTags: [{ id: 'LIST', type: 'WarehouseConnection' }], query: ({ environmentId, ...body }) => ({ body, method: 'POST', - url: `environments/${environmentId}/warehouse-connection/`, + url: `environments/${environmentId}/warehouse-connections/`, }), }), deleteWarehouseConnection: builder.mutation< void, Req['deleteWarehouseConnection'] >({ - invalidatesTags: [{ id: 'SINGLETON', type: 'WarehouseConnection' }], + invalidatesTags: [{ id: 'LIST', type: 'WarehouseConnection' }], query: ({ environmentId }) => ({ method: 'DELETE', - url: `environments/${environmentId}/warehouse-connection/`, + url: `environments/${environmentId}/warehouse-connections/`, }), }), - getWarehouseConnection: builder.query< - Res['warehouseConnection'], - Req['getWarehouseConnection'] + getWarehouseConnections: builder.query< + Res['warehouseConnections'], + Req['getWarehouseConnections'] >({ - providesTags: [{ id: 'SINGLETON', type: 'WarehouseConnection' }], + providesTags: [{ id: 'LIST', type: 'WarehouseConnection' }], query: ({ environmentId }) => ({ - url: `environments/${environmentId}/warehouse-connection/`, + url: `environments/${environmentId}/warehouse-connections/`, }), }), }), @@ -42,5 +42,5 @@ export const warehouseConnectionService = service export const { useCreateWarehouseConnectionMutation, useDeleteWarehouseConnectionMutation, - useGetWarehouseConnectionQuery, + useGetWarehouseConnectionsQuery, } = warehouseConnectionService diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index 2817d8d78d5a..cd303cfd9fda 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -966,7 +966,7 @@ export type Req = { project_id: number gitlab_project_id: number }> - getWarehouseConnection: { environmentId: string } + getWarehouseConnections: { environmentId: string } createWarehouseConnection: { environmentId: string; warehouse_type: string } deleteWarehouseConnection: { environmentId: string } // END OF TYPES diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index d3d1fdbb2560..c73c48b13927 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -1324,6 +1324,6 @@ export type Res = { gitlabProjects: PagedResponse gitlabIssues: PagedResponse gitlabMergeRequests: PagedResponse - warehouseConnection: WarehouseConnection + warehouseConnections: WarehouseConnection[] // END OF TYPES } diff --git a/frontend/web/components/WarehouseEventCodeHelp.tsx b/frontend/web/components/WarehouseEventCodeHelp.tsx index 37fe2481be89..d6ca9593ef86 100644 --- a/frontend/web/components/WarehouseEventCodeHelp.tsx +++ b/frontend/web/components/WarehouseEventCodeHelp.tsx @@ -1,58 +1,102 @@ import React, { FC } from 'react' import CodeHelp from './CodeHelp' -const helloWorldSnippets = { - '.NET': `using Flagsmith; +type SnippetEntry = { + code: string + enabled: boolean +} + +const helloWorldSnippets: Record = { + '.NET': { + code: `using Flagsmith; var client = new FlagsmithClient("YOUR_ENVIRONMENT_KEY"); Console.WriteLine("Hello, Flagsmith Warehouse!");`, - 'Flutter': `import 'package:flagsmith/flagsmith.dart'; + enabled: false, + }, + 'Flutter': { + code: `import 'package:flagsmith/flagsmith.dart'; final flagsmith = FlagsmithClient(apiKey: 'YOUR_ENVIRONMENT_KEY'); print('Hello, Flagsmith Warehouse!');`, - 'Go': `import ( + enabled: false, + }, + 'Go': { + code: `import ( flagsmith "github.com/Flagsmith/flagsmith-go-client/v5" ) client := flagsmith.NewClient("YOUR_ENVIRONMENT_KEY") fmt.Println("Hello, Flagsmith Warehouse!")`, - 'Java': `import com.flagsmith.FlagsmithClient; + enabled: false, + }, + 'Java': { + code: `import com.flagsmith.FlagsmithClient; FlagsmithClient client = FlagsmithClient .newBuilder() .setApiKey("YOUR_ENVIRONMENT_KEY") .build(); System.out.println("Hello, Flagsmith Warehouse!");`, - 'JavaScript': `import flagsmith from 'flagsmith'; + enabled: false, + }, + 'JavaScript': { + code: `import flagsmith from 'flagsmith'; flagsmith.init({ environmentID: 'YOUR_ENVIRONMENT_KEY' }); console.log('Hello, Flagsmith Warehouse!');`, - 'Node JS': `import Flagsmith from 'flagsmith-nodejs'; + enabled: true, + }, + 'Node JS': { + code: `import Flagsmith from 'flagsmith-nodejs'; const flagsmith = new Flagsmith({ environmentKey: 'YOUR_ENVIRONMENT_KEY' }); console.log('Hello, Flagsmith Warehouse!');`, - 'PHP': `use Flagsmith\\Flagsmith; + enabled: false, + }, + 'PHP': { + code: `use Flagsmith\\Flagsmith; $flagsmith = new Flagsmith('YOUR_ENVIRONMENT_KEY'); echo "Hello, Flagsmith Warehouse!";`, - 'Python': `from flagsmith import Flagsmith + enabled: false, + }, + 'Python': { + code: `from flagsmith import Flagsmith flagsmith = Flagsmith(environment_key="YOUR_ENVIRONMENT_KEY") print("Hello, Flagsmith Warehouse!")`, - 'Ruby': `require "flagsmith" + enabled: true, + }, + 'Ruby': { + code: `require "flagsmith" flagsmith = Flagsmith::Client.new(environment_key: "YOUR_ENVIRONMENT_KEY") puts "Hello, Flagsmith Warehouse!"`, - 'Rust': `use flagsmith::Flagsmith; + enabled: false, + }, + 'Rust': { + code: `use flagsmith::Flagsmith; let flagsmith = Flagsmith::new("YOUR_ENVIRONMENT_KEY".to_string()); println!("Hello, Flagsmith Warehouse!");`, - 'iOS': `import FlagsmithClient + 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 = () => (

@@ -61,7 +105,7 @@ const WarehouseEventCodeHelp: FC = () => (

diff --git a/frontend/web/components/WarehouseTab.tsx b/frontend/web/components/WarehouseTab.tsx index 4d08466c7cc9..bced23cb3a27 100644 --- a/frontend/web/components/WarehouseTab.tsx +++ b/frontend/web/components/WarehouseTab.tsx @@ -2,7 +2,7 @@ import React, { FC } from 'react' import { useCreateWarehouseConnectionMutation, useDeleteWarehouseConnectionMutation, - useGetWarehouseConnectionQuery, + useGetWarehouseConnectionsQuery, } from 'common/services/useWarehouseConnection' import Setting from './Setting' import WarehouseConnectionCard from './WarehouseConnectionCard' @@ -12,11 +12,7 @@ type WarehouseTabProps = { } const WarehouseTab: FC = ({ environmentId }) => { - const { - data: connection, - error, - isLoading, - } = useGetWarehouseConnectionQuery( + const { data: connections, isLoading } = useGetWarehouseConnectionsQuery( { environmentId }, { skip: !environmentId }, ) @@ -25,9 +21,9 @@ const WarehouseTab: FC = ({ environmentId }) => { const [deleteConnection] = useDeleteWarehouseConnectionMutation() const hasNoConnection = - !connection && !isLoading && (error as { status?: number })?.status === 404 + !isLoading && (!connections || connections.length === 0) - if (hasNoConnection || (!connection && !isLoading)) { + if (hasNoConnection) { return (
= ({ environmentId }) => { ) } - if (!connection) { + if (!connections || connections.length === 0) { return null } return (
- - deleteConnection({ environmentId }) - .unwrap() - .then(() => toast('Warehouse connection removed')) - .catch(() => - toast('Failed to remove warehouse connection', 'danger'), - ) - } - /> + {connections.map((connection) => ( + + deleteConnection({ environmentId }) + .unwrap() + .then(() => toast('Warehouse connection removed')) + .catch(() => + toast('Failed to remove warehouse connection', 'danger'), + ) + } + /> + ))}
) } From e0867f99ed0578e9e32d3a40ae8e218e72d70d30 Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 19 May 2026 17:51:36 +0200 Subject: [PATCH 6/9] feat: added hardcoded stats --- .../components/WarehouseConnectionCard.tsx | 20 ++++++-- frontend/web/components/WarehouseStats.tsx | 46 +++++++++++++++++++ 2 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 frontend/web/components/WarehouseStats.tsx diff --git a/frontend/web/components/WarehouseConnectionCard.tsx b/frontend/web/components/WarehouseConnectionCard.tsx index 8de6e183f3c8..f51aa92798cf 100644 --- a/frontend/web/components/WarehouseConnectionCard.tsx +++ b/frontend/web/components/WarehouseConnectionCard.tsx @@ -8,6 +8,7 @@ import Tooltip from './Tooltip' import Icon from './icons/Icon' import Button from './base/forms/Button' import WarehouseEventCodeHelp from './WarehouseEventCodeHelp' +import WarehouseStats from './WarehouseStats' import useCollapsibleHeight from 'common/hooks/useCollapsibleHeight' type WarehouseConnectionCardProps = { @@ -80,11 +81,20 @@ const WarehouseConnectionCard: FC = ({
- - {connection.status === 'pending_connection' && ( - + {connection.status === 'pending_connection' ? ( + <> + + + + ) : ( + )}
diff --git a/frontend/web/components/WarehouseStats.tsx b/frontend/web/components/WarehouseStats.tsx new file mode 100644 index 000000000000..d8f3285e068f --- /dev/null +++ b/frontend/web/components/WarehouseStats.tsx @@ -0,0 +1,46 @@ +import React, { FC } from 'react' +import Icon from './icons/Icon' + +type WarehouseStatsProps = { + errored: boolean + lastEventReceived: string + totalEventsReceived: number + uniqueEventsCount: number +} + +const WarehouseStats: FC = ({ + errored, + lastEventReceived, + totalEventsReceived, + uniqueEventsCount, +}) => ( +
+ {errored && ( +
+ + + The connection is currently in error, please contact our team + +
+ )} +
+ Last event received: + {lastEventReceived} +
+
+ Total events received: + + {totalEventsReceived.toLocaleString()} + +
+
+ Number of unique events: + + {uniqueEventsCount.toLocaleString()} + +
+
+) + +WarehouseStats.displayName = 'WarehouseStats' +export default WarehouseStats From 5b694af23d50f68ded8e9a625fb158b3520815a0 Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 19 May 2026 18:00:34 +0200 Subject: [PATCH 7/9] feat: restructured cleanly new components --- .../EnvironmentSettingsPage.tsx | 2 +- .../web/components/pages/environment-settings/index.ts | 1 + .../tabs/warehouse-tab}/WarehouseConnectionCard.tsx | 8 ++++---- .../tabs/warehouse-tab}/WarehouseEventCodeHelp.tsx | 2 +- .../tabs/warehouse-tab}/WarehouseStats.tsx | 2 +- .../environment-settings/tabs/warehouse-tab/index.tsx} | 2 +- frontend/web/routes.js | 2 +- 7 files changed, 10 insertions(+), 9 deletions(-) rename frontend/web/components/pages/{ => environment-settings}/EnvironmentSettingsPage.tsx (99%) create mode 100644 frontend/web/components/pages/environment-settings/index.ts rename frontend/web/components/{ => pages/environment-settings/tabs/warehouse-tab}/WarehouseConnectionCard.tsx (94%) rename frontend/web/components/{ => pages/environment-settings/tabs/warehouse-tab}/WarehouseEventCodeHelp.tsx (98%) rename frontend/web/components/{ => pages/environment-settings/tabs/warehouse-tab}/WarehouseStats.tsx (97%) rename frontend/web/components/{WarehouseTab.tsx => pages/environment-settings/tabs/warehouse-tab/index.tsx} (98%) diff --git a/frontend/web/components/pages/EnvironmentSettingsPage.tsx b/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx similarity index 99% rename from frontend/web/components/pages/EnvironmentSettingsPage.tsx rename to frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx index 4c3e3a12129b..6f02e3cf5560 100644 --- a/frontend/web/components/pages/EnvironmentSettingsPage.tsx +++ b/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx @@ -46,7 +46,7 @@ import { useGetEnvironmentQuery } from 'common/services/useEnvironment' import { useRouteContext } from 'components/providers/RouteContext' import SettingTitle from 'components/SettingTitle' import ChangeRequestsSetting from 'components/ChangeRequestsSetting' -import WarehouseTab from 'components/WarehouseTab' +import WarehouseTab from './tabs/warehouse-tab' const showDisabledFlagOptions: { label: string; value: boolean | null }[] = [ { label: 'Inherit from Project', value: null }, diff --git a/frontend/web/components/pages/environment-settings/index.ts b/frontend/web/components/pages/environment-settings/index.ts new file mode 100644 index 000000000000..1382adb3c229 --- /dev/null +++ b/frontend/web/components/pages/environment-settings/index.ts @@ -0,0 +1 @@ +export { default } from './EnvironmentSettingsPage' diff --git a/frontend/web/components/WarehouseConnectionCard.tsx b/frontend/web/components/pages/environment-settings/tabs/warehouse-tab/WarehouseConnectionCard.tsx similarity index 94% rename from frontend/web/components/WarehouseConnectionCard.tsx rename to frontend/web/components/pages/environment-settings/tabs/warehouse-tab/WarehouseConnectionCard.tsx index f51aa92798cf..bc43aea2db62 100644 --- a/frontend/web/components/WarehouseConnectionCard.tsx +++ b/frontend/web/components/pages/environment-settings/tabs/warehouse-tab/WarehouseConnectionCard.tsx @@ -3,10 +3,10 @@ import { WarehouseConnection, WarehouseConnectionStatus, } from 'common/types/responses' -import ColorSwatch from './ColorSwatch' -import Tooltip from './Tooltip' -import Icon from './icons/Icon' -import Button from './base/forms/Button' +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' diff --git a/frontend/web/components/WarehouseEventCodeHelp.tsx b/frontend/web/components/pages/environment-settings/tabs/warehouse-tab/WarehouseEventCodeHelp.tsx similarity index 98% rename from frontend/web/components/WarehouseEventCodeHelp.tsx rename to frontend/web/components/pages/environment-settings/tabs/warehouse-tab/WarehouseEventCodeHelp.tsx index d6ca9593ef86..096eea47f787 100644 --- a/frontend/web/components/WarehouseEventCodeHelp.tsx +++ b/frontend/web/components/pages/environment-settings/tabs/warehouse-tab/WarehouseEventCodeHelp.tsx @@ -1,5 +1,5 @@ import React, { FC } from 'react' -import CodeHelp from './CodeHelp' +import CodeHelp from 'components/CodeHelp' type SnippetEntry = { code: string diff --git a/frontend/web/components/WarehouseStats.tsx b/frontend/web/components/pages/environment-settings/tabs/warehouse-tab/WarehouseStats.tsx similarity index 97% rename from frontend/web/components/WarehouseStats.tsx rename to frontend/web/components/pages/environment-settings/tabs/warehouse-tab/WarehouseStats.tsx index d8f3285e068f..798f85add889 100644 --- a/frontend/web/components/WarehouseStats.tsx +++ b/frontend/web/components/pages/environment-settings/tabs/warehouse-tab/WarehouseStats.tsx @@ -1,5 +1,5 @@ import React, { FC } from 'react' -import Icon from './icons/Icon' +import Icon from 'components/icons/Icon' type WarehouseStatsProps = { errored: boolean diff --git a/frontend/web/components/WarehouseTab.tsx b/frontend/web/components/pages/environment-settings/tabs/warehouse-tab/index.tsx similarity index 98% rename from frontend/web/components/WarehouseTab.tsx rename to frontend/web/components/pages/environment-settings/tabs/warehouse-tab/index.tsx index bced23cb3a27..c71e61a6ecf0 100644 --- a/frontend/web/components/WarehouseTab.tsx +++ b/frontend/web/components/pages/environment-settings/tabs/warehouse-tab/index.tsx @@ -4,7 +4,7 @@ import { useDeleteWarehouseConnectionMutation, useGetWarehouseConnectionsQuery, } from 'common/services/useWarehouseConnection' -import Setting from './Setting' +import Setting from 'components/Setting' import WarehouseConnectionCard from './WarehouseConnectionCard' type WarehouseTabProps = { diff --git a/frontend/web/routes.js b/frontend/web/routes.js index 3c526e7e9342..cc9802e07536 100644 --- a/frontend/web/routes.js +++ b/frontend/web/routes.js @@ -17,7 +17,7 @@ import AccountSettingsPage from './components/pages/AccountSettingsPage' import NotFoundErrorPage from './components/pages/NotFoundErrorPage' import ProjectSettingsPage from './components/pages/project-settings' import PasswordResetPage from './components/pages/PasswordResetPage' -import EnvironmentSettingsPage from './components/pages/EnvironmentSettingsPage' +import EnvironmentSettingsPage from './components/pages/environment-settings' import InvitePage from './components/pages/InvitePage' import NotFoundPage from './components/pages/NotFoundPage' import ChangeRequestsPage from './components/pages/ChangeRequestsPage' From b419979b97cb65e1a2f837fcd0571506a5eeace3 Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 19 May 2026 18:16:48 +0200 Subject: [PATCH 8/9] feat: added plan based banner for upgrade --- frontend/web/components/PlanBasedAccess.tsx | 5 +++++ .../EnvironmentSettingsPage.tsx | 13 +++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/frontend/web/components/PlanBasedAccess.tsx b/frontend/web/components/PlanBasedAccess.tsx index 4e96d4d59401..5faf7b7e8948 100644 --- a/frontend/web/components/PlanBasedAccess.tsx +++ b/frontend/web/components/PlanBasedAccess.tsx @@ -102,6 +102,11 @@ export const featureDescriptions: Record = { 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 = ({ children, ...props }) => { diff --git a/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx b/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx index 6f02e3cf5560..4eedd8c4d3b7 100644 --- a/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx +++ b/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx @@ -46,6 +46,7 @@ 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 }[] = [ @@ -901,15 +902,19 @@ const EnvironmentSettingsPage: React.FC = () => { )} - {Utils.getPlansPermission('WAREHOUSE') && ( - + +
-
- )} + +
{metadataEnable && environmentContentType?.id && ( From 415c1509d28dfc0daece62e15918337b6f0e2850 Mon Sep 17 00:00:00 2001 From: wadii Date: Wed, 20 May 2026 12:22:45 +0200 Subject: [PATCH 9/9] fix: use uuid to delete single connection in endpoint --- frontend/common/services/useWarehouseConnection.ts | 4 ++-- frontend/common/types/requests.ts | 2 +- .../pages/environment-settings/tabs/warehouse-tab/index.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/common/services/useWarehouseConnection.ts b/frontend/common/services/useWarehouseConnection.ts index a7521310df57..78c2f4f22a90 100644 --- a/frontend/common/services/useWarehouseConnection.ts +++ b/frontend/common/services/useWarehouseConnection.ts @@ -22,9 +22,9 @@ export const warehouseConnectionService = service Req['deleteWarehouseConnection'] >({ invalidatesTags: [{ id: 'LIST', type: 'WarehouseConnection' }], - query: ({ environmentId }) => ({ + query: ({ environmentId, uuid }) => ({ method: 'DELETE', - url: `environments/${environmentId}/warehouse-connections/`, + url: `environments/${environmentId}/warehouse-connections/${uuid}/`, }), }), getWarehouseConnections: builder.query< diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index cd303cfd9fda..ed99ef754b81 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -968,6 +968,6 @@ export type Req = { }> getWarehouseConnections: { environmentId: string } createWarehouseConnection: { environmentId: string; warehouse_type: string } - deleteWarehouseConnection: { environmentId: string } + deleteWarehouseConnection: { environmentId: string; uuid: string } // END OF TYPES } diff --git a/frontend/web/components/pages/environment-settings/tabs/warehouse-tab/index.tsx b/frontend/web/components/pages/environment-settings/tabs/warehouse-tab/index.tsx index c71e61a6ecf0..2df1b32ce1da 100644 --- a/frontend/web/components/pages/environment-settings/tabs/warehouse-tab/index.tsx +++ b/frontend/web/components/pages/environment-settings/tabs/warehouse-tab/index.tsx @@ -63,7 +63,7 @@ const WarehouseTab: FC = ({ environmentId }) => { key={connection.uuid} connection={connection} onDelete={() => - deleteConnection({ environmentId }) + deleteConnection({ environmentId, uuid: connection.uuid }) .unwrap() .then(() => toast('Warehouse connection removed')) .catch(() =>