11import { useCallback , useMemo , useState } from 'react' ;
22import { sq } from 'date-fns/locale' ;
33import ms from 'ms' ;
4+ import { parseAsString , useQueryState } from 'nuqs' ;
45import { useForm } from 'react-hook-form' ;
56import { tcFromSource } from '@hyperdx/common-utils/dist/metadata' ;
67import {
@@ -13,8 +14,14 @@ import { useDebouncedValue } from '@mantine/hooks';
1314import { SQLInlineEditorControlled } from '@/components/SQLInlineEditor' ;
1415import WhereLanguageControlled from '@/components/WhereLanguageControlled' ;
1516import SearchInputV2 from '@/SearchInputV2' ;
17+ import { useSource } from '@/source' ;
1618import { formatAttributeClause } from '@/utils' ;
1719
20+ import DBRowSidePanel from './DBRowSidePanel' ;
21+ import {
22+ BreadcrumbNavigationCallback ,
23+ BreadcrumbPath ,
24+ } from './DBRowSidePanelHeader' ;
1825import { DBSqlRowTable } from './DBRowTable' ;
1926
2027enum ContextBy {
@@ -31,13 +38,42 @@ interface ContextSubpanelProps {
3138 dbSqlRowTableConfig : ChartConfigWithDateRange | undefined ;
3239 rowData : Record < string , any > ;
3340 rowId : string | undefined ;
41+ breadcrumbPath ?: BreadcrumbPath ;
42+ onBreadcrumbClick ?: BreadcrumbNavigationCallback ;
43+ }
44+
45+ // Custom hook to manage nested panel state
46+ function useNestedPanelState ( isNested : boolean ) {
47+ // Query state (URL-based) for root level
48+ const queryState = {
49+ contextRowId : useQueryState ( 'contextRowId' , parseAsString ) ,
50+ contextRowSource : useQueryState ( 'contextRowSource' , parseAsString ) ,
51+ } ;
52+
53+ // Local state for nested levels
54+ const localState = {
55+ contextRowId : useState < string | null > ( null ) ,
56+ contextRowSource : useState < string | null > ( null ) ,
57+ } ;
58+
59+ // Choose which state to use based on nesting level
60+ const activeState = isNested ? localState : queryState ;
61+
62+ return {
63+ contextRowId : activeState . contextRowId [ 0 ] ,
64+ contextRowSource : activeState . contextRowSource [ 0 ] ,
65+ setContextRowId : activeState . contextRowId [ 1 ] ,
66+ setContextRowSource : activeState . contextRowSource [ 1 ] ,
67+ } ;
3468}
3569
3670export default function ContextSubpanel ( {
3771 source,
3872 dbSqlRowTableConfig,
3973 rowData,
4074 rowId,
75+ breadcrumbPath = [ ] ,
76+ onBreadcrumbClick,
4177} : ContextSubpanelProps ) {
4278 const QUERY_KEY_PREFIX = 'context' ;
4379 const { Timestamp : origTimestamp } = rowData ;
@@ -55,6 +91,33 @@ export default function ContextSubpanel({
5591 const formWhere = watch ( 'where' ) ;
5692 const [ debouncedWhere ] = useDebouncedValue ( formWhere , 1000 ) ;
5793
94+ // State management for nested panels
95+ const isNested = breadcrumbPath . length > 0 ;
96+
97+ const {
98+ contextRowId,
99+ contextRowSource,
100+ setContextRowId,
101+ setContextRowSource,
102+ } = useNestedPanelState ( isNested ) ;
103+
104+ const { data : contextRowSidePanelSource } = useSource ( {
105+ id : contextRowSource || '' ,
106+ } ) ;
107+
108+ const handleContextSidePanelClose = useCallback ( ( ) => {
109+ setContextRowId ( null ) ;
110+ setContextRowSource ( null ) ;
111+ } , [ setContextRowId , setContextRowSource ] ) ;
112+
113+ const handleRowExpandClick = useCallback (
114+ ( rowWhere : string ) => {
115+ setContextRowId ( rowWhere ) ;
116+ setContextRowSource ( source . id ) ;
117+ } ,
118+ [ source . id , setContextRowId , setContextRowSource ] ,
119+ ) ;
120+
58121 const date = useMemo ( ( ) => new Date ( origTimestamp ) , [ origTimestamp ] ) ;
59122
60123 const newDateRange = useMemo (
@@ -176,88 +239,107 @@ export default function ContextSubpanel({
176239 ] ) ;
177240
178241 return (
179- config && (
180- < Flex direction = "column" mih = "0px" style = { { flexGrow : 1 } } >
181- < Group justify = "space-between" p = "sm" >
182- < SegmentedControl
183- bg = "dark.7"
184- color = "dark.5"
185- size = "xs"
186- data = { generateSegmentedControlData ( ) }
187- value = { contextBy }
188- onChange = { v => setContextBy ( v as ContextBy ) }
189- />
190- { contextBy === ContextBy . Custom && (
191- < WhereLanguageControlled
192- name = "whereLanguage"
193- control = { control }
194- sqlInput = {
195- originalLanguage === 'lucene' ? null : (
196- < SQLInlineEditorControlled
197- tableConnections = { tcFromSource ( source ) }
198- control = { control }
199- name = "where"
200- placeholder = "SQL WHERE clause (ex. column = 'foo')"
201- language = "sql"
202- enableHotkey
203- size = "sm"
204- />
205- )
206- }
207- luceneInput = {
208- originalLanguage === 'sql' ? null : (
209- < SearchInputV2
210- tableConnections = { tcFromSource ( source ) }
211- control = { control }
212- name = "where"
213- language = "lucene"
214- placeholder = "Lucene where clause (ex. column:value)"
215- enableHotkey
216- size = "sm"
217- />
218- )
219- }
242+ < >
243+ { config && (
244+ < Flex direction = "column" mih = "0px" style = { { flexGrow : 1 } } >
245+ < Group justify = "space-between" p = "sm" >
246+ < SegmentedControl
247+ bg = "dark.7"
248+ color = "dark.5"
249+ size = "xs"
250+ data = { generateSegmentedControlData ( ) }
251+ value = { contextBy }
252+ onChange = { v => setContextBy ( v as ContextBy ) }
220253 />
221- ) }
222- < SegmentedControl
223- bg = "dark.7"
224- color = "dark.5"
225- size = "xs"
226- data = { [
227- { label : '100ms' , value : ms ( '100ms' ) . toString ( ) } ,
228- { label : '500ms' , value : ms ( '500ms' ) . toString ( ) } ,
229- { label : '1s' , value : ms ( '1s' ) . toString ( ) } ,
230- { label : '5s' , value : ms ( '5s' ) . toString ( ) } ,
231- { label : '30s' , value : ms ( '30s' ) . toString ( ) } ,
232- { label : '1m' , value : ms ( '1m' ) . toString ( ) } ,
233- { label : '5m' , value : ms ( '5m' ) . toString ( ) } ,
234- { label : '15m' , value : ms ( '15m' ) . toString ( ) } ,
235- ] }
236- value = { range . toString ( ) }
237- onChange = { value => setRange ( Number ( value ) ) }
238- />
239- </ Group >
240- < Group p = "sm" >
241- < div >
242- { contextBy !== ContextBy . All && (
254+ { contextBy === ContextBy . Custom && (
255+ < WhereLanguageControlled
256+ name = "whereLanguage"
257+ control = { control }
258+ sqlInput = {
259+ originalLanguage === 'lucene' ? null : (
260+ < SQLInlineEditorControlled
261+ tableConnections = { tcFromSource ( source ) }
262+ control = { control }
263+ name = "where"
264+ placeholder = "SQL WHERE clause (ex. column = 'foo')"
265+ language = "sql"
266+ enableHotkey
267+ size = "sm"
268+ />
269+ )
270+ }
271+ luceneInput = {
272+ originalLanguage === 'sql' ? null : (
273+ < SearchInputV2
274+ tableConnections = { tcFromSource ( source ) }
275+ control = { control }
276+ name = "where"
277+ language = "lucene"
278+ placeholder = "Lucene where clause (ex. column:value)"
279+ enableHotkey
280+ size = "sm"
281+ />
282+ )
283+ }
284+ />
285+ ) }
286+ < SegmentedControl
287+ bg = "dark.7"
288+ color = "dark.5"
289+ size = "xs"
290+ data = { [
291+ { label : '100ms' , value : ms ( '100ms' ) . toString ( ) } ,
292+ { label : '500ms' , value : ms ( '500ms' ) . toString ( ) } ,
293+ { label : '1s' , value : ms ( '1s' ) . toString ( ) } ,
294+ { label : '5s' , value : ms ( '5s' ) . toString ( ) } ,
295+ { label : '30s' , value : ms ( '30s' ) . toString ( ) } ,
296+ { label : '1m' , value : ms ( '1m' ) . toString ( ) } ,
297+ { label : '5m' , value : ms ( '5m' ) . toString ( ) } ,
298+ { label : '15m' , value : ms ( '15m' ) . toString ( ) } ,
299+ ] }
300+ value = { range . toString ( ) }
301+ onChange = { value => setRange ( Number ( value ) ) }
302+ />
303+ </ Group >
304+ < Group p = "sm" >
305+ < div >
306+ { contextBy !== ContextBy . All && (
307+ < Badge size = "md" variant = "default" >
308+ { contextBy } :{ CONTEXT_MAPPING [ contextBy ] . value }
309+ </ Badge >
310+ ) }
243311 < Badge size = "md" variant = "default" >
244- { contextBy } : { CONTEXT_MAPPING [ contextBy ] . value }
312+ Time range: ± { ms ( range / 2 ) }
245313 </ Badge >
246- ) }
247- < Badge size = "md" variant = "default" >
248- Time range: ±{ ms ( range / 2 ) }
249- </ Badge >
314+ </ div >
315+ </ Group >
316+ < div style = { { height : '100%' , overflow : 'auto' } } >
317+ < DBSqlRowTable
318+ highlightedLineId = { rowId }
319+ isLive = { false }
320+ config = { config }
321+ queryKeyPrefix = { QUERY_KEY_PREFIX }
322+ onRowExpandClick = { handleRowExpandClick }
323+ />
250324 </ div >
251- </ Group >
252- < div style = { { height : '100%' , overflow : 'auto' } } >
253- < DBSqlRowTable
254- highlightedLineId = { rowId }
255- isLive = { false }
256- config = { config }
257- queryKeyPrefix = { QUERY_KEY_PREFIX }
258- />
259- </ div >
260- </ Flex >
261- )
325+ </ Flex >
326+ ) }
327+ { contextRowId && contextRowSidePanelSource && (
328+ < DBRowSidePanel
329+ source = { contextRowSidePanelSource }
330+ rowId = { contextRowId }
331+ onClose = { handleContextSidePanelClose }
332+ isNestedPanel = { true }
333+ breadcrumbPath = { [
334+ ...breadcrumbPath ,
335+ {
336+ label : `Surrounding Context` ,
337+ rowData,
338+ } ,
339+ ] }
340+ onBreadcrumbClick = { onBreadcrumbClick }
341+ />
342+ ) }
343+ </ >
262344 ) ;
263345}
0 commit comments