1
- import { useCallback , useMemo } from 'react' ;
1
+ import { Fragment , useCallback , useMemo , useState } from 'react' ;
2
2
import { browserHistory } from 'react-router' ;
3
3
import styled from '@emotion/styled' ;
4
4
5
+ import { Button } from 'sentry/components/button' ;
6
+ import Count from 'sentry/components/count' ;
7
+ import EmptyStateWarning from 'sentry/components/emptyStateWarning' ;
5
8
import * as Layout from 'sentry/components/layouts/thirds' ;
9
+ import LoadingIndicator from 'sentry/components/loadingIndicator' ;
6
10
import { DatePageFilter } from 'sentry/components/organizations/datePageFilter' ;
7
11
import { EnvironmentPageFilter } from 'sentry/components/organizations/environmentPageFilter' ;
8
12
import PageFilterBar from 'sentry/components/organizations/pageFilterBar' ;
13
+ import { normalizeDateTimeParams } from 'sentry/components/organizations/pageFilters/parse' ;
9
14
import { ProjectPageFilter } from 'sentry/components/organizations/projectPageFilter' ;
10
- import type { CursorHandler } from 'sentry/components/pagination' ;
11
- import Pagination from 'sentry/components/pagination' ;
15
+ import Panel from 'sentry/components/panels/panel' ;
16
+ import PanelHeader from 'sentry/components/panels/panelHeader' ;
17
+ import PanelItem from 'sentry/components/panels/panelItem' ;
18
+ import PerformanceDuration from 'sentry/components/performanceDuration' ;
12
19
import type { SmartSearchBarProps } from 'sentry/components/smartSearchBar' ;
20
+ import { IconChevron } from 'sentry/icons/iconChevron' ;
21
+ import { t } from 'sentry/locale' ;
13
22
import { space } from 'sentry/styles/space' ;
14
- import { defined } from 'sentry/utils' ;
23
+ import type { PageFilters } from 'sentry/types' ;
24
+ import { useApiQuery } from 'sentry/utils/queryClient' ;
15
25
import { decodeInteger , decodeScalar } from 'sentry/utils/queryString' ;
16
- import { MutableSearch } from 'sentry/utils/tokenizeSearch' ;
17
26
import { useLocation } from 'sentry/utils/useLocation' ;
18
- import { useIndexedSpans } from 'sentry/views/starfish/queries/useIndexedSpans' ;
27
+ import useOrganization from 'sentry/utils/useOrganization' ;
28
+ import usePageFilters from 'sentry/utils/usePageFilters' ;
19
29
20
- import { fields } from './data' ;
21
- import { TraceRow } from './traceRow' ;
30
+ import { ProjectRenderer , SpanIdRenderer , TraceIdRenderer } from './fieldRenderers' ;
22
31
import { TracesSearchBar } from './tracesSearchBar' ;
23
32
24
33
const DEFAULT_PER_PAGE = 20 ;
25
34
35
+ const FIELDS = [
36
+ 'project' ,
37
+ 'transaction.id' ,
38
+ 'id' ,
39
+ 'timestamp' ,
40
+ 'span.op' ,
41
+ 'span.description' ,
42
+ 'span.duration' ,
43
+ ] ;
44
+ type Field = ( typeof FIELDS ) [ number ] ;
45
+
26
46
export function Content ( ) {
27
47
const location = useLocation ( ) ;
28
48
@@ -48,43 +68,16 @@ export function Content() {
48
68
[ location ]
49
69
) ;
50
70
51
- const handleCursor : CursorHandler = useCallback ( ( newCursor , pathname , newQuery ) => {
52
- browserHistory . push ( {
53
- pathname,
54
- query : { ...newQuery , cursor : newCursor } ,
55
- } ) ;
56
- } , [ ] ) ;
57
-
58
- const filters = useMemo ( ( ) => new MutableSearch ( query ?? '' ) . filters , [ query ] ) ;
59
-
60
- const spansQuery = useIndexedSpans ( {
61
- fields,
62
- filters,
71
+ const traces = useTraces < Field > ( {
72
+ fields : FIELDS ,
63
73
limit,
64
- sorts : [ ] ,
65
- referrer : 'api.trace-explorer.table' ,
74
+ query,
66
75
} ) ;
67
76
68
- const traces = useMemo ( ( ) => {
69
- const data = ( spansQuery . data ?? [ ] ) . reduce ( ( acc , span ) => {
70
- const traceId = span . trace ;
71
- if ( ! defined ( traceId ) ) {
72
- // TODO: warn missing trace id
73
- return acc ;
74
- }
75
-
76
- let spansList = acc . get ( traceId ) ;
77
- if ( ! defined ( spansList ) ) {
78
- spansList = [ ] ;
79
- acc . set ( traceId , spansList ) ;
80
- }
81
-
82
- spansList . push ( span ) ;
83
- return acc ;
84
- } , new Map ( ) ) ;
85
-
86
- return Array . from ( data ) ;
87
- } , [ spansQuery . data ] ) ;
77
+ const isLoading = traces . isFetching ;
78
+ const isError = ! isLoading && traces . isError ;
79
+ const isEmpty = ! isLoading && ! isError && ( traces ?. data ?. data ?. length ?? 0 ) === 0 ;
80
+ const data = ! isLoading && ! isError ? traces ?. data ?. data : undefined ;
88
81
89
82
return (
90
83
< LayoutMain fullWidth >
@@ -94,20 +87,250 @@ export function Content() {
94
87
< DatePageFilter />
95
88
</ PageFilterBar >
96
89
< TracesSearchBar query = { query } handleSearch = { handleSearch } />
97
- { traces . map ( ( [ traceId , spans ] ) => (
98
- < TraceRow key = { traceId } traceId = { traceId } spans = { spans } />
99
- ) ) }
100
- < StyledPagination pageLinks = { spansQuery . pageLinks } onCursor = { handleCursor } />
90
+ < StyledPanel >
91
+ < TracePanelContent >
92
+ < StyledPanelHeader align = "right" lightText >
93
+ { t ( 'Trace ID' ) }
94
+ </ StyledPanelHeader >
95
+ < StyledPanelHeader align = "left" lightText >
96
+ { t ( 'Trace Root Name' ) }
97
+ </ StyledPanelHeader >
98
+ < StyledPanelHeader align = "right" lightText >
99
+ { t ( 'Spans' ) }
100
+ </ StyledPanelHeader >
101
+ < StyledPanelHeader align = "right" lightText >
102
+ { t ( 'Breakdown' ) }
103
+ </ StyledPanelHeader >
104
+ < StyledPanelHeader align = "right" lightText >
105
+ { t ( 'Trace Duration' ) }
106
+ </ StyledPanelHeader >
107
+ < StyledPanelHeader align = "right" lightText >
108
+ { t ( 'Issues' ) }
109
+ </ StyledPanelHeader >
110
+ { isLoading && (
111
+ < StyledPanelItem span = { 6 } >
112
+ < LoadingIndicator />
113
+ </ StyledPanelItem >
114
+ ) }
115
+ { isError && ( // TODO: need an error state
116
+ < StyledPanelItem span = { 6 } >
117
+ < EmptyStateWarning withIcon />
118
+ </ StyledPanelItem >
119
+ ) }
120
+ { isEmpty && (
121
+ < StyledPanelItem span = { 6 } >
122
+ < EmptyStateWarning withIcon />
123
+ </ StyledPanelItem >
124
+ ) }
125
+ { data ?. map ( trace => < TraceRow key = { trace . trace } trace = { trace } /> ) }
126
+ </ TracePanelContent >
127
+ </ StyledPanel >
101
128
</ LayoutMain >
102
129
) ;
103
130
}
104
131
132
+ function TraceRow ( { trace} : { trace : TraceResult < Field > } ) {
133
+ const [ expanded , setExpanded ] = useState < boolean > ( false ) ;
134
+ return (
135
+ < Fragment >
136
+ < StyledPanelItem align = "center" center >
137
+ < Button
138
+ icon = { < IconChevron size = "xs" direction = { expanded ? 'down' : 'right' } /> }
139
+ aria-label = { t ( 'Toggle trace details' ) }
140
+ aria-expanded = { expanded }
141
+ size = "zero"
142
+ borderless
143
+ onClick = { ( ) => setExpanded ( e => ! e ) }
144
+ />
145
+ < TraceIdRenderer traceId = { trace . trace } timestamp = { trace . spans [ 0 ] . timestamp } />
146
+ </ StyledPanelItem >
147
+ < StyledPanelItem align = "left" >
148
+ { trace . name ? (
149
+ trace . name
150
+ ) : (
151
+ < EmptyValueContainer > { t ( 'No Name Available' ) } </ EmptyValueContainer >
152
+ ) }
153
+ </ StyledPanelItem >
154
+ < StyledPanelItem align = "right" >
155
+ < Count value = { trace . numSpans } />
156
+ </ StyledPanelItem >
157
+ < StyledPanelItem align = "right" >
158
+ < EmptyValueContainer > { '\u2014' } </ EmptyValueContainer >
159
+ </ StyledPanelItem >
160
+ < StyledPanelItem align = "right" >
161
+ < PerformanceDuration milliseconds = { trace . duration } abbreviation />
162
+ </ StyledPanelItem >
163
+ < StyledPanelItem align = "right" >
164
+ < EmptyValueContainer > { '\u2014' } </ EmptyValueContainer >
165
+ </ StyledPanelItem >
166
+ { expanded && (
167
+ < StyledPanelItem span = { 6 } >
168
+ < StyledPanel >
169
+ < SpanPanelContent >
170
+ < StyledPanelHeader align = "left" lightText >
171
+ { t ( 'Span ID' ) }
172
+ </ StyledPanelHeader >
173
+ < StyledPanelHeader align = "left" lightText >
174
+ { t ( 'Span Description' ) }
175
+ </ StyledPanelHeader >
176
+ < StyledPanelHeader align = "right" lightText />
177
+ < StyledPanelHeader align = "right" lightText >
178
+ { t ( 'Span Duration' ) }
179
+ </ StyledPanelHeader >
180
+ < StyledPanelHeader align = "right" lightText >
181
+ { t ( 'Issues' ) }
182
+ </ StyledPanelHeader >
183
+ { trace . spans . map ( span => (
184
+ < SpanRow key = { span . id } span = { span } trace = { trace . trace } />
185
+ ) ) }
186
+ </ SpanPanelContent >
187
+ </ StyledPanel >
188
+ </ StyledPanelItem >
189
+ ) }
190
+ </ Fragment >
191
+ ) ;
192
+ }
193
+
194
+ function SpanRow ( { span, trace} : { span : SpanResult < Field > ; trace : string } ) {
195
+ return (
196
+ < Fragment >
197
+ < StyledPanelItem align = "right" >
198
+ < SpanIdRenderer
199
+ projectSlug = { span . project }
200
+ transactionId = { span [ 'transaction.id' ] }
201
+ spanId = { span . id }
202
+ trace = { trace }
203
+ timestamp = { span . timestamp }
204
+ />
205
+ </ StyledPanelItem >
206
+ < StyledPanelItem align = "left" >
207
+ < Description >
208
+ < ProjectRenderer projectSlug = { span . project } hideName />
209
+ < strong > { span [ 'span.op' ] } </ strong >
210
+ < em > { '\u2014' } </ em >
211
+ { span [ 'span.description' ] }
212
+ </ Description >
213
+ </ StyledPanelItem >
214
+ < StyledPanelItem align = "right" >
215
+ < EmptyValueContainer > { '\u2014' } </ EmptyValueContainer >
216
+ </ StyledPanelItem >
217
+ < StyledPanelItem align = "right" >
218
+ < PerformanceDuration milliseconds = { span [ 'span.duration' ] } abbreviation />
219
+ </ StyledPanelItem >
220
+ < StyledPanelItem align = "right" >
221
+ < EmptyValueContainer > { '\u2014' } </ EmptyValueContainer >
222
+ </ StyledPanelItem >
223
+ </ Fragment >
224
+ ) ;
225
+ }
226
+
227
+ type SpanResult < F extends string > = Record < F , any > ;
228
+
229
+ interface TraceResult < F extends string > {
230
+ duration : number ;
231
+ name : string | null ;
232
+ numSpans : number ;
233
+ spans : SpanResult < F > [ ] ;
234
+ trace : string ;
235
+ }
236
+
237
+ interface TraceResults < F extends string > {
238
+ data : TraceResult < F > [ ] ;
239
+ meta : any ;
240
+ }
241
+
242
+ interface UseTracesOptions < F extends string > {
243
+ fields : F [ ] ;
244
+ datetime ?: PageFilters [ 'datetime' ] ;
245
+ enabled ?: boolean ;
246
+ limit ?: number ;
247
+ query ?: string ;
248
+ }
249
+
250
+ function useTraces < F extends string > ( {
251
+ fields,
252
+ datetime,
253
+ enabled,
254
+ limit,
255
+ query,
256
+ } : UseTracesOptions < F > ) {
257
+ const organization = useOrganization ( ) ;
258
+ const { selection} = usePageFilters ( ) ;
259
+
260
+ const path = `/organizations/${ organization . slug } /traces/` ;
261
+
262
+ const endpointOptions = {
263
+ query : {
264
+ project : selection . projects ,
265
+ environment : selection . environments ,
266
+ ...( datetime ?? normalizeDateTimeParams ( selection . datetime ) ) ,
267
+ field : fields ,
268
+ query,
269
+ per_page : limit ,
270
+ maxSpansPerTrace : 10 ,
271
+ } ,
272
+ } ;
273
+
274
+ return useApiQuery < TraceResults < F > > ( [ path , endpointOptions ] , {
275
+ staleTime : 0 ,
276
+ refetchOnWindowFocus : false ,
277
+ retry : false ,
278
+ enabled,
279
+ } ) ;
280
+ }
281
+
105
282
const LayoutMain = styled ( Layout . Main ) `
106
283
display: flex;
107
284
flex-direction: column;
108
285
gap: ${ space ( 2 ) } ;
109
286
` ;
110
287
111
- const StyledPagination = styled ( Pagination ) `
112
- margin: 0px;
288
+ const StyledPanel = styled ( Panel ) `
289
+ margin-bottom: 0px;
290
+ ` ;
291
+
292
+ const TracePanelContent = styled ( 'div' ) `
293
+ width: 100%;
294
+ display: grid;
295
+ grid-template-columns: repeat(1, min-content) auto repeat(4, min-content);
296
+ ` ;
297
+
298
+ const SpanPanelContent = styled ( 'div' ) `
299
+ width: 100%;
300
+ display: grid;
301
+ grid-template-columns: repeat(1, min-content) auto repeat(3, min-content);
302
+ ` ;
303
+
304
+ const StyledPanelHeader = styled ( PanelHeader ) < { align : 'left' | 'right' } > `
305
+ white-space: nowrap;
306
+ justify-content: ${ p => ( p . align === 'left' ? 'flex-start' : 'flex-end' ) } ;
307
+ padding: ${ space ( 2 ) } ${ space ( 1 ) } ;
308
+ ` ;
309
+
310
+ const Description = styled ( 'div' ) `
311
+ ${ p => p . theme . overflowEllipsis } ;
312
+ display: flex;
313
+ flex-direction: row;
314
+ align-items: center;
315
+ gap: ${ space ( 1 ) } ;
316
+ ` ;
317
+
318
+ const StyledPanelItem = styled ( PanelItem ) < {
319
+ align ?: 'left' | 'center' | 'right' ;
320
+ span ?: number ;
321
+ } > `
322
+ padding: ${ space ( 1 ) } ;
323
+ ${ p => p . theme . overflowEllipsis } ;
324
+ ${ p =>
325
+ p . align === 'center'
326
+ ? `
327
+ justify-content: space-around;`
328
+ : p . align === 'left' || p . align === 'right'
329
+ ? `text-align: ${ p . align } ;`
330
+ : undefined }
331
+ ${ p => p . span && `grid-column: auto / span ${ p . span } ` }
332
+ ` ;
333
+
334
+ const EmptyValueContainer = styled ( 'span' ) `
335
+ color: ${ p => p . theme . gray300 } ;
113
336
` ;
0 commit comments