1- import { useMemo , useState , useEffect , useRef , useCallback } from 'react' ;
1+ import { useMemo , useState , useEffect , useRef , useCallback , useLayoutEffect } from 'react' ;
22import { AgGridReact } from 'ag-grid-react' ;
33import type {
44 ColDef ,
@@ -14,11 +14,12 @@ import {
1414 themeQuartz ,
1515 ScrollApiModule ,
1616 TooltipModule ,
17+ RowApiModule ,
1718} from 'ag-grid-community' ;
1819import { Ban , Lock , LockOpen , Moon , PanelBottom , PanelRight , Sun } from 'lucide-react' ;
1920import { MSG_TYPE , PORT_NAME } from '../../../common/constants' ;
2021import ResizablePanel from './ResizablePanel' ;
21- import type { IpcEventDataIndexed , MessagePanel } from '../../../types/shared' ;
22+ import type { IpcEventDataIndexed , MessagePanel , SerialNumber , UUID } from '../../../types/shared' ;
2223import DirectionBadge from './DirectionBadge' ;
2324import formatTimestamp from '../utils/formatTimestamp' ;
2425import DetailPanel from './DetailPanel' ;
@@ -29,6 +30,7 @@ ModuleRegistry.registerModules([
2930 CellStyleModule ,
3031 ScrollApiModule ,
3132 TooltipModule ,
33+ RowApiModule ,
3234] ) ;
3335import { useDevtronContext } from '../context/context' ;
3436
@@ -41,6 +43,7 @@ function Panel() {
4143 const [ selectedRow , setSelectedRow ] = useState < IpcEventDataIndexed | null > ( null ) ;
4244 const [ showDetailPanel , setShowDetailPanel ] = useState < boolean > ( false ) ;
4345 const [ isPortReady , setIsPortReady ] = useState < boolean > ( false ) ;
46+ const [ shouldScrollToBottom , setShouldScrollToBottom ] = useState < boolean > ( false ) ;
4447
4548 const {
4649 theme,
@@ -51,10 +54,47 @@ function Panel() {
5154 setLockToBottom,
5255 } = useDevtronContext ( ) ;
5356
57+ /**
58+ * uuidMapRef stores the serial numbers of events that have a UUID.
59+ * If an event with the same UUID is received later on, we add a gotoSerialNumber property
60+ * to the previous event with the same UUID.
61+ * This allows us to jump between events that are related to each other.
62+ */
63+ const uuidMapRef = useRef ( new Map < UUID , SerialNumber > ( ) ) ;
5464 const lockToBottomRef = useRef ( lockToBottom ) ;
5565 const gridRef = useRef < AgGridReact < IpcEventDataIndexed > | null > ( null ) ;
5666 const portRef = useRef < chrome . runtime . Port | null > ( null ) ;
5767
68+ // scrollToRow uses zero-based indexing
69+ const scrollToRow = useCallback (
70+ ( row : number , position : 'top' | 'bottom' | 'middle' | null ) =>
71+ gridRef . current ?. api . ensureIndexVisible ( row , position ) ,
72+ [ ] ,
73+ ) ;
74+
75+ // go to a row, highlight it and open the detail panel (it uses serialNumber to identify the row)
76+ const gotoRow = useCallback (
77+ ( serialNumber : SerialNumber ) => {
78+ if ( ! gridRef . current || events . length === 0 ) return ;
79+
80+ const firstSerial = events [ 0 ] . serialNumber ;
81+ const index = serialNumber - firstSerial ; // convert to 0-based index
82+
83+ if ( index < 0 || index >= events . length ) return ;
84+
85+ scrollToRow ( index , 'bottom' ) ;
86+
87+ const rowNode = gridRef . current . api . getRowNode ( String ( serialNumber ) ) ;
88+
89+ if ( rowNode ) {
90+ rowNode . setSelected ( true ) ;
91+ setSelectedRow ( rowNode . data ?? null ) ;
92+ setShowDetailPanel ( true ) ;
93+ }
94+ } ,
95+ [ scrollToRow , events ] ,
96+ ) ;
97+
5898 const clearEvents = useCallback ( ( ) => {
5999 if ( isDev ) {
60100 setEvents ( [ ] ) ;
@@ -66,11 +106,22 @@ function Panel() {
66106 try {
67107 portRef . current . postMessage ( { type : MSG_TYPE . CLEAR_EVENTS } satisfies MessagePanel ) ;
68108 setEvents ( [ ] ) ;
109+ uuidMapRef . current . clear ( ) ;
69110 } catch ( error ) {
70111 console . error ( 'Devtron - Error clearing events:' , error ) ;
71112 }
72113 } , [ isPortReady ] ) ;
73114
115+ // used to scroll to the bottom of the grid when a new event is added
116+ useLayoutEffect ( ( ) => {
117+ if ( shouldScrollToBottom && events . length > 0 ) {
118+ requestAnimationFrame ( ( ) => {
119+ scrollToRow ( events . length - 1 , 'bottom' ) ;
120+ } ) ;
121+ setShouldScrollToBottom ( false ) ;
122+ }
123+ } , [ events . length , scrollToRow , shouldScrollToBottom ] ) ;
124+
74125 useEffect ( ( ) => {
75126 // Update lockToBottomRef on first render
76127 const savedLockToBottom = localStorage . getItem ( 'lockToBottom' ) ;
@@ -99,14 +150,43 @@ function Panel() {
99150
100151 const handleOnMessage = ( message : MessagePanel ) : void => {
101152 if ( message . type === MSG_TYPE . RENDER_EVENT ) {
153+ const event = message . event ;
154+
102155 setEvents ( ( prev ) => {
103- const updated = [ ...prev , message . event ] . slice ( - MAX_EVENTS_TO_DISPLAY ) ;
156+ const updated = [ ...prev , event ] . slice ( - MAX_EVENTS_TO_DISPLAY ) ;
157+ // If the event with the same UUID already exists, we update it
158+ if ( event . uuid && uuidMapRef . current . has ( event . uuid ) ) {
159+ const oldEventSerialNumber = uuidMapRef . current . get ( event . uuid ) ! ;
160+ if (
161+ oldEventSerialNumber >= updated [ 0 ] . serialNumber &&
162+ oldEventSerialNumber < updated [ updated . length - 1 ] . serialNumber
163+ ) {
164+ const offset = updated [ 0 ] . serialNumber ;
165+
166+ const oldEventIndex = oldEventSerialNumber - offset ;
167+
168+ // update the old event to include a link to the new event
169+ updated [ oldEventIndex ] = {
170+ ...updated [ oldEventIndex ] ,
171+ gotoSerialNumber : event . serialNumber ,
172+ } ;
173+
174+ // add a link to the old event in the new event
175+ updated [ updated . length - 1 ] = {
176+ ...updated [ updated . length - 1 ] ,
177+ gotoSerialNumber : oldEventSerialNumber ,
178+ } ;
179+ }
180+
181+ uuidMapRef . current . delete ( event . uuid ) ;
182+ } else if ( event . uuid ) {
183+ // If a UUID is encountered for the first time, we store its serialNumber
184+ uuidMapRef . current . set ( event . uuid , event . serialNumber ) ;
185+ }
186+
104187 if ( lockToBottomRef . current ) {
105- requestAnimationFrame ( ( ) => {
106- requestAnimationFrame ( ( ) => {
107- gridRef . current ?. api . ensureIndexVisible ( updated . length - 1 , 'bottom' ) ;
108- } ) ;
109- } ) ;
188+ // If lockToBottom is true, scroll to the newly added event
189+ setShouldScrollToBottom ( true ) ;
110190 }
111191 return updated ;
112192 } ) ;
@@ -130,7 +210,7 @@ function Panel() {
130210 setIsPortReady ( false ) ;
131211 if ( port ) port . disconnect ( ) ;
132212 } ;
133- } , [ setLockToBottom ] ) ;
213+ } , [ scrollToRow , setLockToBottom ] ) ;
134214
135215 const columnDefs : ColDef < IpcEventDataIndexed > [ ] = useMemo (
136216 ( ) => [
@@ -167,8 +247,17 @@ function Panel() {
167247 flex : 1 ,
168248 cellClass : 'font-roboto text-[13px] !p-1 h-full flex items-center' ,
169249 headerClass : '!h-6' ,
170- tooltipValueGetter : ( params ) => {
171- return params . value ; // or a custom string
250+ cellRenderer : ( params : ICellRendererParams < IpcEventDataIndexed > ) => {
251+ return (
252+ < div title = { params . value } >
253+ { params . value }
254+ { params . data ?. responseTime && (
255+ < span className = "ml-2 rounded bg-gray-200 px-1.5 py-0.5 text-xs dark:bg-charcoal-400" >
256+ Response
257+ </ span >
258+ ) }
259+ </ div >
260+ ) ;
172261 } ,
173262 } ,
174263 {
@@ -275,6 +364,7 @@ function Panel() {
275364 suppressScrollOnNewData = { true }
276365 rowData = { events }
277366 columnDefs = { columnDefs }
367+ getRowId = { ( params ) => String ( params . data . serialNumber ) }
278368 theme = { themeQuartz
279369 . withParams ( {
280370 cellFontFamily : 'roboto, sans-serif' ,
@@ -308,6 +398,7 @@ function Panel() {
308398 selectedRow = { selectedRow }
309399 onClose = { handleCloseDetailPanel }
310400 direction = { detailPanelPosition }
401+ gotoRow = { gotoRow }
311402 />
312403 </ ResizablePanel >
313404 </ div >
0 commit comments