From 1ae05b3472348cfaeb094c87c941ab485e501726 Mon Sep 17 00:00:00 2001 From: wadii Date: Mon, 31 Aug 2026 13:17:29 +0200 Subject: [PATCH 01/13] feat: cohort sync key management in environment settings --- frontend/common/services/useCohort.ts | 32 +++- frontend/common/types/requests.ts | 11 ++ frontend/common/types/responses.ts | 9 + .../EnvironmentSettingsPage.tsx | 16 ++ .../tabs/cohort-sync-tab/CohortSyncTab.tsx | 167 ++++++++++++++++++ .../CreateCohortSyncKeyModal.tsx | 95 ++++++++++ .../tabs/cohort-sync-tab/index.ts | 1 + 7 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx create mode 100644 frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CreateCohortSyncKeyModal.tsx create mode 100644 frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/index.ts diff --git a/frontend/common/services/useCohort.ts b/frontend/common/services/useCohort.ts index 0bc89daca4ad..9739344364d9 100644 --- a/frontend/common/services/useCohort.ts +++ b/frontend/common/services/useCohort.ts @@ -4,7 +4,7 @@ import { service } from 'common/service' import toFormData from 'common/utils/toFormData' export const cohortService = service - .enhanceEndpoints({ addTagTypes: ['Cohort', 'Segment'] }) + .enhanceEndpoints({ addTagTypes: ['Cohort', 'CohortSyncKey', 'Segment'] }) .injectEndpoints({ endpoints: (builder) => ({ createCohort: builder.mutation({ @@ -22,6 +22,17 @@ export const cohortService = service url: `environments/${query.environmentApiKey}/cohorts/`, }), }), + createCohortSyncKey: builder.mutation< + Res['cohortSyncKey'], + Req['createCohortSyncKey'] + >({ + invalidatesTags: [{ id: 'LIST', type: 'CohortSyncKey' }], + query: (query) => ({ + body: { name: query.name }, + method: 'POST', + url: `environments/${query.environmentApiKey}/cohorts/sync-keys/`, + }), + }), deleteCohort: builder.mutation({ invalidatesTags: (q, e, arg) => [ { id: 'LIST', type: 'Cohort' }, @@ -38,6 +49,22 @@ export const cohortService = service url: `environments/${query.environmentApiKey}/cohorts/${query.cohortId}/`, }), }), + getCohortSyncKeys: builder.query< + Res['cohortSyncKeys'], + Req['getCohortSyncKeys'] + >({ + providesTags: [{ id: 'LIST', type: 'CohortSyncKey' }], + query: (query) => ({ + url: `environments/${query.environmentApiKey}/cohorts/sync-keys/`, + }), + }), + revokeCohortSyncKey: builder.mutation({ + invalidatesTags: [{ id: 'LIST', type: 'CohortSyncKey' }], + query: (query) => ({ + method: 'DELETE', + url: `environments/${query.environmentApiKey}/cohorts/sync-keys/${query.prefix}/`, + }), + }), syncCohortCsv: builder.mutation< Res['cohortCsvSync'], Req['syncCohortCsv'] @@ -95,8 +122,11 @@ export async function deleteCohort( export const { useCreateCohortMutation, + useCreateCohortSyncKeyMutation, useDeleteCohortMutation, useGetCohortQuery, + useGetCohortSyncKeysQuery, + useRevokeCohortSyncKeyMutation, useSyncCohortCsvMutation, useUpdateCohortMutation, // END OF EXPORTS diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index 211df3f3738d..48441cafb20b 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -207,6 +207,17 @@ export type Req = { identifier_column?: number has_header?: boolean } + getCohortSyncKeys: { + environmentApiKey: string + } + createCohortSyncKey: { + environmentApiKey: string + name: string + } + revokeCohortSyncKey: { + environmentApiKey: string + prefix: string + } cloneSegment: { projectId: number segmentId: number diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index 1bf767750333..a1658908357a 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -1037,6 +1037,13 @@ export type Cohort = { membership_counts: CohortMembershipCounts } +export type CohortSyncKey = { + prefix: string + name: string + created: string + key: string | null +} + export type CohortCsvSyncResult = { version: number added: number @@ -1379,6 +1386,8 @@ export type Res = { segments: PagedResponse segment: Segment cohort: Cohort + cohortSyncKeys: CohortSyncKey[] + cohortSyncKey: CohortSyncKey cohortCsvSync: CohortCsvSyncResult segmentMembers: SegmentMembersResponse auditLogs: PagedResponse diff --git a/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx b/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx index 1ae87d1c1b87..fd46d1b21233 100644 --- a/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx +++ b/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx @@ -47,6 +47,8 @@ import { useRouteContext } from 'components/providers/RouteContext' import SettingTitle from 'components/SettingTitle' import ChangeRequestsSetting from 'components/ChangeRequestsSetting' import PlanBasedBanner from 'components/PlanBasedAccess' +import { getSegmentSources } from 'components/modals/CreateSegmentSourcesModal' +import CohortSyncTab from './tabs/cohort-sync-tab' import WarehouseTab from './tabs/warehouse-tab' const showDisabledFlagOptions: { label: string; value: boolean | null }[] = [ @@ -119,6 +121,11 @@ const EnvironmentSettingsPage: React.FC = () => { const warehouseEnabled = Utils.getFlagsmithHasFeature( 'experimentation_warehouse_connection', ) + const cohortSyncEnabled = getSegmentSources().some( + (source) => + (source.key === 'amplitude' || source.key === 'mixpanel') && + source.active, + ) const getEnvironment = useCallback(async () => { if (!env) return @@ -920,6 +927,15 @@ const EnvironmentSettingsPage: React.FC = () => { )} + {cohortSyncEnabled && ( + +
+ +
+
+ )} {metadataEnable && environmentContentType?.id && ( diff --git a/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx new file mode 100644 index 000000000000..e49a399df8c6 --- /dev/null +++ b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx @@ -0,0 +1,167 @@ +import React, { FC } from 'react' +import moment from 'moment' +import Constants from 'common/constants' +import { useHasPermission } from 'common/providers/Permission' +import { + useGetCohortSyncKeysQuery, + useRevokeCohortSyncKeyMutation, +} from 'common/services/useCohort' +import { EnvironmentPermission } from 'common/types/permissions.types' +import { CohortSyncKey } from 'common/types/responses' +import Button from 'components/base/forms/Button' +import Flex from 'components/base/grid/Flex' +import FormGroup from 'components/base/grid/FormGroup' +import Panel from 'components/base/grid/Panel' +import Row from 'components/base/grid/Row' +import Icon from 'components/icons/Icon' +import Loader from 'components/Loader' +import PanelSearch from 'components/PanelSearch' +import Tooltip from 'components/Tooltip' +import CreateCohortSyncKeyModal from './CreateCohortSyncKeyModal' + +type CohortSyncTabProps = { + environmentApiKey: string +} + +const CohortSyncTab: FC = ({ environmentApiKey }) => { + const { permission: canManage } = useHasPermission({ + id: environmentApiKey, + level: 'environment', + permission: EnvironmentPermission.MANAGE_SEGMENT_OVERRIDES, + }) + + const { data: syncKeys, isLoading } = useGetCohortSyncKeysQuery( + { environmentApiKey }, + { skip: !environmentApiKey }, + ) + const [revokeCohortSyncKey] = useRevokeCohortSyncKeyMutation() + + const handleCreate = () => { + openModal( + 'Create Cohort Sync Key', + , + 'p-0', + ) + } + + const handleRevoke = (syncKey: CohortSyncKey) => { + openConfirm({ + body: ( +
+ Any provider using {syncKey.name} will stop syncing + cohorts into this environment immediately. This cannot be undone. +
+ ), + destructive: true, + onYes: () => { + revokeCohortSyncKey({ environmentApiKey, prefix: syncKey.prefix }) + .unwrap() + .then(() => toast('Cohort sync key revoked')) + .catch(() => toast('Failed to revoke cohort sync key', 'danger')) + }, + title: 'Revoke Cohort Sync Key', + yesText: 'Revoke', + }) + } + + const filterByName = (syncKey: CohortSyncKey, search: string) => + syncKey.name.toLowerCase().includes(search.toLowerCase()) + + const renderRow = (syncKey: CohortSyncKey) => ( + + +
{syncKey.name}
+
+ Created {moment(syncKey.created).format('D MMM YYYY')} +
+
+
+ + {syncKey.prefix} + +
+
+ +
+
+ ) + + return ( + +
+
Cohort Sync Keys
+

+ Cohort sync keys authenticate cohort synchronisation from providers + such as Mixpanel and Amplitude into this environment.{' '} + +

+

+ Key values are shown only once at creation and cannot be recovered + afterwards. +

+ {canManage ? ( + + ) : ( + + Create Cohort Sync Key + + } + place='right' + > + {Constants.environmentPermissions( + EnvironmentPermission.MANAGE_SEGMENT_OVERRIDES, + )} + + )} +
+ {isLoading && !syncKeys ? ( + + ) : ( + +
+ + You currently have no cohort sync keys for this environment. + +
+ + } + isLoading={isLoading} + /> + )} +
+ ) +} + +CohortSyncTab.displayName = 'CohortSyncTab' + +export default CohortSyncTab diff --git a/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CreateCohortSyncKeyModal.tsx b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CreateCohortSyncKeyModal.tsx new file mode 100644 index 000000000000..2dc855891520 --- /dev/null +++ b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CreateCohortSyncKeyModal.tsx @@ -0,0 +1,95 @@ +import React, { FC, useState } from 'react' +import { useCreateCohortSyncKeyMutation } from 'common/services/useCohort' +import Button from 'components/base/forms/Button' +import InputGroup from 'components/base/forms/InputGroup' +import CopyField from 'components/CopyField' +import ErrorMessage from 'components/ErrorMessage' +import ModalHR from 'components/modals/ModalHR' +import WarningMessage from 'components/WarningMessage' + +const NAME_MAX_LENGTH = 50 + +type CreateCohortSyncKeyModalProps = { + environmentApiKey: string +} + +const CreateCohortSyncKeyModal: FC = ({ + environmentApiKey, +}) => { + const [name, setName] = useState('') + const [createdKey, setCreatedKey] = useState(null) + const [createCohortSyncKey, { error, isLoading }] = + useCreateCohortSyncKeyMutation() + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault() + createCohortSyncKey({ environmentApiKey, name: name.trim() }) + .unwrap() + .then((syncKey) => setCreatedKey(syncKey.key)) + .catch(() => {}) + } + + if (createdKey) { + return ( +
+
+ + +
+ +
+ +
+
+ ) + } + + return ( +
+
+ ) => + setName(event.target.value) + } + isValid={!!name.trim().length} + type='text' + placeholder='e.g. Mixpanel production' + /> + +
+ +
+ + +
+ + ) +} + +export default CreateCohortSyncKeyModal diff --git a/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/index.ts b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/index.ts new file mode 100644 index 000000000000..bf99ba7c855c --- /dev/null +++ b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/index.ts @@ -0,0 +1 @@ +export { default } from './CohortSyncTab' From bd96a2956a9ec36a96543fd48f36f4651f3be135 Mon Sep 17 00:00:00 2001 From: wadii Date: Mon, 31 Aug 2026 14:35:26 +0200 Subject: [PATCH 02/13] feat: connect cohort provider modals for Mixpanel and Amplitude --- .../CohortSyncKeyStep.tsx | 161 ++++++++++++++++++ .../ConnectCohortProviderModal.scss | 11 ++ .../ConnectCohortProviderModal.tsx | 135 +++++++++++++++ .../ConnectCohortProviderStep.tsx | 25 +++ .../ConnectCohortProviderModal/index.ts | 3 + .../ConnectCohortProviderModal/providers.ts | 51 ++++++ .../CreateSegmentSourcesModal.tsx | 6 + .../web/components/pages/SegmentsPage.tsx | 14 ++ .../EnvironmentSettingsPage.tsx | 2 +- .../tabs/cohort-sync-tab/CohortSyncTab.tsx | 32 ++-- .../CreateCohortSyncKeyModal.tsx | 4 +- 11 files changed, 427 insertions(+), 17 deletions(-) create mode 100644 frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx create mode 100644 frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderModal.scss create mode 100644 frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderModal.tsx create mode 100644 frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderStep.tsx create mode 100644 frontend/web/components/modals/ConnectCohortProviderModal/index.ts create mode 100644 frontend/web/components/modals/ConnectCohortProviderModal/providers.ts diff --git a/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx b/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx new file mode 100644 index 000000000000..8c96fdcee79d --- /dev/null +++ b/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx @@ -0,0 +1,161 @@ +import React, { FC, useState } from 'react' +import moment from 'moment' +import { Link } from 'react-router-dom' +import { + useCreateCohortSyncKeyMutation, + useGetCohortSyncKeysQuery, +} from 'common/services/useCohort' +import { CohortSyncKey } from 'common/types/responses' +import Button from 'components/base/forms/Button' +import InputGroup from 'components/base/forms/InputGroup' +import CopyField from 'components/CopyField' +import ErrorMessage from 'components/ErrorMessage' +import Loader from 'components/Loader' +import WarningMessage from 'components/WarningMessage' +import ConnectCohortProviderStep from './ConnectCohortProviderStep' + +const NAME_MAX_LENGTH = 50 + +type CohortSyncKeyStepProps = { + environmentApiKey: string + index: number + projectId: number | string + providerLabel: string +} + +const CohortSyncKeyStep: FC = ({ + environmentApiKey, + index, + projectId, + providerLabel, +}) => { + const [name, setName] = useState('') + const [createdKey, setCreatedKey] = useState(null) + const [isCreateFormOpen, setIsCreateFormOpen] = useState(false) + + const { data: syncKeys, isLoading } = useGetCohortSyncKeysQuery( + { environmentApiKey }, + { skip: !environmentApiKey }, + ) + const [createCohortSyncKey, { error, isLoading: isCreating }] = + useCreateCohortSyncKeyMutation() + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault() + createCohortSyncKey({ environmentApiKey, name: name.trim() }) + .unwrap() + .then((syncKey) => { + setCreatedKey(syncKey.key) + setIsCreateFormOpen(false) + setName('') + }) + .catch(() => {}) + } + + const hasKeys = !!syncKeys?.length + const getTitle = () => { + if (createdKey) return 'Synchronisation key' + if (hasKeys && !isCreateFormOpen) return 'Use your existing key' + return 'Create a synchronisation key' + } + + const renderBody = () => { + if (isLoading && !syncKeys) { + return + } + + if (createdKey) { + return ( + <> + + + + ) + } + + if (!hasKeys || isCreateFormOpen) { + return ( +
+ ) => + setName(event.target.value) + } + isValid={!!name.trim().length} + type='text' + placeholder={`e.g. ${providerLabel} production`} + /> + + + + ) + } + + return ( + <> +
+ {syncKeys?.map((syncKey: CohortSyncKey) => ( +
+ {syncKey.name} + + {syncKey.prefix} + {moment(syncKey.created).format('D MMM YYYY')} + +
+ ))} +
+
+ Key values are shown only at creation. Lost it? Create a new key. +
+
+ You can revoke keys in{' '} + closeModal()} + > + Environment Settings + + . +
+ + + ) + } + + return ( + + {renderBody()} + + ) +} + +export default CohortSyncKeyStep diff --git a/frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderModal.scss b/frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderModal.scss new file mode 100644 index 000000000000..2f3f3d24fd4d --- /dev/null +++ b/frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderModal.scss @@ -0,0 +1,11 @@ +.connect-cohort-provider { + &__step-number { + width: 24px; + height: 24px; + } + + &__auth-label { + width: 120px; + flex-shrink: 0; + } +} diff --git a/frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderModal.tsx b/frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderModal.tsx new file mode 100644 index 000000000000..f4b961ff5778 --- /dev/null +++ b/frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderModal.tsx @@ -0,0 +1,135 @@ +import React, { FC, useMemo, useState } from 'react' +import { useGetEnvironmentsQuery } from 'common/services/useEnvironment' +import Button from 'components/base/forms/Button' +import FieldLabel from 'components/base/forms/FieldLabel' +import CopyField from 'components/CopyField' +import EnvironmentSelect from 'components/EnvironmentSelect' +import Loader from 'components/Loader' +import ModalHR from 'components/modals/ModalHR' +import CohortSyncKeyStep from './CohortSyncKeyStep' +import ConnectCohortProviderStep from './ConnectCohortProviderStep' +import { + COHORT_PROVIDERS, + CohortProviderKey, + getCohortProviderEndpoint, +} from './providers' +import './ConnectCohortProviderModal.scss' + +const SEGMENTS_DOCS_URL = 'https://docs.flagsmith.com/basic-features/segments' + +type ConnectCohortProviderModalProps = { + projectId: number | string + provider: CohortProviderKey +} + +const ConnectCohortProviderModal: FC = ({ + projectId, + provider, +}) => { + const config = COHORT_PROVIDERS[provider] + const [selectedEnvironment, setSelectedEnvironment] = useState('') + + const { data: environments, isLoading } = useGetEnvironmentsQuery({ + projectId: Number(projectId), + }) + + // EnvironmentSelect orders alphabetically, so the default matches its first option. + const defaultEnvironment = useMemo( + () => + [...(environments?.results || [])].sort((a, b) => + a.name.localeCompare(b.name), + )[0]?.api_key, + [environments?.results], + ) + const environmentApiKey = selectedEnvironment || defaultEnvironment || '' + + return ( +
+
+ {isLoading && !environments ? ( + + ) : ( +
+
+ + Environment + + setSelectedEnvironment(`${value}`)} + /> +
+ Cohort members are synchronised into this environment only. +
+
+ + + +
+ {config.authRows.map((row) => ( +
+
+ {row.label} +
+ {row.mono ? ( + + {row.value} + + ) : ( + {row.value} + )} +
+ ))} +
+
+ This URL is the same for every environment — your key decides + where cohort members land. +
+
+ +

+ {config.exportStepBody} +

+
+
+ )} +
+ +
+ + +
+
+ ) +} + +export default ConnectCohortProviderModal diff --git a/frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderStep.tsx b/frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderStep.tsx new file mode 100644 index 000000000000..3022baaad21a --- /dev/null +++ b/frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderStep.tsx @@ -0,0 +1,25 @@ +import React, { FC, ReactNode } from 'react' + +type ConnectCohortProviderStepProps = { + index: number + title: string + children: ReactNode +} + +const ConnectCohortProviderStep: FC = ({ + children, + index, + title, +}) => ( +
+ + {index} + +
+
{title}
+ {children} +
+
+) + +export default ConnectCohortProviderStep diff --git a/frontend/web/components/modals/ConnectCohortProviderModal/index.ts b/frontend/web/components/modals/ConnectCohortProviderModal/index.ts new file mode 100644 index 000000000000..89564e0ea6ab --- /dev/null +++ b/frontend/web/components/modals/ConnectCohortProviderModal/index.ts @@ -0,0 +1,3 @@ +export { default } from './ConnectCohortProviderModal' +export { COHORT_PROVIDERS } from './providers' +export type { CohortProviderConfig, CohortProviderKey } from './providers' diff --git a/frontend/web/components/modals/ConnectCohortProviderModal/providers.ts b/frontend/web/components/modals/ConnectCohortProviderModal/providers.ts new file mode 100644 index 000000000000..3583d9fe0d48 --- /dev/null +++ b/frontend/web/components/modals/ConnectCohortProviderModal/providers.ts @@ -0,0 +1,51 @@ +import Project from 'common/project' + +export type CohortProviderKey = 'amplitude' | 'mixpanel' + +export type CohortProviderAuthRow = { + label: string + value: string + mono?: boolean +} + +export type CohortProviderConfig = { + label: string + authRows: CohortProviderAuthRow[] + endpointPath: string + endpointStepTitle: string + exportStepTitle: string + exportStepBody: string +} + +export const COHORT_PROVIDERS: Record = + { + amplitude: { + authRows: [ + { label: 'Authentication', value: 'Bearer token' }, + { label: 'Token', mono: true, value: '{YOUR_SYNCHRONISATION_KEY}' }, + ], + endpointPath: 'cohort-sync/amplitude', + endpointStepTitle: 'Add Flagsmith as a destination in Amplitude', + exportStepBody: + 'In Amplitude, open the cohort you want to target and synchronise it to the Flagsmith destination. Flagsmith creates the managed segment automatically on the first synchronisation, then keeps its members up to date as people enter and leave the cohort.', + exportStepTitle: 'Synchronise your cohort to Flagsmith', + label: 'Amplitude', + }, + mixpanel: { + authRows: [ + { label: 'Authentication', value: 'Basic auth' }, + { label: 'Username', value: 'Any value' }, + { label: 'Password', mono: true, value: '{YOUR_SYNCHRONISATION_KEY}' }, + ], + endpointPath: 'cohort-sync/mixpanel/webhook/', + endpointStepTitle: 'Create a webhook in Mixpanel', + exportStepBody: + 'In Mixpanel, open the cohort you want to target and export it to the webhook you just created. Flagsmith creates the managed segment automatically on the first synchronisation, then keeps its members up to date as people enter and leave the cohort.', + exportStepTitle: 'Export your cohort to the webhook', + label: 'Mixpanel', + }, + } + +export const getCohortProviderEndpoint = ( + provider: CohortProviderKey, +): string => `${Project.api}${COHORT_PROVIDERS[provider].endpointPath}` diff --git a/frontend/web/components/modals/CreateSegmentSourcesModal/CreateSegmentSourcesModal.tsx b/frontend/web/components/modals/CreateSegmentSourcesModal/CreateSegmentSourcesModal.tsx index 90570e0f36c5..1fcbe57d4ab3 100644 --- a/frontend/web/components/modals/CreateSegmentSourcesModal/CreateSegmentSourcesModal.tsx +++ b/frontend/web/components/modals/CreateSegmentSourcesModal/CreateSegmentSourcesModal.tsx @@ -76,20 +76,26 @@ export function getSegmentSources(): SegmentSource[] { type CreateSegmentSourcesModalType = { onManual: () => void + onAmplitude?: () => void onCsv?: () => void + onMixpanel?: () => void sources: SegmentSource[] } const CreateSegmentSourcesModal: FC = ({ + onAmplitude, onCsv, onManual, + onMixpanel, sources, }) => { const [selected, setSelected] = useState(null) const [requested, setRequested] = useState([]) const sourceHandlers: Partial void>> = { + amplitude: onAmplitude, csv: onCsv, + mixpanel: onMixpanel, } const trackSourceEvent = (event: string, source: SegmentSource) => { diff --git a/frontend/web/components/pages/SegmentsPage.tsx b/frontend/web/components/pages/SegmentsPage.tsx index 4c9377562043..2fe170b7b848 100644 --- a/frontend/web/components/pages/SegmentsPage.tsx +++ b/frontend/web/components/pages/SegmentsPage.tsx @@ -18,6 +18,10 @@ import CreateSegmentFromCsv from 'components/modals/CreateSegmentFromCsv' import CreateSegmentSourcesModal, { getSegmentSources, } from 'components/modals/CreateSegmentSourcesModal' +import ConnectCohortProviderModal, { + COHORT_PROVIDERS, + CohortProviderKey, +} from 'components/modals/ConnectCohortProviderModal' import PanelSearch from 'components/PanelSearch' import JSONReference from 'components/JSONReference' @@ -117,6 +121,14 @@ const SegmentsPage: FC = () => { ) } + const openConnectCohortProviderDrawer = (provider: CohortProviderKey) => { + openModal( + `Connect ${COHORT_PROVIDERS[provider].label}`, + , + 'p-0 modal--wide', + ) + } + const newSegment = () => { const sources = getSegmentSources() if ( @@ -129,6 +141,8 @@ const SegmentsPage: FC = () => { sources={sources} onManual={openCreateSegmentDrawer} onCsv={openCreateSegmentFromCsvDrawer} + onMixpanel={() => openConnectCohortProviderDrawer('mixpanel')} + onAmplitude={() => openConnectCohortProviderDrawer('amplitude')} />, 'p-0 modal--wide', ) diff --git a/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx b/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx index fd46d1b21233..afb10d8d658e 100644 --- a/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx +++ b/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx @@ -928,7 +928,7 @@ const EnvironmentSettingsPage: React.FC = () => {
)} {cohortSyncEnabled && ( - +
= ({ environmentApiKey }) => { const handleCreate = () => { openModal( - 'Create Cohort Sync Key', + 'Create Cohort Synchronisation Key', , 'p-0', ) @@ -48,18 +48,21 @@ const CohortSyncTab: FC = ({ environmentApiKey }) => { openConfirm({ body: (
- Any provider using {syncKey.name} will stop syncing - cohorts into this environment immediately. This cannot be undone. + Any provider using {syncKey.name} will stop + synchronising cohorts into this environment immediately. This cannot + be undone.
), destructive: true, onYes: () => { revokeCohortSyncKey({ environmentApiKey, prefix: syncKey.prefix }) .unwrap() - .then(() => toast('Cohort sync key revoked')) - .catch(() => toast('Failed to revoke cohort sync key', 'danger')) + .then(() => toast('Cohort synchronisation key revoked')) + .catch(() => + toast('Failed to revoke cohort synchronisation key', 'danger'), + ) }, - title: 'Revoke Cohort Sync Key', + title: 'Revoke Cohort Synchronisation Key', yesText: 'Revoke', }) } @@ -96,10 +99,10 @@ const CohortSyncTab: FC = ({ environmentApiKey }) => { return (
-
Cohort Sync Keys
+
Cohort Synchronisation Keys

- Cohort sync keys authenticate cohort synchronisation from providers - such as Mixpanel and Amplitude into this environment.{' '} + Cohort synchronisation keys authenticate cohort synchronisation from + providers such as Mixpanel and Amplitude into this environment.{' '} ) : ( - Create Cohort Sync Key + Create Cohort Synchronisation Key } place='right' @@ -141,16 +144,17 @@ const CohortSyncTab: FC = ({ environmentApiKey }) => { ) : ( +

- You currently have no cohort sync keys for this environment. + You currently have no cohort synchronisation keys for this + environment.
diff --git a/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CreateCohortSyncKeyModal.tsx b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CreateCohortSyncKeyModal.tsx index 2dc855891520..9988a37fea3d 100644 --- a/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CreateCohortSyncKeyModal.tsx +++ b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CreateCohortSyncKeyModal.tsx @@ -34,14 +34,14 @@ const CreateCohortSyncKeyModal: FC = ({
From 1f8d0a8ac5c4a77ea93274e0ad7c66b31e352c28 Mon Sep 17 00:00:00 2001 From: wadii Date: Mon, 31 Aug 2026 14:50:35 +0200 Subject: [PATCH 03/13] test: cohort synchronisation keys e2e coverage --- .../e2e/tests/cohort-sync-keys-test.pw.ts | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 frontend/e2e/tests/cohort-sync-keys-test.pw.ts diff --git a/frontend/e2e/tests/cohort-sync-keys-test.pw.ts b/frontend/e2e/tests/cohort-sync-keys-test.pw.ts new file mode 100644 index 000000000000..38ff73b512f6 --- /dev/null +++ b/frontend/e2e/tests/cohort-sync-keys-test.pw.ts @@ -0,0 +1,185 @@ +import { test, expect } from '../test-setup' +import { + byId, + log, + createHelpers, + getFlagsmith, + LONG_TIMEOUT, +} from '../helpers' +import { E2E_USER, PASSWORD, E2E_TEST_PROJECT } from '../config' + +const COHORT_PROVIDERS = ['amplitude', 'mixpanel'] as const + +type CohortProvider = (typeof COHORT_PROVIDERS)[number] + +type SegmentSourceFlagEntry = { + active?: boolean + name?: string + visible?: boolean +} + +// Mirrors `getSegmentSources` in CreateSegmentSourcesModal: only an entry that +// is visible AND active opens the connect modal (and shows the environment tab). +const getActiveCohortProviders = (config: unknown): CohortProvider[] => { + if (!Array.isArray(config)) { + return [] + } + return (config as SegmentSourceFlagEntry[]) + .filter( + (entry) => + COHORT_PROVIDERS.includes(entry?.name as CohortProvider) && + entry?.visible !== false && + entry?.active === true, + ) + .map((entry) => entry.name as CohortProvider) +} + +test.describe('Cohort Synchronisation Keys Tests', () => { + test('Cohort synchronisation keys can be created from a provider connection and revoked in Environment Settings @oss', async ({ + page, + }) => { + const { + click, + gotoProject, + gotoSegments, + login, + setText, + waitForElementVisible, + waitForModalToClose, + } = createHelpers(page) + + const flagsmith = await getFlagsmith() + const activeProviders = flagsmith.hasFeature( + 'create_segment_with_external_sources', + ) + ? getActiveCohortProviders( + flagsmith.getValue('create_segment_with_external_sources', { + fallback: null, + json: true, + }), + ) + : [] + test.skip( + activeProviders.length === 0, + 'No active Amplitude or Mixpanel entry in `create_segment_with_external_sources`, so the cohort synchronisation UI is unreachable', + ) + + // With both providers active each key goes through a different provider; + // with one, the second key goes through its "Create a new key" state. + const firstProvider = activeProviders[0] + const secondProvider = activeProviders[1] ?? activeProviders[0] + + const runId = Date.now() + const keyOne = `e2e key one ${runId}` + const keyTwo = `e2e key two ${runId}` + const keyThree = `e2e key three ${runId}` + + const connectModal = page.locator('.connect-cohort-provider') + const envSelect = connectModal.locator(byId('connect-provider-env-select')) + + const openConnectModal = async (provider: CohortProvider) => { + await click(byId('show-create-segment-btn')) + await click(byId(`segment-source-${provider}`)) + await waitForElementVisible(byId('connect-provider-done')) + await expect(envSelect).toBeVisible({ timeout: LONG_TIMEOUT }) + // The modal defaults to the alphabetically first environment; assert it + // resolved so both keys are created against the same environment. + const label = (await envSelect.innerText()).trim() + expect(label).not.toBe('') + expect(label).not.toBe('Select an Environment') + return label + } + + const createKeyInConnectModal = async (name: string) => { + // Step 1 renders either the create form or the existing-keys state, + // depending on whether this environment already has keys. + await connectModal + .locator( + `${byId('connect-provider-key-name')}, ${byId( + 'connect-provider-new-key', + )}`, + ) + .first() + .waitFor({ state: 'visible', timeout: LONG_TIMEOUT }) + const newKeyButton = connectModal.locator( + byId('connect-provider-new-key'), + ) + if (await newKeyButton.isVisible()) { + await click(byId('connect-provider-new-key')) + } + await setText(byId('connect-provider-key-name'), name) + await click(byId('connect-provider-create-key')) + await expect( + connectModal.locator(byId('connect-provider-key-value')), + ).toHaveValue(/.+/, { timeout: LONG_TIMEOUT }) + await click(byId('connect-provider-done')) + await waitForModalToClose() + } + + log('Login') + await login(E2E_USER, PASSWORD) + await gotoProject(E2E_TEST_PROJECT) + await gotoSegments() + + log(`Create the first key while connecting ${firstProvider}`) + const environmentLabel = await openConnectModal(firstProvider) + await createKeyInConnectModal(keyOne) + + log(`Create the second key while connecting ${secondProvider}`) + expect(await openConnectModal(secondProvider)).toBe(environmentLabel) + await waitForElementVisible(byId('connect-provider-new-key')) + await expect(connectModal).toContainText(keyOne) + // This link carries the environment the modal is working against, so + // following it guarantees we inspect the keys we just created. + const settingsLink = connectModal.locator( + 'a[href*="tab=cohort-synchronisation"]', + ) + await expect(settingsLink).toBeVisible() + const settingsHref = (await settingsLink.getAttribute('href')) ?? '' + expect(settingsHref).not.toBe('') + await createKeyInConnectModal(keyTwo) + + log('Open the Cohort Synchronisation tab in Environment Settings') + await page.goto(settingsHref) + await waitForElementVisible('#cohort-sync-keys-list') + const keysList = page.locator('#cohort-sync-keys-list') + await expect(keysList).toContainText(keyOne) + await expect(keysList).toContainText(keyTwo) + + log('Create a third key from Environment Settings') + await click(byId('create-cohort-sync-key')) + await setText(byId('cohort-sync-key-name'), keyThree) + await click(byId('cohort-sync-key-create')) + await expect(page.locator(byId('cohort-sync-key-value'))).toHaveValue( + /.+/, + { timeout: LONG_TIMEOUT }, + ) + await click(byId('cohort-sync-key-done')) + await waitForModalToClose() + await expect(keysList).toContainText(keyThree) + + log('Revoke every key for this environment') + const revokeButtons = keysList.locator('[aria-label^="Revoke "]') + for ( + let remaining = await revokeButtons.count(); + remaining > 0; + remaining-- + ) { + await revokeButtons.first().click() + await click('#confirm-btn-yes') + await expect(page.locator('#confirm-btn-yes')).toHaveCount(0, { + timeout: LONG_TIMEOUT, + }) + await expect(revokeButtons).toHaveCount(remaining - 1, { + timeout: LONG_TIMEOUT, + }) + } + + log('Verify no keys remain') + // PanelSearch drops the list entirely and renders its empty state instead. + await expect(page.locator('#cohort-sync-keys-list')).toHaveCount(0) + for (const name of [keyOne, keyTwo, keyThree]) { + await expect(page.getByText(name, { exact: true })).toHaveCount(0) + } + }) +}) From 5dba9df59eaab9b1ca580794d034138a90d5557d Mon Sep 17 00:00:00 2001 From: wadii Date: Mon, 31 Aug 2026 15:00:41 +0200 Subject: [PATCH 04/13] fix: address cohort sync keys review feedback --- frontend/common/services/useCohort.ts | 2 +- frontend/common/types/responses.ts | 5 +- .../CohortSyncKeyStep.tsx | 58 +++++++++++++++---- .../ConnectCohortProviderModal.tsx | 2 +- .../ConnectCohortProviderModal/providers.ts | 7 ++- .../tabs/cohort-sync-tab/CohortSyncTab.tsx | 19 +++--- 6 files changed, 69 insertions(+), 24 deletions(-) diff --git a/frontend/common/services/useCohort.ts b/frontend/common/services/useCohort.ts index 9739344364d9..5b96e8235bfc 100644 --- a/frontend/common/services/useCohort.ts +++ b/frontend/common/services/useCohort.ts @@ -23,7 +23,7 @@ export const cohortService = service }), }), createCohortSyncKey: builder.mutation< - Res['cohortSyncKey'], + Res['cohortSyncKeyCreated'], Req['createCohortSyncKey'] >({ invalidatesTags: [{ id: 'LIST', type: 'CohortSyncKey' }], diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index a1658908357a..7b3dd1a35187 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -1044,6 +1044,9 @@ export type CohortSyncKey = { key: string | null } +// The plaintext key only exists in the create response. +export type CohortSyncKeyCreated = CohortSyncKey & { key: string } + export type CohortCsvSyncResult = { version: number added: number @@ -1387,7 +1390,7 @@ export type Res = { segment: Segment cohort: Cohort cohortSyncKeys: CohortSyncKey[] - cohortSyncKey: CohortSyncKey + cohortSyncKeyCreated: CohortSyncKeyCreated cohortCsvSync: CohortCsvSyncResult segmentMembers: SegmentMembersResponse auditLogs: PagedResponse diff --git a/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx b/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx index 8c96fdcee79d..8cb6bdb6a5df 100644 --- a/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx +++ b/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx @@ -1,16 +1,19 @@ import React, { FC, useState } from 'react' import moment from 'moment' import { Link } from 'react-router-dom' +import Constants from 'common/constants' +import { useHasPermission } from 'common/providers/Permission' import { useCreateCohortSyncKeyMutation, useGetCohortSyncKeysQuery, } from 'common/services/useCohort' +import { EnvironmentPermission } from 'common/types/permissions.types' import { CohortSyncKey } from 'common/types/responses' import Button from 'components/base/forms/Button' import InputGroup from 'components/base/forms/InputGroup' import CopyField from 'components/CopyField' import ErrorMessage from 'components/ErrorMessage' -import Loader from 'components/Loader' +import Tooltip from 'components/Tooltip' import WarningMessage from 'components/WarningMessage' import ConnectCohortProviderStep from './ConnectCohortProviderStep' @@ -33,6 +36,13 @@ const CohortSyncKeyStep: FC = ({ const [createdKey, setCreatedKey] = useState(null) const [isCreateFormOpen, setIsCreateFormOpen] = useState(false) + const { isLoading: isLoadingPermission, permission: canManage } = + useHasPermission({ + id: environmentApiKey, + level: 'environment', + permission: EnvironmentPermission.MANAGE_SEGMENT_OVERRIDES, + }) + const { data: syncKeys, isLoading } = useGetCohortSyncKeysQuery( { environmentApiKey }, { skip: !environmentApiKey }, @@ -56,11 +66,12 @@ const CohortSyncKeyStep: FC = ({ const getTitle = () => { if (createdKey) return 'Synchronisation key' if (hasKeys && !isCreateFormOpen) return 'Use your existing key' + if (!canManage) return 'Synchronisation key' return 'Create a synchronisation key' } const renderBody = () => { - if (isLoading && !syncKeys) { + if ((isLoading && !syncKeys) || isLoadingPermission) { return } @@ -80,6 +91,15 @@ const CohortSyncKeyStep: FC = ({ ) } + if (!hasKeys && !canManage) { + return ( +
+ You do not have permission to create synchronisation keys in this + environment. +
+ ) + } + if (!hasKeys || isCreateFormOpen) { return (
@@ -127,7 +147,8 @@ const CohortSyncKeyStep: FC = ({ ))}
- Key values are shown only at creation. Lost it? Create a new key. + Key values are shown only at creation. + {canManage && ' Lost it? Create a new key.'}
You can revoke keys in{' '} @@ -139,14 +160,29 @@ const CohortSyncKeyStep: FC = ({ .
- + {canManage ? ( + + ) : ( + + Create a new key + + } + place='right' + > + {Constants.environmentPermissions( + EnvironmentPermission.MANAGE_SEGMENT_OVERRIDES, + )} + + )} ) } diff --git a/frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderModal.tsx b/frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderModal.tsx index f4b961ff5778..2b9d8f06fa65 100644 --- a/frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderModal.tsx +++ b/frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderModal.tsx @@ -4,7 +4,6 @@ import Button from 'components/base/forms/Button' import FieldLabel from 'components/base/forms/FieldLabel' import CopyField from 'components/CopyField' import EnvironmentSelect from 'components/EnvironmentSelect' -import Loader from 'components/Loader' import ModalHR from 'components/modals/ModalHR' import CohortSyncKeyStep from './CohortSyncKeyStep' import ConnectCohortProviderStep from './ConnectCohortProviderStep' @@ -78,6 +77,7 @@ const ConnectCohortProviderModal: FC = ({ title={config.endpointStepTitle} > = { label: 'Authentication', value: 'Bearer token' }, { label: 'Token', mono: true, value: '{YOUR_SYNCHRONISATION_KEY}' }, ], - endpointPath: 'cohort-sync/amplitude', + // Amplitude posts cohort list creation here, then adds and removes + // members under `lists/{list_id}/add` and `lists/{list_id}/remove`. + endpointFieldTitle: 'List endpoint URL', + endpointPath: 'cohort-sync/amplitude/lists/', endpointStepTitle: 'Add Flagsmith as a destination in Amplitude', exportStepBody: 'In Amplitude, open the cohort you want to target and synchronise it to the Flagsmith destination. Flagsmith creates the managed segment automatically on the first synchronisation, then keeps its members up to date as people enter and leave the cohort.', @@ -37,6 +41,7 @@ export const COHORT_PROVIDERS: Record = { label: 'Username', value: 'Any value' }, { label: 'Password', mono: true, value: '{YOUR_SYNCHRONISATION_KEY}' }, ], + endpointFieldTitle: 'Webhook URL', endpointPath: 'cohort-sync/mixpanel/webhook/', endpointStepTitle: 'Create a webhook in Mixpanel', exportStepBody: diff --git a/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx index 99fb867f4096..9b095297bf63 100644 --- a/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx +++ b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx @@ -14,7 +14,6 @@ import FormGroup from 'components/base/grid/FormGroup' import Panel from 'components/base/grid/Panel' import Row from 'components/base/grid/Row' import Icon from 'components/icons/Icon' -import Loader from 'components/Loader' import PanelSearch from 'components/PanelSearch' import Tooltip from 'components/Tooltip' import CreateCohortSyncKeyModal from './CreateCohortSyncKeyModal' @@ -84,14 +83,16 @@ const CohortSyncTab: FC = ({ environmentApiKey }) => {
- + {canManage && ( + + )}
) From 29fbf47923bf293bd18c9509de42a81be0f94c99 Mon Sep 17 00:00:00 2001 From: wadii Date: Mon, 31 Aug 2026 15:21:28 +0200 Subject: [PATCH 05/13] fix: modal router crash, dual permission gate and absolute provider URLs --- .../CohortSyncKeyStep.tsx | 27 ++++++++++++++----- .../ConnectCohortProviderModal/providers.ts | 8 +++++- .../EnvironmentSettingsPage.tsx | 1 + .../tabs/cohort-sync-tab/CohortSyncTab.tsx | 20 +++++++++++--- 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx b/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx index 8cb6bdb6a5df..a4e8f151f131 100644 --- a/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx +++ b/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx @@ -1,13 +1,15 @@ import React, { FC, useState } from 'react' import moment from 'moment' -import { Link } from 'react-router-dom' import Constants from 'common/constants' import { useHasPermission } from 'common/providers/Permission' import { useCreateCohortSyncKeyMutation, useGetCohortSyncKeysQuery, } from 'common/services/useCohort' -import { EnvironmentPermission } from 'common/types/permissions.types' +import { + EnvironmentPermission, + ProjectPermission, +} from 'common/types/permissions.types' import { CohortSyncKey } from 'common/types/responses' import Button from 'components/base/forms/Button' import InputGroup from 'components/base/forms/InputGroup' @@ -36,12 +38,24 @@ const CohortSyncKeyStep: FC = ({ const [createdKey, setCreatedKey] = useState(null) const [isCreateFormOpen, setIsCreateFormOpen] = useState(false) - const { isLoading: isLoadingPermission, permission: canManage } = + // Key writes need both permissions; mirrors the API's CohortPermission. + const { isLoading: isLoadingEnvPermission, permission: canManageOverrides } = useHasPermission({ id: environmentApiKey, level: 'environment', permission: EnvironmentPermission.MANAGE_SEGMENT_OVERRIDES, }) + const { + isLoading: isLoadingProjectPermission, + permission: canManageSegments, + } = useHasPermission({ + id: `${projectId}`, + level: 'project', + permission: ProjectPermission.MANAGE_SEGMENTS, + }) + const isLoadingPermission = + isLoadingEnvPermission || isLoadingProjectPermission + const canManage = !!canManageOverrides && !!canManageSegments const { data: syncKeys, isLoading } = useGetCohortSyncKeysQuery( { environmentApiKey }, @@ -152,12 +166,13 @@ const CohortSyncKeyStep: FC = ({
You can revoke keys in{' '} - closeModal()} > Environment Settings - + .
{canManage ? ( diff --git a/frontend/web/components/modals/ConnectCohortProviderModal/providers.ts b/frontend/web/components/modals/ConnectCohortProviderModal/providers.ts index fa5c212cc475..cf27c095414e 100644 --- a/frontend/web/components/modals/ConnectCohortProviderModal/providers.ts +++ b/frontend/web/components/modals/ConnectCohortProviderModal/providers.ts @@ -51,6 +51,12 @@ export const COHORT_PROVIDERS: Record = }, } +// Proxied self-hosted deployments configure a relative Project.api ('/api/v1/'); +// providers need an absolute callback URL, so resolve against the page origin. export const getCohortProviderEndpoint = ( provider: CohortProviderKey, -): string => `${Project.api}${COHORT_PROVIDERS[provider].endpointPath}` +): string => + new URL( + COHORT_PROVIDERS[provider].endpointPath, + new URL(Project.api, window.location.origin), + ).toString() diff --git a/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx b/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx index afb10d8d658e..001ea493535a 100644 --- a/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx +++ b/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx @@ -932,6 +932,7 @@ const EnvironmentSettingsPage: React.FC = () => {
diff --git a/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx index 9b095297bf63..ce2593199054 100644 --- a/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx +++ b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx @@ -6,7 +6,10 @@ import { useGetCohortSyncKeysQuery, useRevokeCohortSyncKeyMutation, } from 'common/services/useCohort' -import { EnvironmentPermission } from 'common/types/permissions.types' +import { + EnvironmentPermission, + ProjectPermission, +} from 'common/types/permissions.types' import { CohortSyncKey } from 'common/types/responses' import Button from 'components/base/forms/Button' import Flex from 'components/base/grid/Flex' @@ -20,14 +23,25 @@ import CreateCohortSyncKeyModal from './CreateCohortSyncKeyModal' type CohortSyncTabProps = { environmentApiKey: string + projectId: number | string } -const CohortSyncTab: FC = ({ environmentApiKey }) => { - const { permission: canManage } = useHasPermission({ +const CohortSyncTab: FC = ({ + environmentApiKey, + projectId, +}) => { + // Key writes need both permissions; mirrors the API's CohortPermission. + const { permission: canManageOverrides } = useHasPermission({ id: environmentApiKey, level: 'environment', permission: EnvironmentPermission.MANAGE_SEGMENT_OVERRIDES, }) + const { permission: canManageSegments } = useHasPermission({ + id: `${projectId}`, + level: 'project', + permission: ProjectPermission.MANAGE_SEGMENTS, + }) + const canManage = !!canManageOverrides && !!canManageSegments const { data: syncKeys, isLoading } = useGetCohortSyncKeysQuery( { environmentApiKey }, From 7f717651424ba32bd44960abdbcd5dccd45cfdf6 Mon Sep 17 00:00:00 2001 From: wadii Date: Mon, 31 Aug 2026 15:31:18 +0200 Subject: [PATCH 06/13] fix: amplitude connect step matches destination workflow without URL --- .../ConnectCohortProviderModal.tsx | 30 ++++++++++------ .../ConnectCohortProviderModal/providers.ts | 35 ++++++++++++------- 2 files changed, 42 insertions(+), 23 deletions(-) diff --git a/frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderModal.tsx b/frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderModal.tsx index 2b9d8f06fa65..994042752f42 100644 --- a/frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderModal.tsx +++ b/frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderModal.tsx @@ -26,6 +26,7 @@ const ConnectCohortProviderModal: FC = ({ provider, }) => { const config = COHORT_PROVIDERS[provider] + const endpointUrl = getCohortProviderEndpoint(provider) const [selectedEnvironment, setSelectedEnvironment] = useState('') const { data: environments, isLoading } = useGetEnvironmentsQuery({ @@ -76,12 +77,19 @@ const ConnectCohortProviderModal: FC = ({ index={2} title={config.endpointStepTitle} > - + {!!config.endpointStepBody && ( +

+ {config.endpointStepBody} +

+ )} + {!!endpointUrl && ( + + )}
{config.authRows.map((row) => (
= ({
))}
-
- This URL is the same for every environment — your key decides - where cohort members land. -
+ {!!endpointUrl && ( +
+ This URL is the same for every environment — your key decides + where cohort members land. +
+ )}

diff --git a/frontend/web/components/modals/ConnectCohortProviderModal/providers.ts b/frontend/web/components/modals/ConnectCohortProviderModal/providers.ts index cf27c095414e..d7df10ffcdcc 100644 --- a/frontend/web/components/modals/ConnectCohortProviderModal/providers.ts +++ b/frontend/web/components/modals/ConnectCohortProviderModal/providers.ts @@ -11,9 +11,14 @@ export type CohortProviderAuthRow = { export type CohortProviderConfig = { label: string authRows: CohortProviderAuthRow[] - endpointFieldTitle: string - endpointPath: string + // Providers where the user pastes a URL themselves carry an endpoint; + // Amplitude calls Flagsmith's portal-registered endpoints instead. + endpoint?: { + fieldTitle: string + path: string + } endpointStepTitle: string + endpointStepBody?: string exportStepTitle: string exportStepBody: string } @@ -22,13 +27,10 @@ export const COHORT_PROVIDERS: Record = { amplitude: { authRows: [ - { label: 'Authentication', value: 'Bearer token' }, - { label: 'Token', mono: true, value: '{YOUR_SYNCHRONISATION_KEY}' }, + { label: 'API key', mono: true, value: '{YOUR_SYNCHRONISATION_KEY}' }, ], - // Amplitude posts cohort list creation here, then adds and removes - // members under `lists/{list_id}/add` and `lists/{list_id}/remove`. - endpointFieldTitle: 'List endpoint URL', - endpointPath: 'cohort-sync/amplitude/lists/', + endpointStepBody: + 'In Amplitude, open Data → Destinations and add Flagsmith as a cohort destination. Paste your synchronisation key when asked for the API key.', endpointStepTitle: 'Add Flagsmith as a destination in Amplitude', exportStepBody: 'In Amplitude, open the cohort you want to target and synchronise it to the Flagsmith destination. Flagsmith creates the managed segment automatically on the first synchronisation, then keeps its members up to date as people enter and leave the cohort.', @@ -41,8 +43,10 @@ export const COHORT_PROVIDERS: Record = { label: 'Username', value: 'Any value' }, { label: 'Password', mono: true, value: '{YOUR_SYNCHRONISATION_KEY}' }, ], - endpointFieldTitle: 'Webhook URL', - endpointPath: 'cohort-sync/mixpanel/webhook/', + endpoint: { + fieldTitle: 'Webhook URL', + path: 'cohort-sync/mixpanel/webhook/', + }, endpointStepTitle: 'Create a webhook in Mixpanel', exportStepBody: 'In Mixpanel, open the cohort you want to target and export it to the webhook you just created. Flagsmith creates the managed segment automatically on the first synchronisation, then keeps its members up to date as people enter and leave the cohort.', @@ -55,8 +59,13 @@ export const COHORT_PROVIDERS: Record = // providers need an absolute callback URL, so resolve against the page origin. export const getCohortProviderEndpoint = ( provider: CohortProviderKey, -): string => - new URL( - COHORT_PROVIDERS[provider].endpointPath, +): string | null => { + const endpoint = COHORT_PROVIDERS[provider].endpoint + if (!endpoint) { + return null + } + return new URL( + endpoint.path, new URL(Project.api, window.location.origin), ).toString() +} From 7260b79b9ba29313ec9ff39e9409e1bfe676d392 Mon Sep 17 00:00:00 2001 From: wadii Date: Mon, 31 Aug 2026 16:39:49 +0200 Subject: [PATCH 07/13] fix: dual permission tooltip copy and scoped e2e key revocation --- frontend/common/constants.ts | 2 ++ .../e2e/tests/cohort-sync-keys-test.pw.ts | 23 +++++-------------- .../CohortSyncKeyStep.tsx | 9 ++++---- .../tabs/cohort-sync-tab/CohortSyncTab.tsx | 4 +--- 4 files changed, 13 insertions(+), 25 deletions(-) diff --git a/frontend/common/constants.ts b/frontend/common/constants.ts index a643ea5a2c5b..3340c028da74 100644 --- a/frontend/common/constants.ts +++ b/frontend/common/constants.ts @@ -198,6 +198,8 @@ const Constants = { 'React Native': 'javascript', }, }, + cohortSyncKeyPermissions: + 'To manage cohort synchronisation keys you need the Manage segment overrides permission for this environment and the Manage segments permission for this project.
Please contact an administrator.', colours: { primary: '#6837fc', white: '#ffffff', diff --git a/frontend/e2e/tests/cohort-sync-keys-test.pw.ts b/frontend/e2e/tests/cohort-sync-keys-test.pw.ts index 38ff73b512f6..2d71bda1c62f 100644 --- a/frontend/e2e/tests/cohort-sync-keys-test.pw.ts +++ b/frontend/e2e/tests/cohort-sync-keys-test.pw.ts @@ -158,26 +158,15 @@ test.describe('Cohort Synchronisation Keys Tests', () => { await waitForModalToClose() await expect(keysList).toContainText(keyThree) - log('Revoke every key for this environment') - const revokeButtons = keysList.locator('[aria-label^="Revoke "]') - for ( - let remaining = await revokeButtons.count(); - remaining > 0; - remaining-- - ) { - await revokeButtons.first().click() + log('Revoke the keys created by this test') + for (const name of [keyOne, keyTwo, keyThree]) { + const row = keysList.locator('.list-item').filter({ hasText: name }) + await row.locator('[aria-label^="Revoke "]').click() await click('#confirm-btn-yes') - await expect(page.locator('#confirm-btn-yes')).toHaveCount(0, { - timeout: LONG_TIMEOUT, - }) - await expect(revokeButtons).toHaveCount(remaining - 1, { - timeout: LONG_TIMEOUT, - }) + await expect(row).toHaveCount(0, { timeout: LONG_TIMEOUT }) } - log('Verify no keys remain') - // PanelSearch drops the list entirely and renders its empty state instead. - await expect(page.locator('#cohort-sync-keys-list')).toHaveCount(0) + log('Verify the test keys are gone') for (const name of [keyOne, keyTwo, keyThree]) { await expect(page.getByText(name, { exact: true })).toHaveCount(0) } diff --git a/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx b/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx index a4e8f151f131..9e8ce1c1ed57 100644 --- a/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx +++ b/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx @@ -108,8 +108,9 @@ const CohortSyncKeyStep: FC = ({ if (!hasKeys && !canManage) { return (

- You do not have permission to create synchronisation keys in this - environment. + Creating synchronisation keys needs the Manage segment overrides + permission for this environment and the Manage segments permission for + this project.
) } @@ -193,9 +194,7 @@ const CohortSyncKeyStep: FC = ({ } place='right' > - {Constants.environmentPermissions( - EnvironmentPermission.MANAGE_SEGMENT_OVERRIDES, - )} + {Constants.cohortSyncKeyPermissions} )} diff --git a/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx index ce2593199054..2280a866962e 100644 --- a/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx +++ b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx @@ -148,9 +148,7 @@ const CohortSyncTab: FC = ({ } place='right' > - {Constants.environmentPermissions( - EnvironmentPermission.MANAGE_SEGMENT_OVERRIDES, - )} + {Constants.cohortSyncKeyPermissions} )} From cb70c79a8e98214be4e79d1fbfacb1e30d729ecf Mon Sep 17 00:00:00 2001 From: wadii Date: Mon, 31 Aug 2026 16:48:58 +0200 Subject: [PATCH 08/13] fix: cohort keys list header layout --- .../tabs/cohort-sync-tab/CohortSyncTab.tsx | 59 +++++++++---------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx index 2280a866962e..22fac323fac2 100644 --- a/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx +++ b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx @@ -14,7 +14,6 @@ import { CohortSyncKey } from 'common/types/responses' import Button from 'components/base/forms/Button' import Flex from 'components/base/grid/Flex' import FormGroup from 'components/base/grid/FormGroup' -import Panel from 'components/base/grid/Panel' import Row from 'components/base/grid/Row' import Icon from 'components/icons/Icon' import PanelSearch from 'components/PanelSearch' @@ -127,50 +126,48 @@ const CohortSyncTab: FC = ({ Learn about Segments.

-

+

Key values are shown only once at creation and cannot be recovered afterwards.

- {canManage ? ( - - ) : ( - - Create Cohort Synchronisation Key - - } - place='right' - > - {Constants.cohortSyncKeyPermissions} - - )} {isLoading && !syncKeys ? ( ) : ( + Create Cohort Key + + ) : ( + + Create Cohort Key + + } + place='left' + > + {Constants.cohortSyncKeyPermissions} + + ) + } renderNoResults={ - -
- - You currently have no cohort synchronisation keys for this - environment. - -
-
+ + You currently have no cohort synchronisation keys for this + environment. + } isLoading={isLoading} /> From 114b1600c11ce7f41ac9362f36ae48fcf37166a8 Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 1 Sep 2026 12:52:00 +0200 Subject: [PATCH 09/13] fix: rename environment settings tab to Cohorts and remove extra webhooks divider --- frontend/e2e/tests/cohort-sync-keys-test.pw.ts | 2 +- .../modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx | 2 +- .../pages/environment-settings/EnvironmentSettingsPage.tsx | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/e2e/tests/cohort-sync-keys-test.pw.ts b/frontend/e2e/tests/cohort-sync-keys-test.pw.ts index 2d71bda1c62f..4488baf9ead7 100644 --- a/frontend/e2e/tests/cohort-sync-keys-test.pw.ts +++ b/frontend/e2e/tests/cohort-sync-keys-test.pw.ts @@ -132,7 +132,7 @@ test.describe('Cohort Synchronisation Keys Tests', () => { // This link carries the environment the modal is working against, so // following it guarantees we inspect the keys we just created. const settingsLink = connectModal.locator( - 'a[href*="tab=cohort-synchronisation"]', + 'a[href*="tab=cohorts"]', ) await expect(settingsLink).toBeVisible() const settingsHref = (await settingsLink.getAttribute('href')) ?? '' diff --git a/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx b/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx index 9e8ce1c1ed57..9a4eaff098f1 100644 --- a/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx +++ b/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx @@ -169,7 +169,7 @@ const CohortSyncKeyStep: FC = ({ You can revoke keys in{' '} {/* Plain anchor: this renders in the modal root, outside the Router. */} closeModal()} > Environment Settings diff --git a/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx b/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx index 001ea493535a..6c8693b16191 100644 --- a/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx +++ b/frontend/web/components/pages/environment-settings/EnvironmentSettingsPage.tsx @@ -799,7 +799,6 @@ const EnvironmentSettingsPage: React.FC = () => {
-
Feature Webhooks
@@ -928,7 +927,7 @@ const EnvironmentSettingsPage: React.FC = () => { )} {cohortSyncEnabled && ( - +
Date: Tue, 1 Sep 2026 13:51:11 +0200 Subject: [PATCH 10/13] fix: cohort key modal polish, identities env pre-fill and env-scoped override segments --- frontend/web/components/PanelSearch.tsx | 7 ++++++- frontend/web/components/SegmentOverrides.js | 6 ++++++ .../ConnectCohortProviderModal/CohortSyncKeyStep.tsx | 10 +++++----- frontend/web/components/modals/CreateSegment.tsx | 3 +++ .../tabs/cohort-sync-tab/CreateCohortSyncKeyModal.tsx | 7 +++---- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/frontend/web/components/PanelSearch.tsx b/frontend/web/components/PanelSearch.tsx index d435b684cfd4..ba8f5bb5e19e 100644 --- a/frontend/web/components/PanelSearch.tsx +++ b/frontend/web/components/PanelSearch.tsx @@ -319,7 +319,12 @@ const PanelSearch = (props: PanelSearchProps): ReactElement => { )} {onRefresh && ( - )} diff --git a/frontend/web/components/SegmentOverrides.js b/frontend/web/components/SegmentOverrides.js index 102fcec50d55..d117f9f4c550 100644 --- a/frontend/web/components/SegmentOverrides.js +++ b/frontend/web/components/SegmentOverrides.js @@ -739,6 +739,12 @@ class TheComponent extends Component { const filter = (segment) => { if (segment.feature && segment.feature !== this.props.feature) return false + // A cohort-managed segment only has members in its own environment. + if ( + segment.cohort && + segment.cohort.environment_api_key !== this.props.environmentId + ) + return false if (this.props.id && this.props.id !== segment.id) return null const foundSegment = find(value, (v) => v.segment === segment.id) return !value || !foundSegment || (foundSegment && foundSegment.toRemove) diff --git a/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx b/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx index 9a4eaff098f1..00b44e56bf6d 100644 --- a/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx +++ b/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx @@ -81,7 +81,7 @@ const CohortSyncKeyStep: FC = ({ if (createdKey) return 'Synchronisation key' if (hasKeys && !isCreateFormOpen) return 'Use your existing key' if (!canManage) return 'Synchronisation key' - return 'Create a synchronisation key' + return 'Generate a synchronisation key' } const renderBody = () => { @@ -97,10 +97,9 @@ const CohortSyncKeyStep: FC = ({ className='font-monospace' data-test='connect-provider-key-value' /> - +
+ +
) } @@ -119,6 +118,7 @@ const CohortSyncKeyStep: FC = ({ return ( = (props) => { useEffect(() => { if (segmentData) { props.onSegmentRetrieved?.(segmentData) + if (segmentData.cohort?.environment_api_key) { + setEnvironmentId(segmentData.cohort.environment_api_key) + } } //eslint-disable-next-line }, [segmentData]) diff --git a/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CreateCohortSyncKeyModal.tsx b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CreateCohortSyncKeyModal.tsx index 9988a37fea3d..338228646574 100644 --- a/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CreateCohortSyncKeyModal.tsx +++ b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CreateCohortSyncKeyModal.tsx @@ -39,10 +39,9 @@ const CreateCohortSyncKeyModal: FC = ({ className='font-monospace' data-test='cohort-sync-key-value' /> - +
+ +
From 27d36ff6faed2e81e827c2c5870a10528c56a7eb Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 1 Sep 2026 13:53:24 +0200 Subject: [PATCH 11/13] fix: surface sync key query errors instead of empty state --- .../tabs/cohort-sync-tab/CohortSyncTab.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx index 22fac323fac2..03c65344a792 100644 --- a/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx +++ b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx @@ -12,6 +12,7 @@ import { } from 'common/types/permissions.types' import { CohortSyncKey } from 'common/types/responses' import Button from 'components/base/forms/Button' +import ErrorMessage from 'components/ErrorMessage' import Flex from 'components/base/grid/Flex' import FormGroup from 'components/base/grid/FormGroup' import Row from 'components/base/grid/Row' @@ -42,7 +43,11 @@ const CohortSyncTab: FC = ({ }) const canManage = !!canManageOverrides && !!canManageSegments - const { data: syncKeys, isLoading } = useGetCohortSyncKeysQuery( + const { + data: syncKeys, + error, + isLoading, + } = useGetCohortSyncKeysQuery( { environmentApiKey }, { skip: !environmentApiKey }, ) @@ -131,9 +136,9 @@ const CohortSyncTab: FC = ({ afterwards.

- {isLoading && !syncKeys ? ( - - ) : ( + {!!error && } + {isLoading && !syncKeys && !error && } + {!error && (!isLoading || !!syncKeys) && ( Date: Tue, 1 Sep 2026 14:03:04 +0200 Subject: [PATCH 12/13] fix: tighten show-once warning spacing --- .../modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx | 2 +- .../tabs/cohort-sync-tab/CreateCohortSyncKeyModal.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx b/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx index 00b44e56bf6d..a9a447d7b78a 100644 --- a/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx +++ b/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx @@ -97,7 +97,7 @@ const CohortSyncKeyStep: FC = ({ className='font-monospace' data-test='connect-provider-key-value' /> -
+
diff --git a/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CreateCohortSyncKeyModal.tsx b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CreateCohortSyncKeyModal.tsx index 338228646574..3329043d385a 100644 --- a/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CreateCohortSyncKeyModal.tsx +++ b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CreateCohortSyncKeyModal.tsx @@ -39,7 +39,7 @@ const CreateCohortSyncKeyModal: FC = ({ className='font-monospace' data-test='cohort-sync-key-value' /> -
+
From 4d2225f92922ae87d6b9e8bf8340e5e3ab1c6dc9 Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 1 Sep 2026 14:44:20 +0200 Subject: [PATCH 13/13] fix: use Link component for docs and settings links --- .../ConnectCohortProviderModal/CohortSyncKeyStep.tsx | 7 ++++--- .../ConnectCohortProviderModal.tsx | 10 +++------- .../tabs/cohort-sync-tab/CohortSyncTab.tsx | 7 +++---- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx b/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx index a9a447d7b78a..09f35e863019 100644 --- a/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx +++ b/frontend/web/components/modals/ConnectCohortProviderModal/CohortSyncKeyStep.tsx @@ -13,6 +13,7 @@ import { import { CohortSyncKey } from 'common/types/responses' import Button from 'components/base/forms/Button' import InputGroup from 'components/base/forms/InputGroup' +import Link from 'components/base/link/Link' import CopyField from 'components/CopyField' import ErrorMessage from 'components/ErrorMessage' import Tooltip from 'components/Tooltip' @@ -167,13 +168,13 @@ const CohortSyncKeyStep: FC = ({
{canManage ? ( diff --git a/frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderModal.tsx b/frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderModal.tsx index 994042752f42..834409f72451 100644 --- a/frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderModal.tsx +++ b/frontend/web/components/modals/ConnectCohortProviderModal/ConnectCohortProviderModal.tsx @@ -2,6 +2,7 @@ import React, { FC, useMemo, useState } from 'react' import { useGetEnvironmentsQuery } from 'common/services/useEnvironment' import Button from 'components/base/forms/Button' import FieldLabel from 'components/base/forms/FieldLabel' +import Link from 'components/base/link/Link' import CopyField from 'components/CopyField' import EnvironmentSelect from 'components/EnvironmentSelect' import ModalHR from 'components/modals/ModalHR' @@ -126,14 +127,9 @@ const ConnectCohortProviderModal: FC = ({
- + diff --git a/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx index 03c65344a792..7438e9d4b020 100644 --- a/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx +++ b/frontend/web/components/pages/environment-settings/tabs/cohort-sync-tab/CohortSyncTab.tsx @@ -13,6 +13,7 @@ import { import { CohortSyncKey } from 'common/types/responses' import Button from 'components/base/forms/Button' import ErrorMessage from 'components/ErrorMessage' +import Link from 'components/base/link/Link' import Flex from 'components/base/grid/Flex' import FormGroup from 'components/base/grid/FormGroup' import Row from 'components/base/grid/Row' @@ -122,14 +123,12 @@ const CohortSyncTab: FC = ({

Cohort synchronisation keys authenticate cohort synchronisation from providers such as Mixpanel and Amplitude into this environment.{' '} - +

Key values are shown only once at creation and cannot be recovered