/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/
import React from 'react';
import { get } from 'lodash';
import {
EuiFlexItem,
EuiFlexGroup,
EuiTitle,
EuiSpacer,
EuiCallOut,
} from '@elastic/eui';
import { FeatureChart } from '../components/FeatureChart/FeatureChart';
import {
Detector,
FeatureAttributes,
Anomalies,
DateRange,
FEATURE_TYPE,
FeatureAggregationData,
EntityData,
UNITS,
} from '../../../models/interfaces';
import { NoFeaturePrompt } from '../components/FeatureChart/NoFeaturePrompt';
import { focusOnFeatureAccordion } from '../../ConfigureModel/utils/helpers';
import moment from 'moment';
import { HeatmapCell } from './AnomalyHeatmapChart';
interface FeatureBreakDownProps {
title?: string;
detector: Detector;
anomalyAndFeatureResults: Anomalies[];
annotations: any[][];
isLoading: boolean;
dateRange: DateRange;
featureDataSeriesName: string;
showFeatureMissingDataPointAnnotation?: boolean;
rawAnomalyResults?: Anomalies[];
isFeatureDataMissing?: boolean;
isHCDetector?: boolean;
selectedHeatmapCell?: HeatmapCell;
}
export const FeatureBreakDown = React.memo((props: FeatureBreakDownProps) => {
const getFeatureDataForChart = (
anomalyAndFeatureResults: Anomalies[],
featureId: string
) => {
if (props.isHCDetector && !props.selectedHeatmapCell) {
return [];
} else {
let featureResults = [] as FeatureAggregationData[][];
if (anomalyAndFeatureResults !== undefined) {
anomalyAndFeatureResults.forEach((timeSeries: Anomalies) => {
const curFeatureData = get(
timeSeries,
`featureData.${featureId}`,
[]
) as FeatureAggregationData[];
featureResults.push(curFeatureData);
});
}
return featureResults;
}
};
// Returns an array of entity combinations - one for each time series.
// Need to propagate this data to the chart in order to label the different possible time series
const getEntityDataForChart = (anomalyAndFeatureResults: Anomalies[]) => {
if (props.isHCDetector && !props.selectedHeatmapCell) {
return [];
} else {
let entityData = [] as EntityData[][];
if (anomalyAndFeatureResults !== undefined) {
anomalyAndFeatureResults.forEach((timeSeries: Anomalies) => {
// extracting the entity data from the first anomaly point in the time series
entityData.push(get(timeSeries, 'anomalies.0.entity', []));
});
}
return entityData;
}
};
const getAnnotationData = () => {
return props.isHCDetector && !props.selectedHeatmapCell
? []
: props.annotations;
};
return (
<React.Fragment>
{props.title ? (
<EuiFlexGroup alignItems="flexEnd">
<EuiFlexItem>
<EuiTitle size="s" className="preview-title">
<h4>{props.title}</h4>
</EuiTitle>
<EuiSpacer size="s" />
</EuiFlexItem>
</EuiFlexGroup>
) : null}
{props.showFeatureMissingDataPointAnnotation &&
props.detector.enabledTime &&
props.isFeatureDataMissing ? (
<EuiCallOut
title={`Missing data is only shown since last enabled time: ${moment(
props.detector.enabledTime
).format('MM/DD/YY h:mm A')}`}
color={'warning'}
iconType={'alert'}
style={{ marginBottom: '20px' }}
/>
) : null}
{get(props, 'detector.featureAttributes', []).map(
(feature: FeatureAttributes, index: number) => (
<React.Fragment key={`${feature.featureName}-${feature.featureId}`}>
<FeatureChart
feature={feature}
featureData={getFeatureDataForChart(
props.anomalyAndFeatureResults,
//@ts-ignore
feature.featureId
)}
rawFeatureData={getFeatureDataForChart(
//@ts-ignore
props.rawAnomalyResults,
feature.featureId
)}
annotations={getAnnotationData()}
isLoading={props.isLoading}
dateRange={props.dateRange}
featureType={
get(
props,
`detector.uiMetadata.features.${feature.featureName}.featureType`
) as FEATURE_TYPE
}
field={
get(
props,
`detector.uiMetadata.features.${feature.featureName}.featureType`
) === FEATURE_TYPE.SIMPLE
? get(
props,
`detector.uiMetadata.features.${feature.featureName}.aggregationOf`
)
: undefined
}
aggregationMethod={
get(
props,
`detector.uiMetadata.features.${feature.featureName}.featureType`
) === FEATURE_TYPE.SIMPLE
? get(
props,
`detector.uiMetadata.features.${feature.featureName}.aggregationBy`
)
: undefined
}
featureDataSeriesName={props.featureDataSeriesName}
edit={props.title === 'Sample feature breakdown'}
onEdit={() => {
focusOnFeatureAccordion(index);
}}
detectorInterval={props.detector.detectionInterval.period}
showFeatureMissingDataPointAnnotation={
props.showFeatureMissingDataPointAnnotation
}
detectorEnabledTime={props.detector.enabledTime}
entityData={getEntityDataForChart(props.anomalyAndFeatureResults)}
isHCDetector={props.isHCDetector}
windowDelay={get(props, `detector.windowDelay.period`, {
period: { interval: 0, unit: UNITS.MINUTES },
})}
/>
{index + 1 ===
get(props, 'detector.featureAttributes', []).length ? null : (
<EuiSpacer size="l" />
)}
</React.Fragment>
)
)}
{!props.isLoading &&
get(props, 'detector.featureAttributes.length', 0) === 0 ? (
<NoFeaturePrompt detectorId={props.detector?.id} />
) : null}
</React.Fragment>
);
});