[8.4] [ML] Fix Anomaly charts time and query not being added to dashboard and case (#140147) (#141681)

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Quynh Nguyen (Quinn) 2022-09-23 15:00:38 -05:00 committed by GitHub
parent 764d6389bd
commit 6f80844a1d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 115 additions and 32 deletions

View file

@ -109,9 +109,10 @@ export class ElasticChartService extends FtrService {
*/
public async getChartDebugData(
dataTestSubj?: string,
match: number = 0
match: number = 0,
timeout: number | undefined = undefined
): Promise<DebugState | null> {
const chart = await this.getChart(dataTestSubj, undefined, match);
const chart = await this.getChart(dataTestSubj, timeout, match);
try {
const visContainer = await chart.findByCssSelector('.echChartStatus');

View file

@ -16,7 +16,7 @@ import {
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { useMlKibana } from '../contexts/kibana';
import type { AppStateSelectedCells, ExplorerJob } from './explorer_utils';
import { AppStateSelectedCells, ExplorerJob } from './explorer_utils';
import { TimeRangeBounds } from '../util/time_buckets';
import { AddAnomalyChartsToDashboardControl } from './dashboard_controls/add_anomaly_charts_to_dashboard_controls';
@ -68,8 +68,8 @@ export const AnomalyContextMenu: FC<AnomalyContextMenuProps> = ({
return (
<>
{menuItems.length > 0 && chartsCount > 0 && (
<EuiFlexItem grow={false} style={{ marginLeft: 'auto', alignSelf: 'baseline' }}>
{menuItems.length > 0 && chartsCount > 0 ? (
<EuiFlexItem grow={false} css={{ marginLeft: 'auto', alignSelf: 'baseline' }}>
<EuiPopover
button={
<EuiButtonIcon
@ -92,16 +92,16 @@ export const AnomalyContextMenu: FC<AnomalyContextMenuProps> = ({
<EuiContextMenuPanel items={menuItems} />
</EuiPopover>
</EuiFlexItem>
)}
) : null}
{isAddDashboardsActive && selectedJobs ? (
<AddAnomalyChartsToDashboardControl
onClose={async () => {
setIsAddDashboardActive(false);
}}
jobIds={jobIds}
selectedCells={selectedCells}
bounds={bounds}
interval={interval}
jobIds={jobIds}
/>
) : null}
</>

View file

@ -80,7 +80,7 @@ export const AnomalyTimeline: FC<AnomalyTimelineProps> = React.memo(
const { overallAnnotations } = explorerState;
const { filterActive } = useObservable(
const { filterActive, queryString } = useObservable(
anomalyExplorerCommonStateService.getFilterSettings$(),
anomalyExplorerCommonStateService.getFilterSettings()
);
@ -420,6 +420,7 @@ export const AnomalyTimeline: FC<AnomalyTimelineProps> = React.memo(
}}
jobIds={selectedJobs.map(({ id }) => id)}
viewBy={viewBySwimlaneFieldName!}
queryString={queryString}
/>
)}
</>

View file

@ -53,6 +53,8 @@ interface SwimLanePagination {
* Service for managing anomaly timeline state.
*/
export class AnomalyTimelineStateService extends StateService {
// TODO: Add services for getSelectionInfluencers, getSelectionJobIds, & getSelectionTimeRange
// to consolidate usage
private readonly _explorerURLStateCallback: (
update: AnomalyExplorerSwimLaneUrlState,
replaceState?: boolean

View file

@ -4,35 +4,45 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FC, useCallback, useState } from 'react';
import React, { FC, useCallback, useMemo, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFieldNumber, EuiFormRow, formatDate, htmlIdGenerator } from '@elastic/eui';
import { TimeRange } from '@kbn/data-plugin/common/query';
import { EuiFieldNumber, EuiFormRow, htmlIdGenerator, formatDate } from '@elastic/eui';
import type { Query, TimeRange } from '@kbn/es-query';
import useObservable from 'react-use/lib/useObservable';
import { TimeRangeBounds } from '../../util/time_buckets';
import {
AppStateSelectedCells,
getSelectionInfluencers,
getSelectionTimeRange,
} from '../explorer_utils';
import { isDefined } from '../../../../common/types/guards';
import { useAnomalyExplorerContext } from '../anomaly_explorer_context';
import { escapeKueryForFieldValuePair } from '../../util/string_utils';
import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search';
import { useDashboardTable } from './use_dashboards_table';
import { AddToDashboardControl } from './add_to_dashboard_controls';
import { useAddToDashboardActions } from './use_add_to_dashboard_actions';
import { AppStateSelectedCells, getSelectionTimeRange } from '../explorer_utils';
import { DEFAULT_MAX_SERIES_TO_PLOT } from '../../services/anomaly_explorer_charts_service';
import { JobId } from '../../../../common/types/anomaly_detection_jobs';
import { ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE } from '../../../embeddables';
import { getDefaultExplorerChartsPanelTitle } from '../../../embeddables/anomaly_charts/anomaly_charts_embeddable';
import { TimeRangeBounds } from '../../util/time_buckets';
import { useTableSeverity } from '../../components/controls/select_severity';
import { MAX_ANOMALY_CHARTS_ALLOWED } from '../../../embeddables/anomaly_charts/anomaly_charts_initializer';
import { useTimeRangeUpdates } from '../../contexts/kibana/use_timefilter';
function getDefaultEmbeddablePanelConfig(jobIds: JobId[]) {
function getDefaultEmbeddablePanelConfig(jobIds: JobId[], queryString?: string) {
return {
id: htmlIdGenerator()(),
title: getDefaultExplorerChartsPanelTitle(jobIds),
title: getDefaultExplorerChartsPanelTitle(jobIds).concat(queryString ? `- ${queryString}` : ''),
};
}
export interface AddToDashboardControlProps {
jobIds: string[];
onClose: (callback?: () => Promise<void>) => void;
selectedCells?: AppStateSelectedCells | null;
bounds?: TimeRangeBounds;
interval?: number;
onClose: (callback?: () => Promise<void>) => void;
}
/**
@ -41,34 +51,83 @@ export interface AddToDashboardControlProps {
export const AddAnomalyChartsToDashboardControl: FC<AddToDashboardControlProps> = ({
onClose,
jobIds,
selectedCells,
bounds,
interval,
bounds,
}) => {
const [severity] = useTableSeverity();
const [maxSeriesToPlot, setMaxSeriesToPlot] = useState(DEFAULT_MAX_SERIES_TO_PLOT);
const { anomalyExplorerCommonStateService, anomalyTimelineStateService, chartsStateService } =
useAnomalyExplorerContext();
const { queryString } = useObservable(
anomalyExplorerCommonStateService.getFilterSettings$(),
anomalyExplorerCommonStateService.getFilterSettings()
);
const globalTimeRange = useTimeRangeUpdates();
const getEmbeddableInput = useCallback(() => {
let timeRange: TimeRange | undefined;
const selectedCells = useObservable(
anomalyTimelineStateService.getSelectedCells$(),
anomalyTimelineStateService.getSelectedCells()
);
const chartsData = useObservable(
chartsStateService.getChartsData$(),
chartsStateService.getChartsData()
);
const timeRangeToPlot: TimeRange = useMemo(() => {
if (chartsData.seriesToPlot.length > 0) {
return {
from: formatDate(chartsData.seriesToPlot[0].plotEarliest, 'MMM D, YYYY @ HH:mm:ss.SSS'),
to: formatDate(chartsData.seriesToPlot[0].plotLatest, 'MMM D, YYYY @ HH:mm:ss.SSS'),
} as TimeRange;
}
if (!!selectedCells && interval !== undefined && bounds !== undefined) {
const { earliestMs, latestMs } = getSelectionTimeRange(selectedCells, bounds);
timeRange = {
return {
from: formatDate(earliestMs, 'MMM D, YYYY @ HH:mm:ss.SSS'),
to: formatDate(latestMs, 'MMM D, YYYY @ HH:mm:ss.SSS'),
mode: 'absolute',
};
}
const config = getDefaultEmbeddablePanelConfig(jobIds);
return globalTimeRange;
}, [chartsData.seriesToPlot, globalTimeRange, selectedCells, bounds, interval]);
const getEmbeddableInput = useCallback(() => {
// Respect the query and the influencers selected
// If no query or filter set, filter out to the lanes the selected cells
// And if no selected cells, show everything
const selectionInfluencers = getSelectionInfluencers(
selectedCells,
selectedCells?.viewByFieldName!
);
const influencers = selectionInfluencers ?? [];
const config = getDefaultEmbeddablePanelConfig(jobIds, queryString);
const queryFromSelectedCells = influencers
.map((s) => escapeKueryForFieldValuePair(s.fieldName, s.fieldValue))
.join(' or ');
// When adding anomaly charts to Dashboard, we want to respect the Dashboard's time range
// so we are not passing the time range here
return {
...config,
jobIds,
maxSeriesToPlot: maxSeriesToPlot ?? DEFAULT_MAX_SERIES_TO_PLOT,
severityThreshold: severity.val,
...(timeRange ?? {}),
timeRange: timeRangeToPlot,
...((isDefined(queryString) && queryString !== '') ||
(queryFromSelectedCells !== undefined && queryFromSelectedCells !== '')
? {
query: {
query: queryString === '' ? queryFromSelectedCells : queryString,
language: SEARCH_QUERY_LANGUAGE.KUERY,
} as Query,
}
: {}),
};
}, [selectedCells, interval, bounds, jobIds, maxSeriesToPlot, severity]);
}, [jobIds, maxSeriesToPlot, severity, queryString, selectedCells, timeRangeToPlot]);
const { dashboardItems, isLoading, search } = useDashboardTable();
const { addToDashboardAndEditCallback } = useAddToDashboardActions(

View file

@ -16,6 +16,8 @@ import {
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { DashboardSavedObject } from '@kbn/dashboard-plugin/public';
import type { Query } from '@kbn/es-query';
import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search';
import { getDefaultSwimlanePanelTitle } from '../../../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable';
import { SWIMLANE_TYPE, SwimlaneType } from '../explorer_constants';
import { JobId } from '../../../../common/types/anomaly_detection_jobs';
@ -33,9 +35,9 @@ export interface DashboardItem {
export type EuiTableProps = EuiInMemoryTableProps<DashboardItem>;
function getDefaultEmbeddablePanelConfig(jobIds: JobId[]) {
function getDefaultEmbeddablePanelConfig(jobIds: JobId[], queryString?: string) {
return {
title: getDefaultSwimlanePanelTitle(jobIds),
title: getDefaultSwimlanePanelTitle(jobIds).concat(queryString ? `- ${queryString}` : ''),
id: htmlIdGenerator()(),
};
}
@ -44,6 +46,7 @@ interface AddToDashboardControlProps {
jobIds: JobId[];
viewBy: string;
onClose: (callback?: () => Promise<void>) => void;
queryString?: string;
}
/**
@ -53,19 +56,23 @@ export const AddSwimlaneToDashboardControl: FC<AddToDashboardControlProps> = ({
onClose,
jobIds,
viewBy,
queryString,
}) => {
const { dashboardItems, isLoading, search } = useDashboardTable();
const [selectedSwimlane, setSelectedSwimlane] = useState<SwimlaneType>(SWIMLANE_TYPE.OVERALL);
const getEmbeddableInput = useCallback(() => {
const config = getDefaultEmbeddablePanelConfig(jobIds);
const config = getDefaultEmbeddablePanelConfig(jobIds, queryString);
return {
...config,
jobIds,
swimlaneType: selectedSwimlane,
...(selectedSwimlane === SWIMLANE_TYPE.VIEW_BY ? { viewBy } : {}),
...(queryString !== undefined
? { query: { query: queryString, language: SEARCH_QUERY_LANGUAGE.KUERY } as Query }
: {}),
};
}, [selectedSwimlane]);

View file

@ -7,7 +7,6 @@
import './_index.scss';
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { escapeKuery } from '@kbn/es-query';
import {
EuiButtonEmpty,
@ -46,6 +45,7 @@ import { EmbeddedMapComponentWrapper } from './explorer_chart_embedded_map';
import { useActiveCursor } from '@kbn/charts-plugin/public';
import { Chart, Settings } from '@elastic/charts';
import useObservable from 'react-use/lib/useObservable';
import { escapeKueryForFieldValuePair } from '../../util/string_utils';
const textTooManyBuckets = i18n.translate('xpack.ml.explorer.charts.tooManyBucketsDescription', {
defaultMessage:
@ -67,7 +67,7 @@ const openInMapsPluginMessage = i18n.translate('xpack.ml.explorer.charts.openInM
export function getEntitiesQuery(series) {
const queryString = series.entityFields
?.map(({ fieldName, fieldValue }) => `${escapeKuery(fieldName)}:${escapeKuery(fieldValue)}`)
?.map(({ fieldName, fieldValue }) => escapeKueryForFieldValuePair(fieldName, fieldValue))
.join(' or ');
const query = {
language: SEARCH_QUERY_LANGUAGE.KUERY,

View file

@ -11,6 +11,8 @@
import d3 from 'd3';
import he from 'he';
import { escapeKuery } from '@kbn/es-query';
import { isDefined } from '../../../common/types/guards';
import { CustomUrlAnomalyRecordDoc } from '../../../common/types/custom_urls';
import { Detector } from '../../../common/types/anomaly_detection_jobs';
@ -129,6 +131,14 @@ export function escapeForElasticsearchQuery(str: string): string {
return String(str).replace(/[-[\]{}()+!<>=?:\/\\^"~*&|\s]/g, '\\$&');
}
export function escapeKueryForFieldValuePair(
name: string,
value: string | number | boolean | undefined
): string {
if (!isDefined(name) || !isDefined(value)) return '';
return `${escapeKuery(name)}:${escapeKuery(value.toString())}`;
}
export function calculateTextWidth(txt: string | number, isNumber: boolean) {
txt = isNumber && typeof txt === 'number' ? d3.format(',')(txt) : txt;

View file

@ -13,12 +13,15 @@ import {
Query,
toElasticsearchQuery,
} from '@kbn/es-query';
import { getDefaultQuery } from '@kbn/data-plugin/public';
export function processFilters(
filters: Filter[],
query: Query,
optionalFilters?: Filter[],
optionalQuery?: Query,
controlledBy?: string
): estypes.QueryDslQueryContainer {
const filters = optionalFilters ?? [];
const query = optionalQuery ?? getDefaultQuery();
const inputQuery =
query.language === 'kuery'
? toElasticsearchQuery(fromKueryExpression(query.query as string))

View file

@ -71,7 +71,7 @@ export function SwimLaneProvider({ getService }: FtrProviderContext) {
return {
async getDebugState(testSubj: string): Promise<HeatmapDebugState> {
const state = await elasticChart.getChartDebugData(testSubj);
const state = await elasticChart.getChartDebugData(testSubj, 0, 5000);
if (!state) {
throw new Error('Swim lane debug state is not available');
}