Skip to content

Commit 4a2bc40

Browse files
committed
feat(events): Add hierarchical event listing
Including show more button for children
1 parent 2a6f036 commit 4a2bc40

File tree

4 files changed

+265
-80
lines changed

4 files changed

+265
-80
lines changed

clients/apps/web/src/app/(main)/dashboard/[organization]/(header)/usage-billing/events/ClientPage.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import DateRangePicker from '@/components/Metrics/DateRangePicker'
1010
import { Modal } from '@/components/Modal'
1111
import { useModal } from '@/components/Modal/useModal'
1212
import Pagination from '@/components/Pagination/Pagination'
13-
import { useEventNames, useEvents } from '@/hooks/queries/events'
13+
import { useEventNames, useHierarchicalEvents } from '@/hooks/queries/events'
1414
import useDebounce from '@/utils/useDebounce'
1515
import AddOutlined from '@mui/icons-material/AddOutlined'
1616
import RefreshOutlined from '@mui/icons-material/RefreshOutlined'
@@ -123,7 +123,7 @@ const ClientPage: React.FC<ClientPageProps> = ({ organization }) => {
123123
const debouncedMetadata = useDebounce(metadata, 500)
124124

125125
const eventParameters = useMemo(():
126-
| operations['events:list']['parameters']['query']
126+
| operations['events:list_hierarchical']['parameters']['query']
127127
| undefined => {
128128
return {
129129
name:
@@ -150,7 +150,10 @@ const ClientPage: React.FC<ClientPageProps> = ({ organization }) => {
150150
debouncedMetadata,
151151
])
152152

153-
const { data: events } = useEvents(organization.id, eventParameters)
153+
const { data: events } = useHierarchicalEvents(
154+
organization.id,
155+
eventParameters,
156+
)
154157

155158
const searchParams = useSearchParams()
156159

Lines changed: 173 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,83 @@
1+
import { api } from '@/utils/client'
12
import KeyboardArrowDownOutlined from '@mui/icons-material/KeyboardArrowDownOutlined'
23
import KeyboardArrowRightOutlined from '@mui/icons-material/KeyboardArrowRightOutlined'
3-
import { schemas } from '@polar-sh/client'
4+
import { schemas, unwrap } from '@polar-sh/client'
45
import Avatar from '@polar-sh/ui/components/atoms/Avatar'
6+
import Button from '@polar-sh/ui/components/atoms/Button'
57
import {
68
Tooltip,
79
TooltipContent,
810
TooltipTrigger,
911
} from '@polar-sh/ui/components/ui/tooltip'
1012
import Link from 'next/link'
11-
import { useMemo, useState } from 'react'
13+
import { useCallback, useEffect, useMemo, useState } from 'react'
1214
import { EventCustomer } from './EventCustomer'
1315
import { EventSourceBadge } from './EventSourceBadge'
1416
import { useEventCard, useEventCostBadge, useEventDisplayName } from './utils'
1517

1618
const 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

Comments
 (0)