Skip to content

Commit 1b19059

Browse files
authored
Support multiple category fields (opensearch-project#66)
Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com>
1 parent d8e937d commit 1b19059

File tree

30 files changed

+2121
-1045
lines changed

30 files changed

+2121
-1045
lines changed

public/models/interfaces.ts

-10
Original file line numberDiff line numberDiff line change
@@ -143,16 +143,6 @@ export type DetectorListItem = {
143143
enabledTime?: number;
144144
};
145145

146-
export type HistoricalDetectorListItem = {
147-
id: string;
148-
name: string;
149-
curState: DETECTOR_STATE;
150-
indices: string[];
151-
totalAnomalies: number;
152-
dataStartTime: number;
153-
dataEndTime: number;
154-
};
155-
156146
export type EntityData = {
157147
name: string;
158148
value: string;

public/pages/AnomalyCharts/components/FeatureChart/FeatureChart.tsx

+2-4
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ interface FeatureChartProps {
7474
showFeatureMissingDataPointAnnotation?: boolean;
7575
detectorEnabledTime?: number;
7676
rawFeatureData: FeatureAggregationData[];
77-
titlePrefix?: string;
7877
}
7978

8079
export const FeatureChart = (props: FeatureChartProps) => {
@@ -165,10 +164,9 @@ export const FeatureChart = (props: FeatureChartProps) => {
165164
return (
166165
<ContentPanel
167166
title={
168-
(props.titlePrefix ? props.titlePrefix + ' - ' : '') +
169-
(props.feature.featureEnabled
167+
props.feature.featureEnabled
170168
? props.feature.featureName
171-
: `${props.feature.featureName} (disabled)`)
169+
: `${props.feature.featureName} (disabled)`
172170
}
173171
bodyStyles={
174172
!props.feature.featureEnabled

public/pages/AnomalyCharts/containers/AnomaliesChart.tsx

+21-12
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {
3333
EuiSuperDatePicker,
3434
EuiTitle,
3535
} from '@elastic/eui';
36-
import { get } from 'lodash';
36+
import { get, orderBy } from 'lodash';
3737
import moment, { DurationInputArg2 } from 'moment';
3838
import React, { useState } from 'react';
3939
import { EntityAnomalySummaries } from '../../../../server/models/interfaces';
@@ -58,6 +58,7 @@ import {
5858
getConfidenceWording,
5959
getFeatureBreakdownWording,
6060
getFeatureDataWording,
61+
getHCTitle,
6162
} from '../utils/anomalyChartUtils';
6263
import {
6364
DATE_PICKER_QUICK_OPTIONS,
@@ -67,7 +68,7 @@ import { AnomalyOccurrenceChart } from './AnomalyOccurrenceChart';
6768
import { FeatureBreakDown } from './FeatureBreakDown';
6869
import { convertTimestampToString } from '../../../utils/utils';
6970

70-
interface AnomaliesChartProps {
71+
export interface AnomaliesChartProps {
7172
onDateRangeChange(
7273
startDate: number,
7374
endDate: number,
@@ -245,9 +246,13 @@ export const AnomaliesChart = React.memo((props: AnomaliesChartProps) => {
245246
<AnomalyHeatmapChart
246247
detectorId={get(props.detector, 'id', '')}
247248
detectorName={get(props.detector, 'name', '')}
249+
detectorTaskProgress={get(
250+
props.detector,
251+
'taskProgress',
252+
0
253+
)}
254+
isHistorical={props.isHistorical}
248255
dateRange={props.dateRange}
249-
//@ts-ignore
250-
title={props.detectorCategoryField[0]}
251256
anomalies={anomalies}
252257
isLoading={props.isLoading}
253258
showAlerts={props.showAlerts}
@@ -266,6 +271,12 @@ export const AnomaliesChart = React.memo((props: AnomaliesChartProps) => {
266271
onDisplayOptionChanged={props.onDisplayOptionChanged}
267272
heatmapDisplayOption={props.heatmapDisplayOption}
268273
isNotSample={props.isNotSample}
274+
// Category fields in HC results are always sorted alphabetically. To make all chart
275+
// wording consistent with the returned results, we sort the given category
276+
// fields in alphabetical order as well.
277+
categoryField={orderBy(props.detectorCategoryField, [
278+
(categoryField) => categoryField.toLowerCase(),
279+
])}
269280
/>,
270281
props.isNotSample !== true
271282
? [
@@ -284,15 +295,13 @@ export const AnomaliesChart = React.memo((props: AnomaliesChartProps) => {
284295
<EuiSpacer size="s" />
285296
<EuiTitle size="s">
286297
<h3>
287-
{`${get(
288-
props,
289-
'detectorCategoryField.0'
290-
)} `}
291298
<b>
292-
{
293-
props.selectedHeatmapCell
294-
?.entityValue
295-
}
299+
{props.selectedHeatmapCell
300+
? getHCTitle(
301+
props.selectedHeatmapCell
302+
.entityList
303+
)
304+
: '-'}
296305
</b>
297306
</h3>
298307
</EuiTitle>

public/pages/AnomalyCharts/containers/AnomalyHeatmapChart.tsx

+99-43
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@
2424
* permissions and limitations under the License.
2525
*/
2626

27-
import React, { useState } from 'react';
28-
27+
import React, { useState, useEffect } from 'react';
2928
import moment from 'moment';
3029
import Plotly, { PlotData } from 'plotly.js-dist';
3130
import plotComponentFactory from 'react-plotly.js/factory';
@@ -54,13 +53,25 @@ import {
5453
filterHeatmapPlotDataByY,
5554
getEntitytAnomaliesHeatmapData,
5655
} from '../utils/anomalyChartUtils';
57-
import { MIN_IN_MILLI_SECS } from '../../../../server/utils/constants';
58-
import { EntityAnomalySummaries } from '../../../../server/models/interfaces';
56+
import {
57+
MIN_IN_MILLI_SECS,
58+
ENTITY_LIST_DELIMITER,
59+
} from '../../../../server/utils/constants';
60+
import {
61+
EntityAnomalySummaries,
62+
Entity,
63+
} from '../../../../server/models/interfaces';
64+
import { HEATMAP_CHART_Y_AXIS_WIDTH } from '../utils/constants';
65+
import {
66+
convertToEntityList,
67+
convertToCategoryFieldString,
68+
} from '../../utils/anomalyResultUtils';
5969

6070
interface AnomalyHeatmapChartProps {
61-
title: string;
6271
detectorId: string;
6372
detectorName: string;
73+
detectorTaskProgress?: number;
74+
isHistorical?: boolean;
6475
anomalies?: any[];
6576
dateRange: DateRange;
6677
isLoading: boolean;
@@ -72,11 +83,13 @@ interface AnomalyHeatmapChartProps {
7283
heatmapDisplayOption?: HeatmapDisplayOption;
7384
entityAnomalySummaries?: EntityAnomalySummaries[];
7485
isNotSample?: boolean;
86+
categoryField?: string[];
7587
}
7688

7789
export interface HeatmapCell {
7890
dateRange: DateRange;
79-
entityValue: string;
91+
entityList: Entity[];
92+
modelId?: string;
8093
}
8194

8295
export interface HeatmapDisplayOption {
@@ -123,14 +136,15 @@ export const AnomalyHeatmapChart = React.memo(
123136
//@ts-ignore
124137
individualEntities = inputHeatmapData[0].y.filter(
125138
//@ts-ignore
126-
(entityValue) => entityValue && entityValue.trim().length > 0
139+
(entityListAsString) =>
140+
entityListAsString && entityListAsString.trim().length > 0
127141
);
128142
}
129143
const individualEntityOptions = [] as any[];
130144
//@ts-ignore
131-
individualEntities.forEach((entityValue) => {
145+
individualEntities.forEach((entityListAsString: string) => {
132146
individualEntityOptions.push({
133-
label: entityValue,
147+
label: entityListAsString.replace(ENTITY_LIST_DELIMITER, ', '),
134148
});
135149
});
136150

@@ -165,7 +179,7 @@ export const AnomalyHeatmapChart = React.memo(
165179
getEntitytAnomaliesHeatmapData(
166180
props.dateRange,
167181
props.entityAnomalySummaries,
168-
props.heatmapDisplayOption.entityOption.value
182+
props.heatmapDisplayOption?.entityOption.value
169183
)
170184
: // use anomalies data in case of sample result
171185
getAnomaliesHeatmapData(
@@ -184,7 +198,7 @@ export const AnomalyHeatmapChart = React.memo(
184198
AnomalyHeatmapSortType
185199
>(
186200
props.isNotSample
187-
? props.heatmapDisplayOption.sortType
201+
? props.heatmapDisplayOption?.sortType
188202
: SORT_BY_FIELD_OPTIONS[0].value
189203
);
190204

@@ -215,9 +229,25 @@ export const AnomalyHeatmapChart = React.memo(
215229
return false;
216230
};
217231

232+
// Custom hook to refresh all of the heatmap data when running a historical task
233+
useEffect(() => {
234+
if (props.isHistorical) {
235+
const updateHeatmapPlotData = getAnomaliesHeatmapData(
236+
props.anomalies,
237+
props.dateRange,
238+
sortByFieldValue,
239+
get(COMBINED_OPTIONS.options[0], 'value')
240+
);
241+
setOriginalHeatmapData(updateHeatmapPlotData);
242+
setHeatmapData(updateHeatmapPlotData);
243+
setNumEntities(updateHeatmapPlotData[0].y.length);
244+
setEntityViewOptions(getViewEntityOptions(updateHeatmapPlotData));
245+
}
246+
}, [props.detectorTaskProgress]);
247+
218248
const handleHeatmapClick = (event: Plotly.PlotMouseEvent) => {
219249
const selectedCellIndices = get(event, 'points[0].pointIndex', []);
220-
const selectedEntity = get(event, 'points[0].y', '');
250+
const selectedEntityString = get(event, 'points[0].y', '');
221251
if (!isEmpty(selectedCellIndices)) {
222252
let anomalyCount = get(event, 'points[0].text', 0);
223253
if (
@@ -262,7 +292,11 @@ export const AnomalyHeatmapChart = React.memo(
262292
startDate: selectedStartDate,
263293
endDate: selectedEndDate,
264294
},
265-
entityValue: selectedEntity,
295+
entityList: convertToEntityList(
296+
selectedEntityString,
297+
get(props, 'categoryField', []),
298+
ENTITY_LIST_DELIMITER
299+
),
266300
} as HeatmapCell);
267301
}
268302
}
@@ -345,7 +379,7 @@ export const AnomalyHeatmapChart = React.memo(
345379

346380
setNumEntities(nonCombinedOptions.length);
347381
const selectedYs = nonCombinedOptions.map((option) =>
348-
get(option, 'label', '')
382+
get(option, 'label', '').replace(', ', ENTITY_LIST_DELIMITER)
349383
);
350384

351385
let selectedHeatmapData = filterHeatmapPlotDataByY(
@@ -407,40 +441,53 @@ export const AnomalyHeatmapChart = React.memo(
407441
</EuiFlexItem>
408442
</EuiFlexGroup>
409443
<EuiFlexGroup style={{ padding: '0px' }}>
410-
<EuiFlexItem grow={false} style={{ minWidth: '80px' }}>
411-
<EuiFlexGroup alignItems="center" justifyContent="flexEnd">
412-
<EuiText textAlign="right">
413-
<h4>{props.title}</h4>
414-
</EuiText>
415-
</EuiFlexGroup>
416-
</EuiFlexItem>
417444
<EuiFlexItem style={{ paddingLeft: '5px' }}>
418445
<EuiFlexGroup
419446
style={{ padding: '0px' }}
420447
justifyContent="spaceBetween"
421448
>
422-
<EuiFlexItem grow={false} style={{ marginLeft: '0px' }}>
423-
<EuiFlexGroup gutterSize="s" alignItems="center">
424-
<EuiFlexItem style={{ minWidth: 300 }}>
425-
<EuiComboBox
426-
placeholder="Select options"
427-
options={entityViewOptions}
428-
selectedOptions={currentViewOptions}
429-
onChange={(selectedOptions) =>
430-
handleViewEntityOptionsChange(selectedOptions)
431-
}
432-
/>
433-
</EuiFlexItem>
434-
<EuiFlexItem style={{ minWidth: 150 }}>
435-
<EuiSuperSelect
436-
options={SORT_BY_FIELD_OPTIONS}
437-
valueOfSelected={sortByFieldValue}
438-
onChange={(value) => handleSortByFieldChange(value)}
439-
hasDividers
440-
/>
441-
</EuiFlexItem>
442-
</EuiFlexGroup>
443-
</EuiFlexItem>
449+
<EuiFlexGroup
450+
direction="column"
451+
justifyContent="spaceBetween"
452+
style={{ padding: '12px', marginBottom: '6px' }}
453+
>
454+
<EuiFlexItem style={{ marginBottom: '0px' }}>
455+
<EuiText>
456+
<h4>
457+
View by:&nbsp;
458+
<b>
459+
{convertToCategoryFieldString(
460+
get(props, 'categoryField', []) as string[],
461+
', '
462+
)}
463+
</b>
464+
</h4>
465+
</EuiText>
466+
</EuiFlexItem>
467+
468+
<EuiFlexItem grow={false}>
469+
<EuiFlexGroup gutterSize="s" alignItems="center">
470+
<EuiFlexItem style={{ minWidth: 300 }}>
471+
<EuiComboBox
472+
placeholder="Select options"
473+
options={entityViewOptions}
474+
selectedOptions={currentViewOptions}
475+
onChange={(selectedOptions) =>
476+
handleViewEntityOptionsChange(selectedOptions)
477+
}
478+
/>
479+
</EuiFlexItem>
480+
<EuiFlexItem style={{ minWidth: 150 }}>
481+
<EuiSuperSelect
482+
options={SORT_BY_FIELD_OPTIONS}
483+
valueOfSelected={sortByFieldValue}
484+
onChange={(value) => handleSortByFieldChange(value)}
485+
hasDividers
486+
/>
487+
</EuiFlexItem>
488+
</EuiFlexGroup>
489+
</EuiFlexItem>
490+
</EuiFlexGroup>
444491
<EuiFlexItem grow={false}>
445492
<EuiFlexGroup alignItems="center">
446493
<EuiFlexItem>
@@ -542,6 +589,15 @@ export const AnomalyHeatmapChart = React.memo(
542589
showline: true,
543590
showgrid: false,
544591
fixedrange: true,
592+
automargin: true,
593+
tickmode: 'array',
594+
tickvals: heatmapData[0].y,
595+
ticktext: heatmapData[0].y.map((label: string) =>
596+
label.length <= HEATMAP_CHART_Y_AXIS_WIDTH
597+
? label
598+
: label.substring(0, HEATMAP_CHART_Y_AXIS_WIDTH - 3) +
599+
'...'
600+
),
545601
},
546602
margin: {
547603
l: 100,

0 commit comments

Comments
 (0)