mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
b41a07f85a
commit
5d31e88c5d
5 changed files with 177 additions and 10 deletions
|
@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 [];
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}')`
|
||||
);
|
||||
},
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue