mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[ML] Fix Anomaly charts time and query not being added to dashboard and case (#140147)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
c5debde7b7
commit
5dcec78d38
17 changed files with 299 additions and 132 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');
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { cloneDeep } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import rison, { RisonValue } from 'rison-node';
|
||||
import { escapeKuery } from '@kbn/es-query';
|
||||
import React, { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { APP_ID as MAPS_APP_ID } from '@kbn/maps-plugin/common';
|
||||
import {
|
||||
|
@ -32,7 +31,7 @@ import {
|
|||
} from '../../../../common/util/date_utils';
|
||||
import { parseInterval } from '../../../../common/util/parse_interval';
|
||||
import { ml } from '../../services/ml_api_service';
|
||||
import { replaceStringTokens } from '../../util/string_utils';
|
||||
import { escapeKueryForFieldValuePair, replaceStringTokens } from '../../util/string_utils';
|
||||
import { getUrlForRecord, openCustomUrlWindow } from '../../util/custom_url_utils';
|
||||
import { ML_APP_LOCATOR, ML_PAGES } from '../../../../common/constants/locator';
|
||||
import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search';
|
||||
|
@ -101,7 +100,7 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
|
|||
? {
|
||||
query: {
|
||||
language: SEARCH_QUERY_LANGUAGE.KUERY,
|
||||
query: `${escapeKuery(anomaly.entityName)}:${escapeKuery(anomaly.entityValue)}`,
|
||||
query: escapeKueryForFieldValuePair(anomaly.entityName, anomaly.entityValue),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
|
@ -147,7 +146,7 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
|
|||
? {
|
||||
query: {
|
||||
language: SEARCH_QUERY_LANGUAGE.KUERY,
|
||||
query: `${escapeKuery(anomaly.entityName)}:${escapeKuery(anomaly.entityValue)}${
|
||||
query: `${escapeKueryForFieldValuePair(anomaly.entityName, anomaly.entityValue)}${
|
||||
influencersQueryString !== '' ? ` and (${influencersQueryString})` : ''
|
||||
}`,
|
||||
},
|
||||
|
|
|
@ -13,15 +13,27 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiPopover,
|
||||
EuiPopoverTitle,
|
||||
formatDate,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import type { Query, TimeRange } from '@kbn/es-query';
|
||||
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 { useCasesModal } from '../contexts/kibana/use_cases_modal';
|
||||
import { DEFAULT_MAX_SERIES_TO_PLOT } from '../services/anomaly_explorer_charts_service';
|
||||
import { ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE } from '../../embeddables';
|
||||
import { useTimeRangeUpdates } from '../contexts/kibana/use_timefilter';
|
||||
import { useMlKibana } from '../contexts/kibana';
|
||||
import type { AppStateSelectedCells, ExplorerJob } from './explorer_utils';
|
||||
import {
|
||||
AppStateSelectedCells,
|
||||
ExplorerJob,
|
||||
getSelectionInfluencers,
|
||||
getSelectionTimeRange,
|
||||
} from './explorer_utils';
|
||||
import { TimeRangeBounds } from '../util/time_buckets';
|
||||
import { AddAnomalyChartsToDashboardControl } from './dashboard_controls/add_anomaly_charts_to_dashboard_controls';
|
||||
|
||||
|
@ -48,7 +60,6 @@ export const AnomalyContextMenu: FC<AnomalyContextMenuProps> = ({
|
|||
const globalTimeRange = useTimeRangeUpdates(true);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isAddDashboardsActive, setIsAddDashboardActive] = useState(false);
|
||||
|
||||
const closePopoverOnAction = useCallback(
|
||||
(actionCallback: Function) => {
|
||||
setIsMenuOpen(false);
|
||||
|
@ -62,6 +73,36 @@ export const AnomalyContextMenu: FC<AnomalyContextMenuProps> = ({
|
|||
const canEditDashboards = capabilities.dashboard?.createNew ?? false;
|
||||
const casesPrivileges = cases?.helpers.canUseCases();
|
||||
|
||||
const { anomalyExplorerCommonStateService, chartsStateService } = useAnomalyExplorerContext();
|
||||
const { queryString } = useObservable(
|
||||
anomalyExplorerCommonStateService.getFilterSettings$(),
|
||||
anomalyExplorerCommonStateService.getFilterSettings()
|
||||
);
|
||||
|
||||
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);
|
||||
return {
|
||||
from: formatDate(earliestMs, 'MMM D, YYYY @ HH:mm:ss.SSS'),
|
||||
to: formatDate(latestMs, 'MMM D, YYYY @ HH:mm:ss.SSS'),
|
||||
mode: 'absolute',
|
||||
};
|
||||
}
|
||||
|
||||
return globalTimeRange;
|
||||
}, [chartsData.seriesToPlot, globalTimeRange, selectedCells, bounds, interval]);
|
||||
|
||||
const menuItems = useMemo(() => {
|
||||
const items = [];
|
||||
if (canEditDashboards) {
|
||||
|
@ -80,6 +121,17 @@ export const AnomalyContextMenu: FC<AnomalyContextMenuProps> = ({
|
|||
}
|
||||
|
||||
if (!!casesPrivileges?.create || !!casesPrivileges?.update) {
|
||||
const selectionInfluencers = getSelectionInfluencers(
|
||||
selectedCells,
|
||||
selectedCells?.viewByFieldName!
|
||||
);
|
||||
|
||||
const queryFromSelectedCells = Array.isArray(selectionInfluencers)
|
||||
? selectionInfluencers
|
||||
.map((s) => escapeKueryForFieldValuePair(s.fieldName, s.fieldValue))
|
||||
.join(' or ')
|
||||
: '';
|
||||
|
||||
items.push(
|
||||
<EuiContextMenuItem
|
||||
key="attachToCase"
|
||||
|
@ -87,8 +139,16 @@ export const AnomalyContextMenu: FC<AnomalyContextMenuProps> = ({
|
|||
null,
|
||||
openCasesModal.bind(null, {
|
||||
jobIds: selectedJobs?.map((v) => v.id),
|
||||
timeRange: globalTimeRange,
|
||||
timeRange: timeRangeToPlot,
|
||||
maxSeriesToPlot: DEFAULT_MAX_SERIES_TO_PLOT,
|
||||
...((isDefined(queryString) && queryString !== '') || queryFromSelectedCells !== ''
|
||||
? {
|
||||
query: {
|
||||
query: queryString === '' ? queryFromSelectedCells : queryString,
|
||||
language: SEARCH_QUERY_LANGUAGE.KUERY,
|
||||
} as Query,
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
)}
|
||||
data-test-subj="mlAnomalyAttachChartsToCasesButton"
|
||||
|
@ -99,7 +159,15 @@ export const AnomalyContextMenu: FC<AnomalyContextMenuProps> = ({
|
|||
}
|
||||
return items;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [canEditDashboards, globalTimeRange, closePopoverOnAction, selectedJobs]);
|
||||
}, [
|
||||
canEditDashboards,
|
||||
globalTimeRange,
|
||||
closePopoverOnAction,
|
||||
selectedJobs,
|
||||
selectedCells,
|
||||
queryString,
|
||||
timeRangeToPlot,
|
||||
]);
|
||||
|
||||
const jobIds = selectedJobs.map(({ id }) => id);
|
||||
|
||||
|
@ -140,9 +208,6 @@ export const AnomalyContextMenu: FC<AnomalyContextMenuProps> = ({
|
|||
onClose={async () => {
|
||||
setIsAddDashboardActive(false);
|
||||
}}
|
||||
selectedCells={selectedCells}
|
||||
bounds={bounds}
|
||||
interval={interval}
|
||||
jobIds={jobIds}
|
||||
/>
|
||||
) : null}
|
||||
|
|
|
@ -26,6 +26,8 @@ import { i18n } from '@kbn/i18n';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import { SEARCH_QUERY_LANGUAGE } from '../../../common/constants/search';
|
||||
import { useCasesModal } from '../contexts/kibana/use_cases_modal';
|
||||
import { useTimeRangeUpdates } from '../contexts/kibana/use_timefilter';
|
||||
import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '../..';
|
||||
|
@ -94,7 +96,7 @@ export const AnomalyTimeline: FC<AnomalyTimelineProps> = React.memo(
|
|||
|
||||
const { overallAnnotations } = explorerState;
|
||||
|
||||
const { filterActive } = useObservable(
|
||||
const { filterActive, queryString } = useObservable(
|
||||
anomalyExplorerCommonStateService.getFilterSettings$(),
|
||||
anomalyExplorerCommonStateService.getFilterSettings()
|
||||
);
|
||||
|
@ -171,9 +173,17 @@ export const AnomalyTimeline: FC<AnomalyTimelineProps> = React.memo(
|
|||
...(swimLaneType === SWIMLANE_TYPE.VIEW_BY ? { viewBy: viewBySwimlaneFieldName } : {}),
|
||||
jobIds: selectedJobs?.map((v) => v.id),
|
||||
timeRange: globalTimeRange,
|
||||
...(isDefined(queryString) && queryString !== ''
|
||||
? {
|
||||
query: {
|
||||
query: queryString,
|
||||
language: SEARCH_QUERY_LANGUAGE.KUERY,
|
||||
} as Query,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
},
|
||||
[openCasesModalCallback, selectedJobs, globalTimeRange, viewBySwimlaneFieldName]
|
||||
[openCasesModalCallback, selectedJobs, globalTimeRange, viewBySwimlaneFieldName, queryString]
|
||||
);
|
||||
|
||||
const annotations = useMemo(() => overallAnnotations.annotationsData, [overallAnnotations]);
|
||||
|
@ -513,6 +523,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
|
||||
|
|
|
@ -6,32 +6,33 @@
|
|||
*/
|
||||
import React, { FC, useCallback, 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 } from '@elastic/eui';
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { getSelectionInfluencers } 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';
|
||||
|
||||
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[];
|
||||
selectedCells?: AppStateSelectedCells | null;
|
||||
bounds?: TimeRangeBounds;
|
||||
interval?: number;
|
||||
onClose: (callback?: () => Promise<void>) => void;
|
||||
}
|
||||
|
||||
|
@ -41,34 +42,55 @@ export interface AddToDashboardControlProps {
|
|||
export const AddAnomalyChartsToDashboardControl: FC<AddToDashboardControlProps> = ({
|
||||
onClose,
|
||||
jobIds,
|
||||
selectedCells,
|
||||
bounds,
|
||||
interval,
|
||||
}) => {
|
||||
const [severity] = useTableSeverity();
|
||||
const [maxSeriesToPlot, setMaxSeriesToPlot] = useState(DEFAULT_MAX_SERIES_TO_PLOT);
|
||||
const { anomalyExplorerCommonStateService, anomalyTimelineStateService } =
|
||||
useAnomalyExplorerContext();
|
||||
const { queryString } = useObservable(
|
||||
anomalyExplorerCommonStateService.getFilterSettings$(),
|
||||
anomalyExplorerCommonStateService.getFilterSettings()
|
||||
);
|
||||
|
||||
const selectedCells = useObservable(
|
||||
anomalyTimelineStateService.getSelectedCells$(),
|
||||
anomalyTimelineStateService.getSelectedCells()
|
||||
);
|
||||
|
||||
const getEmbeddableInput = useCallback(() => {
|
||||
let timeRange: TimeRange | undefined;
|
||||
if (!!selectedCells && interval !== undefined && bounds !== undefined) {
|
||||
const { earliestMs, latestMs } = getSelectionTimeRange(selectedCells, bounds);
|
||||
timeRange = {
|
||||
from: formatDate(earliestMs, 'MMM D, YYYY @ HH:mm:ss.SSS'),
|
||||
to: formatDate(latestMs, 'MMM D, YYYY @ HH:mm:ss.SSS'),
|
||||
mode: 'absolute',
|
||||
};
|
||||
}
|
||||
// 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 config = getDefaultEmbeddablePanelConfig(jobIds);
|
||||
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 ?? {}),
|
||||
...((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]);
|
||||
|
||||
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 }
|
||||
: {}),
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [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;
|
||||
|
||||
|
|
|
@ -29,34 +29,43 @@ export const initComponent = memoize(
|
|||
const inputProps =
|
||||
persistableStateAttachmentState as unknown as AnomalyChartsEmbeddableInput;
|
||||
|
||||
const listItems = [
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.anomalyCharts.description.jobIdsLabel"
|
||||
defaultMessage="Job IDs"
|
||||
/>
|
||||
),
|
||||
description: inputProps.jobIds.join(', '),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.anomalyCharts.description.timeRangeLabel"
|
||||
defaultMessage="Time range"
|
||||
/>
|
||||
),
|
||||
description: `${dataFormatter.convert(
|
||||
inputProps.timeRange.from
|
||||
)} - ${dataFormatter.convert(inputProps.timeRange.to)}`,
|
||||
},
|
||||
];
|
||||
|
||||
if (typeof inputProps.query?.query === 'string' && inputProps.query?.query !== '') {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.anomalySwimLane.description.queryLabel"
|
||||
defaultMessage="Query"
|
||||
/>
|
||||
),
|
||||
description: inputProps.query?.query,
|
||||
});
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<EuiDescriptionList
|
||||
compressed
|
||||
type={'inline'}
|
||||
listItems={[
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.anomalyCharts.description.jobIdsLabel"
|
||||
defaultMessage="Job IDs"
|
||||
/>
|
||||
),
|
||||
description: inputProps.jobIds.join(', '),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.anomalyCharts.description.timeRangeLabel"
|
||||
defaultMessage="Time range"
|
||||
/>
|
||||
),
|
||||
description: `${dataFormatter.convert(
|
||||
inputProps.timeRange.from
|
||||
)} - ${dataFormatter.convert(inputProps.timeRange.to)}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<EuiDescriptionList compressed type={'inline'} listItems={listItems} />
|
||||
<EmbeddableComponent {...inputProps} />
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -28,47 +28,57 @@ export const initComponent = memoize(
|
|||
const inputProps =
|
||||
persistableStateAttachmentState as unknown as AnomalySwimlaneEmbeddableInput;
|
||||
|
||||
const listItems = [
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.anomalySwimLane.description.jobIdsLabel"
|
||||
defaultMessage="Job IDs"
|
||||
/>
|
||||
),
|
||||
description: inputProps.jobIds.join(', '),
|
||||
},
|
||||
...(inputProps.viewBy
|
||||
? [
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.anomalySwimLane.description.viewByLabel"
|
||||
defaultMessage="View by"
|
||||
/>
|
||||
),
|
||||
description: inputProps.viewBy,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.anomalySwimLane.description.timeRangeLabel"
|
||||
defaultMessage="Time range"
|
||||
/>
|
||||
),
|
||||
description: `${dataFormatter.convert(
|
||||
inputProps.timeRange.from
|
||||
)} - ${dataFormatter.convert(inputProps.timeRange.to)}`,
|
||||
},
|
||||
];
|
||||
|
||||
if (typeof inputProps.query?.query === 'string' && inputProps.query?.query !== '') {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.anomalySwimLane.description.queryLabel"
|
||||
defaultMessage="Query"
|
||||
/>
|
||||
),
|
||||
description: inputProps.query?.query,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiDescriptionList
|
||||
compressed
|
||||
type={'inline'}
|
||||
listItems={[
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.anomalySwimLane.description.jobIdsLabel"
|
||||
defaultMessage="Job IDs"
|
||||
/>
|
||||
),
|
||||
description: inputProps.jobIds.join(', '),
|
||||
},
|
||||
...(inputProps.viewBy
|
||||
? [
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.anomalySwimLane.description.viewByLabel"
|
||||
defaultMessage="View by"
|
||||
/>
|
||||
),
|
||||
description: inputProps.viewBy,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.cases.anomalySwimLane.description.timeRangeLabel"
|
||||
defaultMessage="Time range"
|
||||
/>
|
||||
),
|
||||
description: `${dataFormatter.convert(
|
||||
inputProps.timeRange.from
|
||||
)} - ${dataFormatter.convert(inputProps.timeRange.to)}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<EuiDescriptionList compressed type={'inline'} listItems={listItems} />
|
||||
<EmbeddableComponent {...inputProps} />
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useCallback, useState, useMemo, useEffect } from 'react';
|
||||
import React, { FC, useCallback, useState, useMemo, useEffect, useRef } from 'react';
|
||||
import { EuiCallOut, EuiLoadingChart, EuiResizeObserver, EuiText } from '@elastic/eui';
|
||||
import { Observable } from 'rxjs';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
@ -117,16 +117,30 @@ export const EmbeddableAnomalyChartsContainer: FC<EmbeddableAnomalyChartsContain
|
|||
severity.val,
|
||||
{ onRenderComplete, onError, onLoading }
|
||||
);
|
||||
|
||||
// Holds the container height for previously fetched data
|
||||
const containerHeightRef = useRef<number>();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const resizeHandler = useCallback(
|
||||
throttle((e: { width: number; height: number }) => {
|
||||
// Keep previous container height so it doesn't change the page layout
|
||||
if (!isExplorerLoading) {
|
||||
containerHeightRef.current = e.height;
|
||||
}
|
||||
|
||||
if (Math.abs(chartWidth - e.width) > 20) {
|
||||
setChartWidth(e.width);
|
||||
}
|
||||
}, RESIZE_THROTTLE_TIME_MS),
|
||||
[chartWidth]
|
||||
[!isExplorerLoading, chartWidth]
|
||||
);
|
||||
|
||||
const containerHeight = useMemo(() => {
|
||||
// Persists container height during loading to prevent page from jumping
|
||||
return isExplorerLoading ? containerHeightRef.current : undefined;
|
||||
}, [isExplorerLoading]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<EuiCallOut
|
||||
|
@ -173,6 +187,7 @@ export const EmbeddableAnomalyChartsContainer: FC<EmbeddableAnomalyChartsContain
|
|||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
padding: '8px',
|
||||
height: containerHeight,
|
||||
}}
|
||||
data-test-subj={`mlExplorerEmbeddable_${embeddableContext.id}`}
|
||||
ref={resizeRef}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -5,10 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { stringHash } from '@kbn/ml-string-hash';
|
||||
import type { Job, Datafeed } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs';
|
||||
import type { AnomalySwimlaneEmbeddableInput } from '@kbn/ml-plugin/public';
|
||||
import type { AnomalyChartsEmbeddableInput } from '@kbn/ml-plugin/public/embeddables';
|
||||
import type {
|
||||
AnomalyChartsEmbeddableInput,
|
||||
AnomalySwimlaneEmbeddableInput,
|
||||
} from '@kbn/ml-plugin/public/embeddables';
|
||||
import { stringHash } from '@kbn/ml-string-hash';
|
||||
import type { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { USER } from '../../../services/ml/security_common';
|
||||
|
||||
|
@ -480,14 +482,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
const expectedAttachment = {
|
||||
jobIds: [testData.jobConfig.job_id],
|
||||
timeRange: {
|
||||
from: '2016-02-07T00:00:00.000Z',
|
||||
to: '2016-02-11T23:59:54.000Z',
|
||||
},
|
||||
maxSeriesToPlot: 6,
|
||||
} as AnomalyChartsEmbeddableInput;
|
||||
|
||||
expectedAttachment.id = stringHash(JSON.stringify(expectedAttachment)).toString();
|
||||
// @ts-expect-error Setting id to be undefined here
|
||||
// since time range expected is of the chart plotEarliest/plotLatest, not of the global time range
|
||||
// but, chart time range might vary depends on the time of the test
|
||||
// we don't know the hashed string id for sure
|
||||
expectedAttachment.id = undefined;
|
||||
|
||||
await ml.cases.assertCaseWithAnomalyChartsAttachment(
|
||||
{
|
||||
|
|
|
@ -13,20 +13,31 @@ export type MlAnomalyCharts = ProvidedType<typeof AnomalyChartsProvider>;
|
|||
|
||||
export function AnomalyChartsProvider({ getService }: FtrProviderContext) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
const retry = getService('retry');
|
||||
|
||||
return {
|
||||
async assertAnomalyExplorerChartsCount(
|
||||
chartsContainerSubj: string,
|
||||
chartsContainerSubj: string | undefined,
|
||||
expectedChartsCount: number
|
||||
) {
|
||||
const chartsContainer = await testSubjects.find(chartsContainerSubj);
|
||||
const actualChartsCount = (
|
||||
await chartsContainer.findAllByClassName('ml-explorer-chart-container', 3000)
|
||||
).length;
|
||||
expect(actualChartsCount).to.eql(
|
||||
expectedChartsCount,
|
||||
`Expect ${expectedChartsCount} charts to appear, got ${actualChartsCount}`
|
||||
);
|
||||
await retry.tryForTime(5000, async () => {
|
||||
// For anomaly charts, time range expected is of the chart plotEarliest/plotLatest
|
||||
// and not of the global time range
|
||||
// but since plot earliest & latest might vary depends on the current time
|
||||
// we don't know the exact hashed id for sure
|
||||
// so we find first available chart container if id is not provided
|
||||
const chartsContainer =
|
||||
chartsContainerSubj !== undefined
|
||||
? await testSubjects.find(chartsContainerSubj)
|
||||
: await testSubjects.find('mlExplorerChartsContainer');
|
||||
const actualChartsCount = (
|
||||
await chartsContainer?.findAllByClassName('ml-explorer-chart-container', 3000)
|
||||
).length;
|
||||
expect(actualChartsCount).to.eql(
|
||||
expectedChartsCount,
|
||||
`Expect ${expectedChartsCount} charts to appear, got ${actualChartsCount}`
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ export function MachineLearningCasesProvider(
|
|||
await testSubjects.existOrFail('comment-persistableState-ml_anomaly_charts');
|
||||
|
||||
await mlAnomalyCharts.assertAnomalyExplorerChartsCount(
|
||||
`mlExplorerEmbeddable_${attachment.id}`,
|
||||
attachment.id !== undefined ? `mlExplorerEmbeddable_${attachment.id}` : undefined,
|
||||
expectedChartsCount
|
||||
);
|
||||
},
|
||||
|
|
|
@ -74,7 +74,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