1
- import { useMemo } from 'react' ;
1
+ import { Fragment , useMemo } from 'react' ;
2
+ import styled from '@emotion/styled' ;
2
3
import type { YAXisComponentOption } from 'echarts' ;
3
4
4
5
import { AreaChart } from 'sentry/components/charts/areaChart' ;
5
6
import ErrorPanel from 'sentry/components/charts/errorPanel' ;
7
+ import { CompactSelect } from 'sentry/components/core/compactSelect' ;
6
8
import { Flex } from 'sentry/components/core/layout' ;
9
+ import { Text } from 'sentry/components/core/text' ;
10
+ import LoadingIndicator from 'sentry/components/loadingIndicator' ;
7
11
import Placeholder from 'sentry/components/placeholder' ;
8
12
import { IconWarning } from 'sentry/icons' ;
9
- import { t } from 'sentry/locale' ;
13
+ import { t , tn } from 'sentry/locale' ;
10
14
import { space } from 'sentry/styles/space' ;
11
15
import type { DataCondition } from 'sentry/types/workflowEngine/dataConditions' ;
12
16
import type { MetricDetectorConfig } from 'sentry/types/workflowEngine/detectors' ;
13
17
import {
14
18
AlertRuleSensitivity ,
15
19
AlertRuleThresholdType ,
16
- TimePeriod ,
17
20
} from 'sentry/views/alerts/rules/metric/types' ;
21
+ import { getBackendDataset } from 'sentry/views/detectors/components/forms/metric/metricFormData' ;
18
22
import type { DetectorDataset } from 'sentry/views/detectors/datasetConfig/types' ;
19
23
import { useIncidentMarkers } from 'sentry/views/detectors/hooks/useIncidentMarkers' ;
20
24
import { useMetricDetectorAnomalyPeriods } from 'sentry/views/detectors/hooks/useMetricDetectorAnomalyPeriods' ;
21
25
import { useMetricDetectorSeries } from 'sentry/views/detectors/hooks/useMetricDetectorSeries' ;
22
26
import { useMetricDetectorThresholdSeries } from 'sentry/views/detectors/hooks/useMetricDetectorThresholdSeries' ;
27
+ import { useTimePeriodSelection } from 'sentry/views/detectors/hooks/useTimePeriodSelection' ;
23
28
24
29
const CHART_HEIGHT = 180 ;
25
30
31
+ function ChartError ( ) {
32
+ return (
33
+ < Flex style = { { height : CHART_HEIGHT } } justify = "center" align = "center" >
34
+ < ErrorPanel >
35
+ < IconWarning color = "gray300" size = "lg" />
36
+ < div > { t ( 'Error loading chart data' ) } </ div >
37
+ </ ErrorPanel >
38
+ </ Flex >
39
+ ) ;
40
+ }
41
+
42
+ function ChartLoading ( ) {
43
+ return (
44
+ < Flex style = { { height : CHART_HEIGHT } } justify = "center" align = "center" >
45
+ < Placeholder height = { `${ CHART_HEIGHT - 20 } px` } />
46
+ </ Flex >
47
+ ) ;
48
+ }
49
+
26
50
interface MetricDetectorChartProps {
27
51
/**
28
52
* The aggregate function to use (e.g., "avg(span.duration)")
@@ -61,10 +85,6 @@ interface MetricDetectorChartProps {
61
85
* Used in anomaly detection
62
86
*/
63
87
sensitivity : AlertRuleSensitivity | undefined ;
64
- /**
65
- * The time period for the chart data (optional, defaults to 7d)
66
- */
67
- statsPeriod : TimePeriod ;
68
88
/**
69
89
* Used in anomaly detection
70
90
*/
@@ -80,19 +100,24 @@ export function MetricDetectorChart({
80
100
projectId,
81
101
conditions,
82
102
detectionType,
83
- statsPeriod,
84
103
comparisonDelta,
85
104
sensitivity,
86
105
thresholdType,
87
106
} : MetricDetectorChartProps ) {
88
- const { series, comparisonSeries, isLoading, isError} = useMetricDetectorSeries ( {
107
+ const { selectedTimePeriod, setSelectedTimePeriod, timePeriodOptions} =
108
+ useTimePeriodSelection ( {
109
+ dataset : getBackendDataset ( dataset ) ,
110
+ interval,
111
+ } ) ;
112
+
113
+ const { series, comparisonSeries, isLoading, error} = useMetricDetectorSeries ( {
89
114
dataset,
90
115
aggregate,
91
116
interval,
92
117
query,
93
118
environment,
94
119
projectId,
95
- statsPeriod,
120
+ statsPeriod : selectedTimePeriod ,
96
121
comparisonDelta,
97
122
} ) ;
98
123
@@ -105,25 +130,26 @@ export function MetricDetectorChart({
105
130
106
131
const isAnomalyDetection = detectionType === 'dynamic' ;
107
132
const shouldFetchAnomalies =
108
- isAnomalyDetection && ! isLoading && ! isError && series . length > 0 ;
133
+ isAnomalyDetection && ! isLoading && ! error && series . length > 0 ;
109
134
110
135
// Fetch anomaly data when detection type is dynamic and series data is ready
111
136
const {
112
137
anomalyPeriods,
113
138
isLoading : isLoadingAnomalies ,
114
- error : anomalyErrorObject ,
139
+ error : anomalyError ,
115
140
} = useMetricDetectorAnomalyPeriods ( {
116
141
series : shouldFetchAnomalies ? series : [ ] ,
142
+ isLoadingSeries : isLoading ,
117
143
dataset,
118
144
aggregate,
119
145
query,
120
146
environment,
121
147
projectId,
122
- statsPeriod,
123
- timePeriod : interval ,
148
+ statsPeriod : selectedTimePeriod ,
149
+ interval,
124
150
thresholdType,
125
151
sensitivity,
126
- enabled : shouldFetchAnomalies ,
152
+ enabled : isAnomalyDetection ,
127
153
} ) ;
128
154
129
155
// Create anomaly marker rendering from pre-grouped anomaly periods
@@ -134,9 +160,6 @@ export function MetricDetectorChart({
134
160
yAxisIndex : 1 , // Use index 1 to avoid conflict with main chart axis
135
161
} ) ;
136
162
137
- const anomalyLoading = shouldFetchAnomalies ? isLoadingAnomalies : false ;
138
- const anomalyError = shouldFetchAnomalies ? anomalyErrorObject : null ;
139
-
140
163
// Calculate y-axis bounds to ensure all thresholds are visible
141
164
const maxValue = useMemo ( ( ) => {
142
165
// Get max from series data
@@ -219,40 +242,80 @@ export function MetricDetectorChart({
219
242
return baseGrid ;
220
243
} , [ isAnomalyDetection , anomalyMarkerResult . incidentMarkerGrid ] ) ;
221
244
222
- if ( isLoading || anomalyLoading ) {
223
- return (
224
- < Flex style = { { height : CHART_HEIGHT } } justify = "center" align = "center" >
225
- < Placeholder height = { `${ CHART_HEIGHT - 20 } px` } />
226
- </ Flex >
227
- ) ;
228
- }
229
-
230
- if ( isError || anomalyError ) {
231
- return (
232
- < Flex style = { { height : CHART_HEIGHT } } justify = "center" align = "center" >
233
- < ErrorPanel >
234
- < IconWarning color = "gray300" size = "lg" />
235
- < div > { t ( 'Error loading chart data' ) } </ div >
236
- </ ErrorPanel >
237
- </ Flex >
238
- ) ;
239
- }
240
-
241
245
return (
242
- < AreaChart
243
- isGroupedByDate
244
- showTimeInTooltip
245
- height = { CHART_HEIGHT }
246
- stacked = { false }
247
- series = { series }
248
- additionalSeries = { additionalSeries }
249
- yAxes = { yAxes . length > 1 ? yAxes : undefined }
250
- yAxis = { yAxes . length === 1 ? yAxes [ 0 ] : undefined }
251
- grid = { grid }
252
- xAxis = { isAnomalyDetection ? anomalyMarkerResult . incidentMarkerXAxis : undefined }
253
- ref = {
254
- isAnomalyDetection ? anomalyMarkerResult . connectIncidentMarkerChartRef : undefined
255
- }
256
- />
246
+ < ChartContainer >
247
+ { isLoading ? (
248
+ < ChartLoading />
249
+ ) : error ? (
250
+ < ChartError />
251
+ ) : (
252
+ < AreaChart
253
+ isGroupedByDate
254
+ showTimeInTooltip
255
+ height = { CHART_HEIGHT }
256
+ stacked = { false }
257
+ series = { series }
258
+ additionalSeries = { additionalSeries }
259
+ yAxes = { yAxes . length > 1 ? yAxes : undefined }
260
+ yAxis = { yAxes . length === 1 ? yAxes [ 0 ] : undefined }
261
+ grid = { grid }
262
+ xAxis = { isAnomalyDetection ? anomalyMarkerResult . incidentMarkerXAxis : undefined }
263
+ ref = {
264
+ isAnomalyDetection
265
+ ? anomalyMarkerResult . connectIncidentMarkerChartRef
266
+ : undefined
267
+ }
268
+ />
269
+ ) }
270
+ < ChartFooter >
271
+ { shouldFetchAnomalies ? (
272
+ < Flex align = "center" gap = "sm" >
273
+ { isLoadingAnomalies ? (
274
+ < Fragment >
275
+ < AnomalyLoadingIndicator size = { 18 } />
276
+ < Text variant = "muted" > { t ( 'Loading anomalies...' ) } </ Text >
277
+ </ Fragment >
278
+ ) : anomalyError ? (
279
+ < Text variant = "muted" > { t ( 'Error loading anomalies' ) } </ Text >
280
+ ) : anomalyPeriods . length === 0 ? (
281
+ < Text variant = "muted" > { t ( 'No anomalies found for this time period' ) } </ Text >
282
+ ) : (
283
+ < Text variant = "muted" >
284
+ { tn ( 'Found %s anomaly' , 'Found %s anomalies' , anomalyPeriods . length ) }
285
+ </ Text >
286
+ ) }
287
+ </ Flex >
288
+ ) : (
289
+ < div />
290
+ ) }
291
+ < CompactSelect
292
+ size = "sm"
293
+ options = { timePeriodOptions }
294
+ value = { selectedTimePeriod }
295
+ onChange = { opt => setSelectedTimePeriod ( opt . value ) }
296
+ triggerProps = { {
297
+ borderless : true ,
298
+ prefix : t ( 'Display' ) ,
299
+ } }
300
+ />
301
+ </ ChartFooter >
302
+ </ ChartContainer >
257
303
) ;
258
304
}
305
+
306
+ const ChartContainer = styled ( 'div' ) `
307
+ max-width: 1440px;
308
+ border-top: 1px solid ${ p => p . theme . border } ;
309
+ ` ;
310
+
311
+ const ChartFooter = styled ( 'div' ) `
312
+ display: flex;
313
+ justify-content: space-between;
314
+ align-items: center;
315
+ padding: ${ p => `${ p . theme . space . sm } 0 ${ p . theme . space . sm } ${ p . theme . space . lg } ` } ;
316
+ border-top: 1px solid ${ p => p . theme . border } ;
317
+ ` ;
318
+
319
+ const AnomalyLoadingIndicator = styled ( LoadingIndicator ) `
320
+ margin: 0;
321
+ ` ;
0 commit comments