mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
764d6389bd
commit
6f80844a1d
10 changed files with 115 additions and 32 deletions
|
@ -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');
|
||||
|
|
|
@ -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}
|
||||
</>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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]);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue