mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[ML] Anomaly Detection: allow snapshot to be reverted from the view datafeed flyout (#133842)
* show revert snapshot flyout on snapshot annotation click * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * use latest_record_time_stamp and fetch all models to ensure they show up * move model snapshot fetch to client to get all snapshots * filter model data to chart timerange * add permission check * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * only show revert message in tooltip with correct permissions * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * remove unnecessary undefined check * move snapshot to be on top of annotation when timestamp is the same Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
decdafab31
commit
ee1bbf2f41
4 changed files with 129 additions and 50 deletions
|
@ -9,7 +9,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
|||
import type { LineAnnotationDatum, RectAnnotationDatum } from '@elastic/charts';
|
||||
import type { ErrorType } from '../util/errors';
|
||||
import type { EntityField } from '../util/anomaly_utils';
|
||||
import type { Datafeed, JobId } from './anomaly_detection_jobs';
|
||||
import type { Datafeed, JobId, ModelSnapshot } from './anomaly_detection_jobs';
|
||||
import { ES_AGGREGATION, ML_JOB_AGGREGATION } from '../constants/aggregation_types';
|
||||
import type { RecordForInfluencer } from './anomalies';
|
||||
|
||||
|
@ -20,12 +20,14 @@ export interface GetStoppedPartitionResult {
|
|||
export interface MLRectAnnotationDatum extends RectAnnotationDatum {
|
||||
header: number;
|
||||
}
|
||||
export interface LineAnnotationDatumWithModelSnapshot extends LineAnnotationDatum {
|
||||
modelSnapshot?: ModelSnapshot;
|
||||
}
|
||||
export interface GetDatafeedResultsChartDataResult {
|
||||
bucketResults: number[][];
|
||||
datafeedResults: number[][];
|
||||
annotationResultsRect: MLRectAnnotationDatum[];
|
||||
annotationResultsLine: LineAnnotationDatum[];
|
||||
modelSnapshotResultsLine: LineAnnotationDatum[];
|
||||
}
|
||||
|
||||
export interface DatafeedResultsChartDataParams {
|
||||
|
|
|
@ -42,11 +42,17 @@ import {
|
|||
ScaleType,
|
||||
Settings,
|
||||
timeFormatter,
|
||||
RectAnnotationEvent,
|
||||
LineAnnotationEvent,
|
||||
} from '@elastic/charts';
|
||||
|
||||
import { DATAFEED_STATE } from '../../../../../../common/constants/states';
|
||||
import { CombinedJobWithStats } from '../../../../../../common/types/anomaly_detection_jobs';
|
||||
import {
|
||||
CombinedJobWithStats,
|
||||
ModelSnapshot,
|
||||
} from '../../../../../../common/types/anomaly_detection_jobs';
|
||||
import { JobMessage } from '../../../../../../common/types/audit_message';
|
||||
import { LineAnnotationDatumWithModelSnapshot } from '../../../../../../common/types/results';
|
||||
import { useToastNotificationService } from '../../../../services/toast_notification_service';
|
||||
import { useMlApiContext } from '../../../../contexts/kibana';
|
||||
import { useCurrentEuiTheme } from '../../../../components/color_range_legend';
|
||||
|
@ -58,14 +64,23 @@ import { checkPermission } from '../../../../capabilities/check_capabilities';
|
|||
|
||||
const dateFormatter = timeFormatter('MM-DD HH:mm:ss');
|
||||
const MAX_CHART_POINTS = 480;
|
||||
const revertSnapshotMessage = i18n.translate(
|
||||
'xpack.ml.jobsList.datafeedChart.revertSnapshotMessage',
|
||||
{
|
||||
defaultMessage: 'Click to revert to this model snapshot.',
|
||||
}
|
||||
);
|
||||
|
||||
interface DatafeedChartFlyoutProps {
|
||||
jobId: string;
|
||||
end: number;
|
||||
onClose: () => void;
|
||||
onModelSnapshotAnnotationClick: (modelSnapshot: ModelSnapshot) => void;
|
||||
}
|
||||
|
||||
function setLineAnnotationHeader(lineDatum: LineAnnotationDatum) {
|
||||
function setLineAnnotationHeader(
|
||||
lineDatum: LineAnnotationDatum | LineAnnotationDatumWithModelSnapshot
|
||||
) {
|
||||
lineDatum.header = dateFormatter(lineDatum.dataValue);
|
||||
return lineDatum;
|
||||
}
|
||||
|
@ -78,12 +93,23 @@ const customTooltip: CustomAnnotationTooltip = ({ details, datum }) => (
|
|||
</div>
|
||||
);
|
||||
|
||||
export const DatafeedChartFlyout: FC<DatafeedChartFlyoutProps> = ({ jobId, end, onClose }) => {
|
||||
export const DatafeedChartFlyout: FC<DatafeedChartFlyoutProps> = ({
|
||||
jobId,
|
||||
end,
|
||||
onClose,
|
||||
onModelSnapshotAnnotationClick,
|
||||
}) => {
|
||||
const [data, setData] = useState<{
|
||||
datafeedConfig: CombinedJobWithStats['datafeed_config'] | undefined;
|
||||
bucketSpan: string | undefined;
|
||||
isInitialized: boolean;
|
||||
}>({ datafeedConfig: undefined, bucketSpan: undefined, isInitialized: false });
|
||||
modelSnapshotData: LineAnnotationDatumWithModelSnapshot[];
|
||||
}>({
|
||||
datafeedConfig: undefined,
|
||||
bucketSpan: undefined,
|
||||
isInitialized: false,
|
||||
modelSnapshotData: [],
|
||||
});
|
||||
const [endDate, setEndDate] = useState<any>(moment(end));
|
||||
const [isLoadingChartData, setIsLoadingChartData] = useState<boolean>(false);
|
||||
const [bucketData, setBucketData] = useState<number[][]>([]);
|
||||
|
@ -91,15 +117,20 @@ export const DatafeedChartFlyout: FC<DatafeedChartFlyoutProps> = ({ jobId, end,
|
|||
rect: RectAnnotationDatum[];
|
||||
line: LineAnnotationDatum[];
|
||||
}>({ rect: [], line: [] });
|
||||
const [modelSnapshotData, setModelSnapshotData] = useState<LineAnnotationDatum[]>([]);
|
||||
const [modelSnapshotDataForTimeRange, setModelSnapshotDataForTimeRange] = useState<
|
||||
LineAnnotationDatumWithModelSnapshot[]
|
||||
>([]);
|
||||
const [messageData, setMessageData] = useState<LineAnnotationDatum[]>([]);
|
||||
const [sourceData, setSourceData] = useState<number[][]>([]);
|
||||
const [showAnnotations, setShowAnnotations] = useState<boolean>(true);
|
||||
const [showModelSnapshots, setShowModelSnapshots] = useState<boolean>(true);
|
||||
const [range, setRange] = useState<{ start: string; end: string } | undefined>();
|
||||
const canUpdateDatafeed = useMemo(() => checkPermission('canUpdateDatafeed'), []);
|
||||
const canCreateJob = useMemo(() => checkPermission('canCreateJob'), []);
|
||||
const canStartStopDatafeed = useMemo(() => checkPermission('canStartStopDatafeed'), []);
|
||||
|
||||
const {
|
||||
getModelSnapshots,
|
||||
results: { getDatafeedResultChartData },
|
||||
} = useMlApiContext();
|
||||
const { displayErrorToast } = useToastNotificationService();
|
||||
|
@ -144,7 +175,11 @@ export const DatafeedChartFlyout: FC<DatafeedChartFlyoutProps> = ({ jobId, end,
|
|||
rect: chartData.annotationResultsRect,
|
||||
line: chartData.annotationResultsLine.map(setLineAnnotationHeader),
|
||||
});
|
||||
setModelSnapshotData(chartData.modelSnapshotResultsLine.map(setLineAnnotationHeader));
|
||||
setModelSnapshotDataForTimeRange(
|
||||
data.modelSnapshotData.filter(
|
||||
(datum) => datum.dataValue >= startTimestamp && datum.dataValue <= endTimestamp
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
const title = i18n.translate('xpack.ml.jobsList.datafeedChart.errorToastTitle', {
|
||||
defaultMessage: 'Error fetching data',
|
||||
|
@ -154,21 +189,37 @@ export const DatafeedChartFlyout: FC<DatafeedChartFlyoutProps> = ({ jobId, end,
|
|||
setIsLoadingChartData(false);
|
||||
}, [endDate, data.bucketSpan]);
|
||||
|
||||
const getJobData = useCallback(async () => {
|
||||
const getJobAndSnapshotData = useCallback(async () => {
|
||||
try {
|
||||
const job: CombinedJobWithStats = await loadFullJob(jobId);
|
||||
const modelSnapshotResultsLine: LineAnnotationDatumWithModelSnapshot[] = [];
|
||||
const modelSnapshotsResp = await getModelSnapshots(jobId);
|
||||
const modelSnapshots = modelSnapshotsResp.model_snapshots ?? [];
|
||||
modelSnapshots.forEach((modelSnapshot) => {
|
||||
const timestamp = Number(modelSnapshot.latest_record_time_stamp);
|
||||
|
||||
modelSnapshotResultsLine.push({
|
||||
dataValue: timestamp,
|
||||
details: `${modelSnapshot.description}. ${
|
||||
canCreateJob && canStartStopDatafeed ? revertSnapshotMessage : ''
|
||||
}`,
|
||||
modelSnapshot,
|
||||
});
|
||||
});
|
||||
|
||||
setData({
|
||||
datafeedConfig: job.datafeed_config,
|
||||
bucketSpan: job.analysis_config.bucket_span,
|
||||
isInitialized: true,
|
||||
modelSnapshotData: modelSnapshotResultsLine.map(setLineAnnotationHeader),
|
||||
});
|
||||
} catch (error) {
|
||||
displayErrorToast(error);
|
||||
}
|
||||
}, [jobId]);
|
||||
|
||||
useEffect(function loadJobWithDatafeed() {
|
||||
getJobData();
|
||||
useEffect(function loadInitialData() {
|
||||
getJobAndSnapshotData();
|
||||
}, []);
|
||||
|
||||
useEffect(
|
||||
|
@ -323,6 +374,24 @@ export const DatafeedChartFlyout: FC<DatafeedChartFlyoutProps> = ({ jobId, end,
|
|||
<Settings
|
||||
showLegend
|
||||
legendPosition={Position.Bottom}
|
||||
onAnnotationClick={(annotations: {
|
||||
rects: RectAnnotationEvent[];
|
||||
lines: LineAnnotationEvent[];
|
||||
}) => {
|
||||
// If it's not a line annotation or if it's not a model snapshot annotation then do nothing
|
||||
if (
|
||||
!(canCreateJob && canStartStopDatafeed) ||
|
||||
annotations.lines?.length === 0 ||
|
||||
(annotations.lines &&
|
||||
!annotations.lines[0].id.includes('Model snapshots'))
|
||||
)
|
||||
return;
|
||||
|
||||
onModelSnapshotAnnotationClick(
|
||||
// @ts-expect-error property 'modelSnapshot' does not exist on type
|
||||
annotations.lines[0].datum.modelSnapshot
|
||||
);
|
||||
}}
|
||||
// TODO use the EUI charts theme see src/plugins/charts/public/services/theme/README.md
|
||||
theme={{
|
||||
lineSeriesStyle: {
|
||||
|
@ -349,28 +418,6 @@ export const DatafeedChartFlyout: FC<DatafeedChartFlyoutProps> = ({ jobId, end,
|
|||
})}
|
||||
position={Position.Left}
|
||||
/>
|
||||
{showModelSnapshots ? (
|
||||
<LineAnnotation
|
||||
id={i18n.translate(
|
||||
'xpack.ml.jobsList.datafeedChart.modelSnapshotsLineSeriesId',
|
||||
{
|
||||
defaultMessage: 'Model snapshots',
|
||||
}
|
||||
)}
|
||||
key="model-snapshots-results-line"
|
||||
domainType={AnnotationDomainType.XDomain}
|
||||
dataValues={modelSnapshotData}
|
||||
marker={<EuiIcon type="asterisk" />}
|
||||
markerPosition={Position.Top}
|
||||
style={{
|
||||
line: {
|
||||
strokeWidth: 3,
|
||||
stroke: euiTheme.euiColorVis1,
|
||||
opacity: 0.5,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{showAnnotations ? (
|
||||
<>
|
||||
<LineAnnotation
|
||||
|
@ -407,6 +454,28 @@ export const DatafeedChartFlyout: FC<DatafeedChartFlyoutProps> = ({ jobId, end,
|
|||
/>
|
||||
</>
|
||||
) : null}
|
||||
{showModelSnapshots ? (
|
||||
<LineAnnotation
|
||||
id={i18n.translate(
|
||||
'xpack.ml.jobsList.datafeedChart.modelSnapshotsLineSeriesId',
|
||||
{
|
||||
defaultMessage: 'Model snapshots',
|
||||
}
|
||||
)}
|
||||
key="model-snapshots-results-line"
|
||||
domainType={AnnotationDomainType.XDomain}
|
||||
dataValues={modelSnapshotDataForTimeRange}
|
||||
marker={<EuiIcon type="asterisk" />}
|
||||
markerPosition={Position.Top}
|
||||
style={{
|
||||
line: {
|
||||
strokeWidth: 3,
|
||||
stroke: euiTheme.euiColorVis1,
|
||||
opacity: 0.5,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{messageData.length > 0 ? (
|
||||
<>
|
||||
<LineAnnotation
|
||||
|
|
|
@ -17,6 +17,7 @@ import { DatafeedPreviewPane } from './datafeed_preview_tab';
|
|||
import { AnnotationsTable } from '../../../../components/annotations/annotations_table';
|
||||
import { DatafeedChartFlyout } from '../datafeed_chart_flyout';
|
||||
import { AnnotationFlyout } from '../../../../components/annotations/annotation_flyout';
|
||||
import { RevertModelSnapshotFlyout } from '../../../../components/model_snapshots/revert_model_snapshot_flyout';
|
||||
import { ModelSnapshotTable } from '../../../../components/model_snapshots';
|
||||
import { ForecastsTable } from './forecasts_table';
|
||||
import { JobDetailsPane } from './job_details_pane';
|
||||
|
@ -29,6 +30,8 @@ export class JobDetailsUI extends Component {
|
|||
|
||||
this.state = {
|
||||
datafeedChartFlyoutVisible: false,
|
||||
modelSnapshot: null,
|
||||
revertSnapshotFlyoutVisible: false,
|
||||
};
|
||||
if (this.props.addYourself) {
|
||||
this.props.addYourself(props.jobId, (j) => this.updateJob(j));
|
||||
|
@ -151,10 +154,31 @@ export class JobDetailsUI extends Component {
|
|||
datafeedChartFlyoutVisible: false,
|
||||
});
|
||||
}}
|
||||
onModelSnapshotAnnotationClick={(modelSnapshot) => {
|
||||
this.setState({
|
||||
modelSnapshot,
|
||||
revertSnapshotFlyoutVisible: true,
|
||||
datafeedChartFlyoutVisible: false,
|
||||
});
|
||||
}}
|
||||
end={job.data_counts.latest_bucket_timestamp}
|
||||
jobId={this.props.jobId}
|
||||
/>
|
||||
) : null}
|
||||
{this.state.revertSnapshotFlyoutVisible === true &&
|
||||
this.state.modelSnapshot !== null ? (
|
||||
<RevertModelSnapshotFlyout
|
||||
snapshot={this.state.modelSnapshot}
|
||||
snapshots={[this.state.modelSnapshot]}
|
||||
job={job}
|
||||
closeFlyout={() => {
|
||||
this.setState({
|
||||
revertSnapshotFlyoutVisible: false,
|
||||
});
|
||||
}}
|
||||
refresh={refreshJobList}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
|
|
@ -657,7 +657,6 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust
|
|||
datafeedResults: [],
|
||||
annotationResultsRect: [],
|
||||
annotationResultsLine: [],
|
||||
modelSnapshotResultsLine: [],
|
||||
};
|
||||
|
||||
const { getDatafeedByJobId } = datafeedsProvider(client!, mlClient);
|
||||
|
@ -739,7 +738,7 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust
|
|||
|
||||
const { getAnnotations } = annotationServiceProvider(client!);
|
||||
|
||||
const [bucketResp, annotationResp, modelSnapshotsResp] = await Promise.all([
|
||||
const [bucketResp, annotationResp] = await Promise.all([
|
||||
mlClient.getBuckets({
|
||||
job_id: jobId,
|
||||
body: { desc: true, start: String(start), end: String(end), page: { from: 0, size: 1000 } },
|
||||
|
@ -750,11 +749,6 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust
|
|||
latestMs: end,
|
||||
maxAnnotations: 1000,
|
||||
}),
|
||||
mlClient.getModelSnapshots({
|
||||
job_id: jobId,
|
||||
start: String(start),
|
||||
end: String(end),
|
||||
}),
|
||||
]);
|
||||
|
||||
const bucketResults = bucketResp?.buckets ?? [];
|
||||
|
@ -786,16 +780,6 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust
|
|||
}
|
||||
});
|
||||
|
||||
const modelSnapshots = modelSnapshotsResp?.model_snapshots ?? [];
|
||||
modelSnapshots.forEach((modelSnapshot) => {
|
||||
const timestamp = Number(modelSnapshot?.timestamp);
|
||||
|
||||
finalResults.modelSnapshotResultsLine.push({
|
||||
dataValue: timestamp,
|
||||
details: modelSnapshot.description,
|
||||
});
|
||||
});
|
||||
|
||||
return finalResults;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue