11import {
22 CheckCircleIcon ,
33 ClockIcon ,
4+ CursorClickIcon ,
5+ EyeIcon ,
6+ PulseIcon ,
7+ UsersIcon ,
8+ WarningIcon ,
49 WarningOctagonIcon ,
510 XCircleIcon ,
611} from '@phosphor-icons/react'
@@ -12,22 +17,25 @@ import _size from 'lodash/size'
1217import _split from 'lodash/split'
1318import _toNumber from 'lodash/toNumber'
1419import _values from 'lodash/values'
15- import React , { useEffect , useMemo , useState } from 'react'
20+ import React , { useEffect , useMemo , useRef , useState } from 'react'
1621import { useTranslation , Trans } from 'react-i18next'
1722import { Link } from '~/ui/Link'
1823import { useFetcher } from 'react-router'
1924import { toast } from 'sonner'
2025
26+ import { getDimensionValues } from '~/api/v2/endpoints'
2127import { useDeduplicateFetcherResponse } from '~/hooks/useDeduplicateFetcherResponse'
2228import { QUERY_CONDITION , QUERY_METRIC , QUERY_TIME } from '~/lib/constants'
2329import { Alerts } from '~/lib/models/Alerts'
2430import type { NotificationChannel } from '~/lib/models/NotificationChannel'
2531import { useAuth } from '~/providers/AuthProvider'
32+ import { useTheme } from '~/providers/ThemeProvider'
2633import type { NotificationChannelActionData } from '~/routes/notification-channel'
2734import type { ProjectViewActionData } from '~/routes/projects.$id'
2835import Button from '~/ui/Button'
2936import Checkbox from '~/ui/Checkbox'
3037import FeedbackButton from '~/ui/FeedbackButton'
38+ import FilterValueInput from '~/ui/FilterValueInput'
3139import Input from '~/ui/Input'
3240import Loader from '~/ui/Loader'
3341import Alert from '~/ui/Alert'
@@ -45,6 +53,14 @@ import {
4553import AlertTemplateEditor from './AlertTemplateEditor'
4654import { BackButton } from '../../View/components/BackButton'
4755
56+ const QUERY_METRIC_ICONS : Record < string , React . ReactNode > = {
57+ [ QUERY_METRIC . PAGE_VIEWS ] : < EyeIcon className = 'size-4' /> ,
58+ [ QUERY_METRIC . UNIQUE_PAGE_VIEWS ] : < UsersIcon className = 'size-4' /> ,
59+ [ QUERY_METRIC . ONLINE_USERS ] : < PulseIcon className = 'size-4' /> ,
60+ [ QUERY_METRIC . CUSTOM_EVENTS ] : < CursorClickIcon className = 'size-4' /> ,
61+ [ QUERY_METRIC . ERRORS ] : < WarningIcon className = 'size-4' /> ,
62+ }
63+
4864interface ProjectAlertsSettingsProps {
4965 alertId ?: string | null
5066 projectId : string
@@ -65,8 +81,12 @@ const ProjectAlertsSettings = ({
6581 backLink,
6682} : ProjectAlertsSettingsProps ) => {
6783 const { isLoading : authLoading } = useAuth ( )
84+ const { theme } = useTheme ( )
6885
69- const { t } = useTranslation ( 'common' )
86+ const {
87+ t,
88+ i18n : { language } ,
89+ } = useTranslation ( 'common' )
7090 const fetcher = useFetcher < ProjectViewActionData > ( )
7191 const channelsFetcher = useFetcher < NotificationChannelActionData > ( )
7292 const shouldHandleFetcherData =
@@ -97,6 +117,35 @@ const ProjectAlertsSettings = ({
97117 const [ availableChannels , setAvailableChannels ] = useState <
98118 NotificationChannel [ ]
99119 > ( [ ] )
120+ const [ customEvents , setCustomEvents ] = useState < string [ ] > ( [ ] )
121+ const [ customEventsLoading , setCustomEventsLoading ] = useState ( false )
122+ const customEventsFetchedRef = useRef ( false )
123+
124+ useEffect ( ( ) => {
125+ if (
126+ form . queryMetric !== QUERY_METRIC . CUSTOM_EVENTS ||
127+ customEventsFetchedRef . current
128+ ) {
129+ return
130+ }
131+
132+ customEventsFetchedRef . current = true
133+ setCustomEventsLoading ( true )
134+ getDimensionValues ( projectId , 'event' , { type : 'traffic' } )
135+ . then ( ( { data } ) => {
136+ setCustomEvents (
137+ ( data || [ ] ) . filter (
138+ ( item ) : item is string => typeof item === 'string' ,
139+ ) ,
140+ )
141+ } )
142+ . catch ( ( reason ) => {
143+ console . error ( 'Failed to fetch custom events:' , reason )
144+ } )
145+ . finally ( ( ) => {
146+ setCustomEventsLoading ( false )
147+ } )
148+ } , [ form . queryMetric , projectId ] )
100149
101150 useEffect ( ( ) => {
102151 const fd = new FormData ( )
@@ -546,40 +595,79 @@ const ProjectAlertsSettings = ({
546595 < Select
547596 id = 'queryMetric'
548597 label = { t ( 'alert.metric' ) }
549- items = { _values ( queryMetricTMapping ) }
598+ items = { _values ( QUERY_METRIC ) }
599+ labelExtractor = { ( item ) => queryMetricTMapping [ item ] }
600+ iconExtractor = { ( item ) => (
601+ < span className = 'text-gray-500 dark:text-gray-400' >
602+ { QUERY_METRIC_ICONS [ item ] }
603+ </ span >
604+ ) }
550605 title = {
551- form . queryMetric ? queryMetricTMapping [ form . queryMetric ] : ''
606+ form . queryMetric ? (
607+ < span className = 'flex items-center gap-2' >
608+ < span className = 'text-gray-500 dark:text-gray-400' >
609+ { QUERY_METRIC_ICONS [ form . queryMetric ] }
610+ </ span >
611+ { queryMetricTMapping [ form . queryMetric ] }
612+ </ span >
613+ ) : (
614+ ''
615+ )
552616 }
553617 onSelect = { ( item ) => {
554- const key = _findKey (
555- queryMetricTMapping ,
556- ( predicate ) => predicate === item ,
557- )
558-
559- // @ts -expect-error
560618 setForm ( ( prevForm ) => ( {
561619 ...prevForm ,
562- queryMetric : key ,
620+ queryMetric : item ,
563621 } ) )
564622 } }
565623 capitalise
566- selectedItem = {
567- form . queryMetric
568- ? queryMetricTMapping [ form . queryMetric ]
569- : undefined
570- }
624+ selectedItem = { form . queryMetric }
571625 />
572626 </ div >
573627 { form . queryMetric === QUERY_METRIC . CUSTOM_EVENTS ? (
574- < Input
575- name = 'queryCustomEvent'
576- label = { t ( 'alert.customEvent' ) }
577- value = { form . queryCustomEvent || '' }
578- placeholder = { t ( 'alert.customEvent' ) }
579- className = 'mt-4'
580- onChange = { handleInput }
581- error = { beenSubmitted ? errors . queryCustomEvent : null }
582- />
628+ < div className = 'mt-4 flex flex-col gap-1' >
629+ < Text
630+ as = 'span'
631+ className = 'flex leading-tight'
632+ size = 'sm'
633+ weight = 'medium'
634+ colour = 'primary'
635+ >
636+ { t ( 'alert.customEvent' ) }
637+ </ Text >
638+ < input
639+ type = 'hidden'
640+ name = 'queryCustomEvent'
641+ value = { form . queryCustomEvent || '' }
642+ />
643+ < FilterValueInput
644+ items = { customEvents }
645+ value = { form . queryCustomEvent || '' }
646+ onChange = { ( value ) =>
647+ setForm ( ( prevForm ) => ( {
648+ ...prevForm ,
649+ queryCustomEvent : value ,
650+ } ) )
651+ }
652+ placeholder = { t ( 'project.filterSearchOrType' ) }
653+ column = 'event'
654+ language = { language }
655+ isLoading = { customEventsLoading }
656+ theme = { theme }
657+ commitOnType
658+ />
659+ { beenSubmitted && errors . queryCustomEvent ? (
660+ < Text
661+ as = 'span'
662+ className = 'block'
663+ size = 'sm'
664+ colour = 'error'
665+ role = 'alert'
666+ >
667+ { errors . queryCustomEvent }
668+ </ Text >
669+ ) : null }
670+ </ div >
583671 ) : null }
584672 { form . queryMetric === QUERY_METRIC . CUSTOM_EVENTS ? (
585673 < Checkbox
0 commit comments