diff --git a/frontend/common/services/useWarehouseConnection.ts b/frontend/common/services/useWarehouseConnection.ts index 18eb6028bb44..5dcce7b64bea 100644 --- a/frontend/common/services/useWarehouseConnection.ts +++ b/frontend/common/services/useWarehouseConnection.ts @@ -27,6 +27,15 @@ export const warehouseConnectionService = service url: `environments/${environmentId}/warehouse-connections/${id}/`, }), }), + getWarehouseConnectionEvents: builder.query< + Res['warehouseConnectionEvents'], + Req['getWarehouseConnectionEvents'] + >({ + providesTags: [{ id: 'EVENTS', type: 'WarehouseConnection' }], + query: ({ environmentId, id }) => ({ + url: `environments/${environmentId}/warehouse-connections/${id}/events/`, + }), + }), getWarehouseConnections: builder.query< Res['warehouseConnections'], Req['getWarehouseConnections'] @@ -75,6 +84,7 @@ export const warehouseConnectionService = service export const { useCreateWarehouseConnectionMutation, useDeleteWarehouseConnectionMutation, + useGetWarehouseConnectionEventsQuery, useGetWarehouseConnectionsQuery, useTestWarehouseConnectionConfigMutation, useTestWarehouseConnectionMutation, diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index ffa081e350b8..0f91cc478657 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -1030,6 +1030,7 @@ export type Req = { environmentId: string exclude_event_stats?: boolean } + getWarehouseConnectionEvents: { environmentId: string; id: number } createWarehouseConnection: { environmentId: string warehouse_type: string diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index 6461b095aa9e..0d8a5ba0096b 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -1279,6 +1279,11 @@ export type WarehouseConnectionTestResult = { status_detail: string | null } +export type WarehouseConnectionEvents = { + events: string[] + is_truncated: boolean +} + export type WarehouseConnection = { id: number warehouse_type: WarehouseType @@ -1523,6 +1528,7 @@ export type Res = { gitlabIssues: PagedResponse gitlabMergeRequests: PagedResponse warehouseConnections: WarehouseConnection[] + warehouseConnectionEvents: WarehouseConnectionEvents warehouseConnectionTestResult: WarehouseConnectionTestResult experiments: PagedResponse & { currentPage: number diff --git a/frontend/web/components/experiments/CreateMetricForm/CreateMetricForm.tsx b/frontend/web/components/experiments/CreateMetricForm/CreateMetricForm.tsx index afeaca52b6de..4be92bdb09b2 100644 --- a/frontend/web/components/experiments/CreateMetricForm/CreateMetricForm.tsx +++ b/frontend/web/components/experiments/CreateMetricForm/CreateMetricForm.tsx @@ -1,6 +1,7 @@ import React, { FC, useState } from 'react' import Button from 'components/base/forms/Button' import Input from 'components/base/forms/Input' +import EventNameSelect from 'components/experiments/EventNameSelect' import { canSubmitMetric, DEFAULT_METRIC_FORM_STATE, @@ -149,13 +150,9 @@ const CreateMetricForm: FC = ({ > Event name - ) => - update({ event: e.target.value }) - } - placeholder='e.g. checkout_completed' + onChange={(event) => update({ event })} /> diff --git a/frontend/web/components/experiments/EventNameSelect/EventNameSelect.scss b/frontend/web/components/experiments/EventNameSelect/EventNameSelect.scss new file mode 100644 index 000000000000..95ce422d2e0f --- /dev/null +++ b/frontend/web/components/experiments/EventNameSelect/EventNameSelect.scss @@ -0,0 +1,29 @@ +.event-name-select { + position: relative; + + .react-select__menu { + position: absolute; + left: 0; + right: 0; + z-index: 3; + } + + .react-select .react-select__menu-list { + overflow-y: auto; + scrollbar-width: auto; + + &::-webkit-scrollbar { + display: block; + width: 8px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: var(--color-border-strong); + border-radius: 4px; + } + } +} diff --git a/frontend/web/components/experiments/EventNameSelect/EventNameSelect.tsx b/frontend/web/components/experiments/EventNameSelect/EventNameSelect.tsx new file mode 100644 index 000000000000..5e960bf5c0be --- /dev/null +++ b/frontend/web/components/experiments/EventNameSelect/EventNameSelect.tsx @@ -0,0 +1,65 @@ +import { FC, useMemo } from 'react' +import CreatableSelect from 'react-select/creatable' +import { + useGetWarehouseConnectionEventsQuery, + useGetWarehouseConnectionsQuery, +} from 'common/services/useWarehouseConnection' +import { WarehouseType } from 'common/types/responses' +import { useRouteContext } from 'components/providers/RouteContext' +import Icon from 'components/icons/Icon' +import { buildEventOptions, EventOption, isUnknownEvent } from './utils' +import './EventNameSelect.scss' + +type EventNameSelectProps = { + value: string + onChange: (value: string) => void +} + +const SUPPORTED_WAREHOUSE_TYPES: WarehouseType[] = ['flagsmith', 'clickhouse'] + +const EventNameSelect: FC = ({ onChange, value }) => { + const { environmentId } = useRouteContext() + const { data: connections } = useGetWarehouseConnectionsQuery( + { environmentId: environmentId ?? '', exclude_event_stats: true }, + { skip: !environmentId }, + ) + const connection = connections?.[0] + const canListEvents = + !!connection && + SUPPORTED_WAREHOUSE_TYPES.includes(connection.warehouse_type) + const { data, isLoading, isSuccess } = useGetWarehouseConnectionEventsQuery( + { environmentId: environmentId ?? '', id: connection?.id ?? 0 }, + { skip: !environmentId || !canListEvents }, + ) + const options = useMemo(() => buildEventOptions(data?.events), [data?.events]) + const showWarning = isSuccess && isUnknownEvent(value, data?.events) + + return ( +
+ onChange(option?.value ?? '')} + placeholder='e.g. checkout_completed' + formatCreateLabel={(input: string) => `Use "${input}"`} + noOptionsMessage={() => 'Type to add a new event'} + /> + {showWarning && ( + + + This event hasn't been received by your warehouse yet. + + )} +
+ ) +} + +EventNameSelect.displayName = 'EventNameSelect' +export default EventNameSelect diff --git a/frontend/web/components/experiments/EventNameSelect/__tests__/utils.test.ts b/frontend/web/components/experiments/EventNameSelect/__tests__/utils.test.ts new file mode 100644 index 000000000000..254c210eb9d5 --- /dev/null +++ b/frontend/web/components/experiments/EventNameSelect/__tests__/utils.test.ts @@ -0,0 +1,33 @@ +import { + buildEventOptions, + isUnknownEvent, +} from 'components/experiments/EventNameSelect/utils' + +describe('buildEventOptions', () => { + it.each([ + ['undefined', undefined, []], + ['empty list', [], []], + [ + 'events', + ['checkout_completed', 'page_view'], + [ + { label: 'checkout_completed', value: 'checkout_completed' }, + { label: 'page_view', value: 'page_view' }, + ], + ], + ])('%s → options', (_, events, expected) => { + expect(buildEventOptions(events)).toEqual(expected) + }) +}) + +describe('isUnknownEvent', () => { + it.each([ + ['empty value', '', ['page_view'], false], + ['known value', 'page_view', ['page_view'], false], + ['unknown value', 'checkout', ['page_view'], true], + ['no fetched list', 'checkout', undefined, false], + ['empty fetched list', 'checkout', [], true], + ])('%s', (_, value, events, expected) => { + expect(isUnknownEvent(value, events)).toBe(expected) + }) +}) diff --git a/frontend/web/components/experiments/EventNameSelect/index.ts b/frontend/web/components/experiments/EventNameSelect/index.ts new file mode 100644 index 000000000000..13381a37b957 --- /dev/null +++ b/frontend/web/components/experiments/EventNameSelect/index.ts @@ -0,0 +1 @@ +export { default } from './EventNameSelect' diff --git a/frontend/web/components/experiments/EventNameSelect/utils.ts b/frontend/web/components/experiments/EventNameSelect/utils.ts new file mode 100644 index 000000000000..0dab8fe29d61 --- /dev/null +++ b/frontend/web/components/experiments/EventNameSelect/utils.ts @@ -0,0 +1,7 @@ +export type EventOption = { label: string; value: string } + +export const buildEventOptions = (events?: string[]): EventOption[] => + (events ?? []).map((event) => ({ label: event, value: event })) + +export const isUnknownEvent = (value: string, events?: string[]): boolean => + !!value && Array.isArray(events) && !events.includes(value)