Skip to content

Commit

Permalink
add ui and logic to add/update/edit/show triggers
Browse files Browse the repository at this point in the history
  • Loading branch information
Richard Borcsik committed Oct 9, 2024
1 parent 144ced4 commit 57ee738
Show file tree
Hide file tree
Showing 2 changed files with 231 additions and 5 deletions.
129 changes: 127 additions & 2 deletions frontend/src/scenes/settings/environment/SessionRecordingSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { IconPlus } from '@posthog/icons'
import { IconPencil, IconPlus, IconTrash } from '@posthog/icons'
import {
LemonBanner,
LemonButton,
LemonDialog,
LemonInput,
LemonSegmentedButton,
LemonSegmentedButtonOption,
LemonSelect,
Expand All @@ -11,7 +12,9 @@ import {
Link,
Spinner,
} from '@posthog/lemon-ui'
import clsx from 'clsx'
import { useActions, useValues } from 'kea'
import { Form } from 'kea-forms'
import { AuthorizedUrlList } from 'lib/components/AuthorizedUrlList/AuthorizedUrlList'
import { AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic'
import { EventSelect } from 'lib/components/EventSelect/EventSelect'
Expand All @@ -21,13 +24,19 @@ import { PropertySelect } from 'lib/components/PropertySelect/PropertySelect'
import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types'
import { SESSION_REPLAY_MINIMUM_DURATION_OPTIONS } from 'lib/constants'
import { IconCancel, IconSelectEvents } from 'lib/lemon-ui/icons'
import { LemonField } from 'lib/lemon-ui/LemonField'
import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel'
import { objectsEqual } from 'lib/utils'
import { sessionReplayIngestionControlLogic } from 'scenes/settings/environment/sessionReplayIngestionControlLogic'
import { teamLogic } from 'scenes/teamLogic'
import { userLogic } from 'scenes/userLogic'

import { AvailableFeature, MultivariateFlagOptions, SessionRecordingAIConfig } from '~/types'
import {
AvailableFeature,
MultivariateFlagOptions,
SessionRecordingAIConfig,
SessionReplayUrlTriggerConfig,
} from '~/types'

function LogCaptureSettings(): JSX.Element {
const { updateCurrentTeam } = useActions(teamLogic)
Expand Down Expand Up @@ -329,6 +338,121 @@ function LinkedFlagSelector(): JSX.Element | null {
)
}

function UrlTriggerForm(): JSX.Element {
const { cancelProposingUrlTrigger } = useActions(sessionReplayIngestionControlLogic)
const { isProposedUrlTriggerSubmitting } = useValues(sessionReplayIngestionControlLogic)

return (
<Form
logic={sessionReplayIngestionControlLogic}
formKey="proposedUrlTrigger"
enableFormOnSubmit
className="w-full flex flex-col border rounded items-center p-2 pl-4 bg-bg-light gap-2"
>
<div className="flex flex-row gap-2 w-full">
<LemonField name="matching">
<LemonSelect options={[{ label: 'Regex', value: 'regex' }]} />
</LemonField>
<LemonField name="url" className="flex-1">
<LemonInput autoFocus placeholder="Enter URL" data-attr="url-input" />
</LemonField>
</div>
<div className="flex justify-end gap-2 w-full">
<LemonButton type="secondary" onClick={cancelProposingUrlTrigger}>
Cancel
</LemonButton>
<LemonButton
htmlType="submit"
type="primary"
disabledReason={isProposedUrlTriggerSubmitting ? 'Saving url trigger in progress' : undefined}
data-attr="url-save"
>
Save
</LemonButton>
</div>
</Form>
)
}

function UrlTriggerRow({ trigger, index }: { trigger: SessionReplayUrlTriggerConfig; index: number }): JSX.Element {
const { editUrlTriggerIndex } = useValues(sessionReplayIngestionControlLogic)
const { setEditUrlTriggerIndex, removeUrlTrigger } = useActions(sessionReplayIngestionControlLogic)

if (editUrlTriggerIndex === index) {
return (
<div className="border rounded p-2 bg-bg-light">
<UrlTriggerForm />
</div>
)
}

return (
<div className={clsx('border rounded flex items-center p-2 pl-4 bg-bg-light')}>
<span title={trigger.url} className="flex-1 truncate">
{trigger.matching === 'regex' ? 'Matches regex: ' : ''} {trigger.url}
</span>
<div className="Actions flex space-x-2 shrink-0">
<LemonButton
icon={<IconPencil />}
onClick={() => setEditUrlTriggerIndex(index)}
tooltip="Edit"
center
/>

<LemonButton
icon={<IconTrash />}
tooltip="Remove URL trigger"
center
onClick={() => {
LemonDialog.open({
title: <>Remove URL trigger</>,
description: `Are you sure you want to remove this URL trigger?`,
primaryButton: {
status: 'danger',
children: 'Remove',
onClick: () => removeUrlTrigger(index),
},
secondaryButton: {
children: 'Cancel',
},
})
}}
/>
</div>
</div>
)
}

function UrlTriggerOptions(): JSX.Element | null {
const { isAddUrlTriggerConfigFormVisible, urlTriggerConfig } = useValues(sessionReplayIngestionControlLogic)
const { newUrlTrigger } = useActions(sessionReplayIngestionControlLogic)

return (
<>
<div className="flex flex-col space-y-2">
<div className="flex items-center mb-4 gap-2 justify-between">
<LemonLabel className="text-base">Enable recordings when URL matches</LemonLabel>
<LemonButton
onClick={() => {
newUrlTrigger()
}}
type="secondary"
icon={<IconPlus />}
data-attr="session-replay-add-url-trigger"
>
Add
</LemonButton>
</div>

{isAddUrlTriggerConfigFormVisible && <UrlTriggerForm />}
{urlTriggerConfig?.map((trigger, index) => (
<UrlTriggerRow key={`${trigger.url}-${trigger.matching}`} trigger={trigger} index={index} />
))}
</div>
</>
)
}

export function ReplayCostControl(): JSX.Element | null {
const { updateCurrentTeam } = useActions(teamLogic)
const { currentTeam } = useValues(teamLogic)
Expand Down Expand Up @@ -483,6 +607,7 @@ export function ReplayCostControl(): JSX.Element | null {
</>
)}
<LinkedFlagSelector />
<UrlTriggerOptions />
</>
</PayGateMini>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,70 @@
import { actions, afterMount, connect, kea, path, props, reducers, selectors } from 'kea'
import { actions, afterMount, connect, kea, listeners, path, props, reducers, selectors, sharedListeners } from 'kea'
import { forms } from 'kea-forms'
import { loaders } from 'kea-loaders'
import { subscriptions } from 'kea-subscriptions'
import api from 'lib/api'
import { isObject } from 'lib/utils'
import { variantKeyToIndexFeatureFlagPayloads } from 'scenes/feature-flags/featureFlagLogic'
import { teamLogic } from 'scenes/teamLogic'

import { FeatureFlagBasicType } from '~/types'
import { FeatureFlagBasicType, SessionReplayUrlTriggerConfig, TeamPublicType, TeamType } from '~/types'

import type { sessionReplayIngestionControlLogicType } from './sessionReplayIngestionControlLogicType'

const NEW_URL_TRIGGER = { url: '', matching: 'regex' }

export const sessionReplayIngestionControlLogic = kea<sessionReplayIngestionControlLogicType>([
path(['scenes', 'settings', 'project', 'sessionReplayIngestionControlLogic']),
actions({
selectFeatureFlag: (flag: FeatureFlagBasicType) => ({ flag }),
setUrlTriggerConfig: (urlTriggerConfig: SessionReplayUrlTriggerConfig[]) => ({ urlTriggerConfig }),

addUrlTrigger: (urlTriggerConfig: SessionReplayUrlTriggerConfig) => ({ urlTriggerConfig }),
removeUrlTrigger: (index: number) => ({ index }),
updateUrlTrigger: (index: number, urlTriggerConfig: SessionReplayUrlTriggerConfig) => ({
index,
urlTriggerConfig,
}),
setEditUrlTriggerIndex: (originalIndex: number | null) => ({ originalIndex }),
newUrlTrigger: true,
cancelProposingUrlTrigger: true,
}),
connect({ values: [teamLogic, ['currentTeam']] }),
connect({ values: [teamLogic, ['currentTeam']], actions: [teamLogic, ['updateCurrentTeam']] }),
reducers({
selectedFlag: [
null as FeatureFlagBasicType | null,
{
selectFeatureFlag: (_, { flag }) => flag,
},
],
urlTriggerConfig: [
null as SessionReplayUrlTriggerConfig[] | null,
{
setUrlTriggerConfig: (_, { urlTriggerConfig }) => urlTriggerConfig,
addUrlTrigger: (state, { urlTriggerConfig }) => [...(state ?? []), urlTriggerConfig],
updateUrlTrigger: (state, { index, urlTriggerConfig: newUrlTriggerConfig }) =>
(state ?? []).map((triggerConfig, i) => (i === index ? newUrlTriggerConfig : triggerConfig)),
removeUrlTrigger: (state, { index }) => {
return (state ?? []).filter((_, i) => i !== index)
},
},
],
editUrlTriggerIndex: [
null as number | null,
{
setEditUrlTriggerIndex: (_, { originalIndex }) => originalIndex,
removeUrlTrigger: (editUrlTriggerIndex, { index }) =>
editUrlTriggerIndex && index < editUrlTriggerIndex
? editUrlTriggerIndex - 1
: index === editUrlTriggerIndex
? null
: editUrlTriggerIndex,
newUrlTrigger: () => -1,
updateUrlTrigger: () => null,
addUrlTrigger: () => null,
cancelProposingUrlTrigger: () => null,
},
],
}),
props({}),
loaders(({ values }) => ({
Expand Down Expand Up @@ -50,8 +93,66 @@ export const sessionReplayIngestionControlLogic = kea<sessionReplayIngestionCont
currentTeam?.session_recording_linked_flag?.id ? selectedFlag || featureFlag : null,
],
flagHasVariants: [(s) => [s.linkedFlag], (linkedFlag) => isObject(linkedFlag?.filters.multivariate)],
remoteUrlTriggerConfig: [
(s) => [s.currentTeam],
(currentTeam) => currentTeam?.session_recording_url_trigger_config,
],
isAddUrlTriggerConfigFormVisible: [
(s) => [s.editUrlTriggerIndex],
(editUrlTriggerIndex) => editUrlTriggerIndex === -1,
],
urlTriggerToEdit: [
(s) => [s.urlTriggerConfig, s.editUrlTriggerIndex],
(urlTriggerConfig, editUrlTriggerIndex) => {
if (
editUrlTriggerIndex === null ||
editUrlTriggerIndex === -1 ||
!urlTriggerConfig?.[editUrlTriggerIndex]
) {
return NEW_URL_TRIGGER
}
return urlTriggerConfig[editUrlTriggerIndex]
},
],
}),
afterMount(({ actions }) => {
actions.loadFeatureFlag()
}),
subscriptions(({ actions }) => ({
currentTeam: (currentTeam: TeamPublicType | TeamType | null) => {
actions.setUrlTriggerConfig(currentTeam?.session_recording_url_trigger_config ?? [])
},
})),
forms(({ values, actions }) => ({
proposedUrlTrigger: {
defaults: { url: '', matching: 'regex' } as SessionReplayUrlTriggerConfig,
submit: async ({ url, matching }) => {
if (values.editUrlTriggerIndex !== null && values.editUrlTriggerIndex >= 0) {
actions.updateUrlTrigger(values.editUrlTriggerIndex, { url, matching })
} else {
actions.addUrlTrigger({ url, matching })
}
},
},
})),
sharedListeners(({ values }) => ({
saveUrlTriggers: async () => {
await teamLogic.asyncActions.updateCurrentTeam({
session_recording_url_trigger_config: values.urlTriggerConfig ?? [],
})
},
})),
listeners(({ sharedListeners, actions, values }) => ({
setEditUrlTriggerIndex: () => {
actions.setProposedUrlTriggerValue('url', values.urlTriggerToEdit.url)
actions.setProposedUrlTriggerValue('matching', values.urlTriggerToEdit.matching)
},
addUrlTrigger: sharedListeners.saveUrlTriggers,
removeUrlTrigger: sharedListeners.saveUrlTriggers,
updateUrlTrigger: sharedListeners.saveUrlTriggers,
submitProposedUrlTriggerSuccess: () => {
actions.setEditUrlTriggerIndex(null)
actions.resetProposedUrlTrigger()
},
})),
])

0 comments on commit 57ee738

Please sign in to comment.