[ML] Fix links to Discover and Maps and custom URLs for jobs with a query in the datafeed (#141871)

Co-authored-by: Dima Arnautov <arnautov.dima@gmail.com>
This commit is contained in:
Quynh Nguyen (Quinn) 2022-09-29 12:28:24 -05:00 committed by GitHub
parent b41a07f85a
commit 5d31e88c5d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 177 additions and 10 deletions

View file

@ -0,0 +1,94 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getFiltersForDSLQuery } from './get_filters_for_datafeed_query';
describe('getFiltersForDSLQuery', () => {
describe('when DSL query contains match_all', () => {
test('returns empty array when query contains a must clause that contains match_all', () => {
const actual = getFiltersForDSLQuery(
{ bool: { must: [{ match_all: {} }] } },
'dataview-id',
'test-alias'
);
expect(actual).toEqual([]);
});
test('returns empty array when query contains match_all', () => {
const actual = getFiltersForDSLQuery({ match_all: {} }, 'dataview-id', 'test-alias');
expect(actual).toEqual([]);
});
});
describe('when DSL query is valid', () => {
const query = {
bool: {
must: [],
filter: [
{
range: {
'@timestamp': {
format: 'strict_date_optional_time',
gte: '2007-09-29T15:05:14.509Z',
lte: '2022-09-29T15:05:14.509Z',
},
},
},
{
match_phrase: {
response_code: '200',
},
},
],
should: [],
must_not: [],
},
};
test('returns filters with alias', () => {
const actual = getFiltersForDSLQuery(query, 'dataview-id', 'test-alias');
expect(actual).toEqual([
{
$state: { store: 'appState' },
meta: {
alias: 'test-alias',
disabled: false,
index: 'dataview-id',
negate: false,
type: 'custom',
value:
'{"bool":{"must":[],"filter":[{"range":{"@timestamp":{"format":"strict_date_optional_time","gte":"2007-09-29T15:05:14.509Z","lte":"2022-09-29T15:05:14.509Z"}}},{"match_phrase":{"response_code":"200"}}],"should":[],"must_not":[]}}',
},
query,
},
]);
});
test('returns empty array when dataViewId is invalid', () => {
const actual = getFiltersForDSLQuery(query, null, 'test-alias');
expect(actual).toEqual([]);
});
test('returns filter with no alias if alias is not provided', () => {
const actual = getFiltersForDSLQuery(query, 'dataview-id');
expect(actual).toEqual([
{
$state: { store: 'appState' },
meta: {
disabled: false,
index: 'dataview-id',
negate: false,
type: 'custom',
value:
'{"bool":{"must":[],"filter":[{"range":{"@timestamp":{"format":"strict_date_optional_time","gte":"2007-09-29T15:05:14.509Z","lte":"2022-09-29T15:05:14.509Z"}}},{"match_phrase":{"response_code":"200"}}],"should":[],"must_not":[]}}',
},
query,
},
]);
});
});
});

View file

@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import type { SerializableRecord } from '@kbn/utility-types';
import { FilterStateStore } from '@kbn/es-query';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { isEqual } from 'lodash';
const defaultEmptyQuery = { bool: { must: [{ match_all: {} }] } };
export const getFiltersForDSLQuery = (
datafeedQuery: QueryDslQueryContainer,
dataViewId: string | null,
alias?: string
) => {
if (
datafeedQuery &&
!isPopulatedObject(datafeedQuery, ['match_all']) &&
!isEqual(datafeedQuery, defaultEmptyQuery) &&
dataViewId !== null
) {
return [
{
meta: {
index: dataViewId,
...(!!alias ? { alias } : {}),
negate: false,
disabled: false,
type: 'custom',
value: JSON.stringify(datafeedQuery),
},
query: datafeedQuery as SerializableRecord,
$state: {
store: FilterStateStore.APP_STATE,
},
},
];
}
return [];
};

View file

