1+ import { api } from '@/utils/client'
12import KeyboardArrowDownOutlined from '@mui/icons-material/KeyboardArrowDownOutlined'
23import KeyboardArrowRightOutlined from '@mui/icons-material/KeyboardArrowRightOutlined'
3- import { schemas } from '@polar-sh/client'
4+ import { schemas , unwrap } from '@polar-sh/client'
45import Avatar from '@polar-sh/ui/components/atoms/Avatar'
6+ import Button from '@polar-sh/ui/components/atoms/Button'
57import {
68 Tooltip ,
79 TooltipContent ,
810 TooltipTrigger ,
911} from '@polar-sh/ui/components/ui/tooltip'
1012import Link from 'next/link'
11- import { useMemo , useState } from 'react'
13+ import { useCallback , useEffect , useMemo , useState } from 'react'
1214import { EventCustomer } from './EventCustomer'
1315import { EventSourceBadge } from './EventSourceBadge'
1416import { useEventCard , useEventCostBadge , useEventDisplayName } from './utils'
1517
1618const EventRow = ( {
1719 event,
1820 organization,
21+ depth = 0 ,
22+ children : initialChildren ,
23+ childrenLimit = 10 ,
1924} : {
2025 event : schemas [ 'Event' ]
2126 organization : schemas [ 'Organization' ]
27+ depth ?: number
28+ children ?: schemas [ 'HierarchicalEvent' ] [ ]
29+ childrenLimit ?: number
2230} ) => {
2331 const [ isExpanded , setIsExpanded ] = useState ( false )
32+ const [ children , setChildren ] = useState < schemas [ 'HierarchicalEvent' ] [ ] > (
33+ initialChildren || [ ] ,
34+ )
35+ const [ childrenPage , setChildrenPage ] = useState ( 1 )
36+ const [ hasMoreChildren , setHasMoreChildren ] = useState (
37+ ( initialChildren ?. length || 0 ) >= childrenLimit ,
38+ )
39+ const [ isLoadingMoreChildren , setIsLoadingMoreChildren ] = useState ( false )
40+
41+ useEffect ( ( ) => {
42+ setChildren ( initialChildren || [ ] )
43+ setChildrenPage ( 1 )
44+ setHasMoreChildren ( ( initialChildren ?. length || 0 ) >= childrenLimit )
45+ } , [ initialChildren , childrenLimit ] )
46+
47+ const hasChildren = children && children . length > 0
2448
2549 const handleToggleExpand = ( ) => {
2650 setIsExpanded ( ! isExpanded )
2751 }
2852
53+ const loadMoreChildren = useCallback ( async ( ) => {
54+ setIsLoadingMoreChildren ( true )
55+ try {
56+ const result = await unwrap (
57+ api . GET ( '/v1/events/hierarchical' , {
58+ params : {
59+ query : {
60+ organization_id : event . organization_id ,
61+ parent_id : event . id ,
62+ page : childrenPage + 1 ,
63+ limit : childrenLimit ,
64+ } ,
65+ } ,
66+ } ) ,
67+ )
68+ setChildren ( ( prev ) => [ ...prev , ...result . items ] )
69+ setChildrenPage ( ( prev ) => prev + 1 )
70+ setHasMoreChildren (
71+ childrenPage + 1 < result . pagination . max_page ||
72+ result . items . length >= childrenLimit ,
73+ )
74+ } catch ( error ) {
75+ console . error ( 'Failed to load more children:' , error )
76+ } finally {
77+ setIsLoadingMoreChildren ( false )
78+ }
79+ } , [ event . id , event . organization_id , childrenPage , childrenLimit ] )
80+
2981 const formattedTimestamp = useMemo (
3082 ( ) =>
3183 new Date ( event . timestamp ) . toLocaleDateString (
@@ -50,68 +102,115 @@ const EventRow = ({
50102
51103 const eventDisplayName = useEventDisplayName ( event . name )
52104 const eventCard = useEventCard ( event )
53- const eventCostBadge = useEventCostBadge ( event )
105+ const eventCostBadge = useEventCostBadge ( event , children )
106+
107+ const leftMargin = depth > 0 ? `${ depth * 32 } px` : '0px'
54108
55109 return (
56- < div className = "dark:bg-polar-800 dark:border-polar-700 group dark:hover:bg-polar-700 flex flex-col rounded-xl border border-gray-200 bg-white font-mono text-sm transition-colors duration-150 hover:bg-gray-50 " >
110+ < div className = "flex flex-col gap-y-2 " >
57111 < div
58- onClick = { handleToggleExpand }
59- className = "flex cursor-pointer flex-row items-center justify-between p-3 select-none"
112+ className = "dark:bg-polar-800 dark:border-polar-700 group dark:hover:bg-polar-700 flex flex-col rounded-xl border border-gray-200 bg-white font-mono text-sm transition-colors duration-150 hover:bg-gray-50"
113+ style = { { marginLeft : leftMargin } }
60114 >
61- < div className = "flex flex-row items-center gap-x-4" >
62- < div className = "dark:bg-polar-700 flex flex-row items-center justify-center rounded-sm border border-gray-200 bg-gray-100 p-1 dark:border-white/5" >
63- { isExpanded ? (
64- < KeyboardArrowDownOutlined fontSize = "inherit" />
65- ) : (
66- < KeyboardArrowRightOutlined fontSize = "inherit" />
67- ) }
68- </ div >
115+ < div
116+ onClick = { handleToggleExpand }
117+ className = "flex cursor-pointer flex-row items-center justify-between p-3 select-none"
118+ >
69119 < div className = "flex flex-row items-center gap-x-4" >
70- < span className = "text-xs" > { eventDisplayName } </ span >
71- < EventSourceBadge source = { event . source } />
120+ < div className = "dark:bg-polar-700 flex flex-row items-center justify-center rounded-sm border border-gray-200 bg-gray-100 p-1 dark:border-white/5" >
121+ { isExpanded ? (
122+ < KeyboardArrowDownOutlined fontSize = "inherit" />
123+ ) : (
124+ < KeyboardArrowRightOutlined fontSize = "inherit" />
125+ ) }
126+ </ div >
127+ < div className = "flex flex-row items-center gap-x-4" >
128+ < span className = "text-xs" > { eventDisplayName } </ span >
129+ < EventSourceBadge source = { event . source } />
130+ { hasChildren && (
131+ < span className = "dark:text-polar-500 text-xxs text-gray-500" >
132+ { children . length } { ' ' }
133+ { children . length === 1 ? 'child' : 'children' }
134+ </ span >
135+ ) }
136+ </ div >
137+ < span className = "dark:text-polar-500 text-xs text-gray-500 capitalize" >
138+ { formattedTimestamp }
139+ </ span >
72140 </ div >
73- < span className = "dark:text-polar-500 text-xs text-gray-500 capitalize" >
74- { formattedTimestamp }
75- </ span >
76- </ div >
77- < div className = "flex flex-row items-center gap-x-6" >
78- { eventCostBadge }
79- < Tooltip >
80- < TooltipTrigger >
81- < Link
82- href = { `/dashboard/${ organization . slug } /customers?customerId=${ event . customer ?. id } &query=${ event . customer ?. email } ` }
83- className = "flex items-center gap-x-3"
84- onClick = { ( e ) => {
85- e . stopPropagation ( )
86- } }
87- >
88- < Avatar
89- className = "text-xxs h-6 w-6 font-sans"
90- name = { event . customer ?. name ?? event . customer ?. email ?? '—' }
91- avatar_url = { event . customer ?. avatar_url ?? null }
92- />
93- </ Link >
94- </ TooltipTrigger >
95- < TooltipContent side = "top" align = "end" >
96- < div className = "flex flex-row items-center gap-x-2 font-sans" >
97- < Avatar
98- className = "text-xxs h-8 w-8 font-sans"
99- name = { event . customer ?. name ?? event . customer ?. email ?? '—' }
100- avatar_url = { event . customer ?. avatar_url ?? null }
101- />
102- < div className = "flex flex-col" >
103- < span className = "text-xs" > { event . customer ?. name ?? '—' } </ span >
104- < span className = "dark:text-polar-500 text-xxs font-mono text-gray-500" >
105- { event . customer ?. email }
106- </ span >
141+ < div className = "flex flex-row items-center gap-x-6" >
142+ { eventCostBadge }
143+ < Tooltip >
144+ < TooltipTrigger >
145+ < Link
146+ href = { `/dashboard/${ organization . slug } /customers?customerId=${ event . customer ?. id } &query=${ event . customer ?. email } ` }
147+ className = "flex items-center gap-x-3"
148+ onClick = { ( e ) => {
149+ e . stopPropagation ( )
150+ } }
151+ >
152+ < Avatar
153+ className = "text-xxs h-6 w-6 font-sans"
154+ name = { event . customer ?. name ?? event . customer ?. email ?? '—' }
155+ avatar_url = { event . customer ?. avatar_url ?? null }
156+ />
157+ </ Link >
158+ </ TooltipTrigger >
159+ < TooltipContent side = "top" align = "end" >
160+ < div className = "flex flex-row items-center gap-x-2 font-sans" >
161+ < Avatar
162+ className = "text-xxs h-8 w-8 font-sans"
163+ name = { event . customer ?. name ?? event . customer ?. email ?? '—' }
164+ avatar_url = { event . customer ?. avatar_url ?? null }
165+ />
166+ < div className = "flex flex-col" >
167+ < span className = "text-xs" >
168+ { event . customer ?. name ?? '—' }
169+ </ span >
170+ < span className = "dark:text-polar-500 text-xxs font-mono text-gray-500" >
171+ { event . customer ?. email }
172+ </ span >
173+ </ div >
107174 </ div >
108- </ div >
109- </ TooltipContent >
110- </ Tooltip >
175+ </ TooltipContent >
176+ </ Tooltip >
177+ </ div >
111178 </ div >
179+ { isExpanded ? eventCard : null }
180+ { isExpanded ? < EventCustomer event = { event } /> : null }
112181 </ div >
113- { isExpanded ? eventCard : null }
114- { isExpanded ? < EventCustomer event = { event } /> : null }
182+ { hasChildren && isExpanded && (
183+ < div className = "flex flex-col gap-y-2" >
184+ { children . map ( ( childHierarchical ) => (
185+ < EventRow
186+ key = { childHierarchical . event . id }
187+ event = { childHierarchical . event }
188+ organization = { organization }
189+ depth = { depth + 1 }
190+ children = { childHierarchical . children }
191+ childrenLimit = { childrenLimit }
192+ />
193+ ) ) }
194+ { hasMoreChildren && (
195+ < div
196+ className = "flex"
197+ style = { {
198+ marginLeft : depth > 0 ? `${ ( depth + 1 ) * 32 } px` : '32px' ,
199+ } }
200+ >
201+ < Button
202+ size = "sm"
203+ variant = "secondary"
204+ onClick = { loadMoreChildren }
205+ loading = { isLoadingMoreChildren }
206+ disabled = { isLoadingMoreChildren }
207+ >
208+ Show more
209+ </ Button >
210+ </ div >
211+ ) }
212+ </ div >
213+ ) }
115214 </ div >
116215 )
117216}
@@ -120,14 +219,29 @@ export const Events = ({
120219 events,
121220 organization,
122221} : {
123- events : schemas [ 'Event' ] [ ]
222+ events : schemas [ 'HierarchicalEvent' ] [ ] | schemas [ ' Event'] [ ]
124223 organization : schemas [ 'Organization' ]
125224} ) => {
225+ const isHierarchical = (
226+ event : schemas [ 'HierarchicalEvent' ] | schemas [ 'Event' ] ,
227+ ) : event is schemas [ 'HierarchicalEvent' ] => {
228+ return 'event' in event && 'children' in event
229+ }
230+
126231 return (
127232 < div className = "flex flex-col gap-y-2" >
128- { events . map ( ( event ) => (
129- < EventRow key = { event . id } event = { event } organization = { organization } />
130- ) ) }
233+ { events . map ( ( item ) =>
234+ isHierarchical ( item ) ? (
235+ < EventRow
236+ key = { item . event . id }
237+ event = { item . event }
238+ organization = { organization }
239+ children = { item . children }
240+ />
241+ ) : (
242+ < EventRow key = { item . id } event = { item } organization = { organization } />
243+ ) ,
244+ ) }
131245 </ div >
132246 )
133247}
0 commit comments