@ -53,6 +53,7 @@ import { useMlKibana } from '../../contexts/kibana';
import { getFieldTypeFromMapping } from '../../services/mapping_service';
import type { AnomaliesTableRecord } from '../../../../common/types/anomalies';
import { getQueryStringForInfluencers } from './get_query_string_for_influencers';
import { getFiltersForDSLQuery } from './get_filters_for_datafeed_query';
interface LinksMenuProps {
anomaly: AnomaliesTableRecord;
bounds: TimeRangeBounds;
@ -78,7 +79,14 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
services: { data, share, application },
} = kibana;
const job = useMemo(() => {
return mlJobService.getJob(props.anomaly.jobId);
}, [props.anomaly.jobId]);
const getAnomaliesMapsLink = async (anomaly: AnomaliesTableRecord) => {
const index = job.datafeed_config.indices[0];
const dataViewId = await getDataViewIdFromName(index);
const initialLayers = getInitialAnomaliesLayers(anomaly.jobId);
const anomalyBucketStartMoment = moment(anomaly.source.timestamp).tz(getDateFormatTz());
const anomalyBucketStart = anomalyBucketStartMoment.toISOString();
@ -104,6 +112,7 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
},
}
: {}),
filters: getFiltersForDSLQuery(job.datafeed_config.query, dataViewId, job.job_id),
});
return location;
};
@ -112,6 +121,9 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
anomaly: AnomaliesTableRecord,
sourceIndicesWithGeoFields: SourceIndicesWithGeoFields
) => {
const index = job.datafeed_config.indices[0];
const dataViewId = await getDataViewIdFromName(index);
// Create a layer for each of the geoFields
const initialLayers = getInitialSourceIndexFieldLayers(
sourceIndicesWithGeoFields[anomaly.jobId]
@ -138,10 +150,18 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
);
const locator = share.url.locators.get(MAPS_APP_LOCATOR);
const filtersFromDatafeedQuery = getFiltersForDSLQuery(
job.datafeed_config.query,
dataViewId,
job.job_id
);
const location = await locator?.getLocation({
initialLayers,
timeRange,
filters: data.query.filterManager.getFilters(),
filters:
filtersFromDatafeedQuery.length > 0
? filtersFromDatafeedQuery
: data.query.filterManager.getFilters(),
...(anomaly.entityName && anomaly.entityValue
? {
query: {
@ -175,7 +195,6 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
}
const getDataViewId = async () => {
const job = mlJobService.getJob(props.anomaly.jobId);
const index = job.datafeed_config.indices[0];
const dataViewId = await getDataViewIdFromName(index);
@ -246,6 +265,7 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
language: 'kuery',
query: kqlQuery,
},
filters: getFiltersForDSLQuery(job.datafeed_config.query, dataViewId, job.job_id),
sort: [['timestamp, asc']],
});
@ -440,7 +460,6 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
const categoryId = props.anomaly.entityValue;
const record = props.anomaly.source;
const job = mlJobService.getJob(props.anomaly.jobId);
if (job === undefined) {
// eslint-disable-next-line no-console
console.log(`viewExamples(): no job found with ID: ${props.anomaly.jobId}`);
@ -545,7 +564,7 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
const appStateProps: RisonValue = {
index: dataViewId,
filters: [],
filters: getFiltersForDSLQuery(job.datafeed_config.query, dataViewId, job.job_id),
};
if (query !== null) {
appStateProps.query = query;

View file

@ -20,6 +20,7 @@ import { getSavedObjectsClient, getDashboard } from '../../../util/dependency_ca
import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
import { cleanEmptyKeys } from '@kbn/dashboard-plugin/public';
import { isFilterPinned } from '@kbn/es-query';
import { getFiltersForDSLQuery } from '../../../components/anomalies_table/get_filters_for_datafeed_query';
export function getNewCustomUrlDefaults(job, dashboards, dataViews) {
// Returns the settings object in the format used by the custom URL editor
@ -50,6 +51,11 @@ export function getNewCustomUrlDefaults(job, dashboards, dataViews) {
const indicesName = datafeedConfig.indices.join();
const defaultDataViewId = dataViews.find((dv) => dv.title === indicesName)?.id;
kibanaSettings.discoverIndexPatternId = defaultDataViewId;
kibanaSettings.filters = getFiltersForDSLQuery(
job.datafeed_config.query,
defaultDataViewId,
job.job_id
);
}
return {
@ -133,16 +139,18 @@ async function buildDashboardUrlFromSettings(settings) {
const response = await savedObjectsClient.get('dashboard', dashboardId);
// Use the filters from the saved dashboard if there are any.
let filters = [];
// Query from the datafeed config will be saved as custom filters
// Use them if there are set.
let filters = settings.kibanaSettings.filters;
// Use the query from the dashboard only if no job entities are selected.
let query = undefined;
// Override with filters and queries from saved dashboard if they are available.
const searchSourceJSON = response.get('kibanaSavedObjectMeta.searchSourceJSON');
if (searchSourceJSON !== undefined) {
const searchSourceData = JSON.parse(searchSourceJSON);
if (searchSourceData.filter !== undefined) {
if (Array.isArray(searchSourceData.filter) && searchSourceData.filter.length > 0) {
filters = searchSourceData.filter;
}
query = searchSourceData.query;
@ -196,7 +204,7 @@ async function buildDashboardUrlFromSettings(settings) {
}
function buildDiscoverUrlFromSettings(settings) {
const { discoverIndexPatternId, queryFieldNames } = settings.kibanaSettings;
const { discoverIndexPatternId, queryFieldNames, filters } = settings.kibanaSettings;
// Add time settings to the global state URL parameter with $earliest$ and
// $latest$ tokens which get substituted for times around the time of the
@ -212,6 +220,7 @@ function buildDiscoverUrlFromSettings(settings) {
// Add the index pattern and query to the appState part of the URL.
const appState = {
index: discoverIndexPatternId,
filters,
};
// If partitioning field entities have been configured add tokens

View file

@ -111,7 +111,7 @@ export function MachineLearningCustomUrlsProvider({
);
expect(actualLabel).to.eql(
expectedLabel,
`Expected custom url item to be '${expectedLabel}' (got '${actualLabel}')`
`Expected custom url label to be '${expectedLabel}' (got '${actualLabel}')`
);
},
@ -123,7 +123,7 @@ export function MachineLearningCustomUrlsProvider({
);
expect(actualUrl).to.eql(
expectedUrl,
`Expected custom url item to be '${expectedUrl}' (got '${actualUrl}')`
`Expected custom url value to be '${expectedUrl}' (got '${actualUrl}')`
);
},