diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/__snapshots__/editor.test.tsx.snap b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/__snapshots__/editor.test.tsx.snap deleted file mode 100644 index 163bad28d8cc2..0000000000000 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/__snapshots__/editor.test.tsx.snap +++ /dev/null @@ -1,1083 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CustomUrlEditor renders the editor for a dashboard type URL with a label 1`] = ` - - -

- -

-
- - - - } - labelType="label" - > - - - - } - labelType="label" - > - - - - } - labelType="label" - > - - - - } - labelType="label" - > - - - - - - - } - labelType="label" - > - - - - - -
-`; - -exports[`CustomUrlEditor renders the editor for a discover type URL with an entity and empty time range interval 1`] = ` - - -

- -

-
- - - - } - labelType="label" - > - - - - } - labelType="label" - > - - - - } - labelType="label" - > - - - - } - labelType="label" - > - - - - - - - } - labelType="label" - > - - - - - - } - labelType="label" - > - - - - - -
-`; - -exports[`CustomUrlEditor renders the editor for a discover type URL with valid time range interval 1`] = ` - - -

- -

-
- - - - } - labelType="label" - > - - - - } - labelType="label" - > - - - - } - labelType="label" - > - - - - } - labelType="label" - > - - - - - - - } - labelType="label" - > - - - - - - } - labelType="label" - > - - - - - -
-`; - -exports[`CustomUrlEditor renders the editor for a new dashboard type URL with no label 1`] = ` - - -

- -

-
- - - - } - labelType="label" - > - - - - } - labelType="label" - > - - - - } - labelType="label" - > - - - - } - labelType="label" - > - - - - - - - } - labelType="label" - > - - - - - -
-`; - -exports[`CustomUrlEditor renders the editor for other type of URL with duplicate label 1`] = ` - - -

- -

-
- - - - } - labelType="label" - > - - - - } - labelType="label" - > - - - - } - labelType="label" - > - - - -
-`; - -exports[`CustomUrlEditor renders the editor for other type of URL with unique label 1`] = ` - - -

- -

-
- - - - } - labelType="label" - > - - - - } - labelType="label" - > - - - - } - labelType="label" - > - - - -
-`; diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.test.tsx b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.test.tsx deleted file mode 100644 index ce7f31df4e86c..0000000000000 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.test.tsx +++ /dev/null @@ -1,163 +0,0 @@ -/* - * 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. - */ - -// Mock the mlJobService that is used for testing custom URLs. -import { shallow } from 'enzyme'; - -jest.mock('../../../services/job_service', () => 'mlJobService'); - -import React from 'react'; - -import { CustomUrlEditor } from './editor'; -import { TIME_RANGE_TYPE, URL_TYPE } from './constants'; -import { CustomUrlSettings } from './utils'; -import { DataViewListItem } from '@kbn/data-views-plugin/common'; - -function prepareTest( - customUrl: CustomUrlSettings, - setEditCustomUrlFn: (url: CustomUrlSettings) => void -) { - const savedCustomUrls = [ - { - url_name: 'Show data', - time_range: 'auto', - url_value: - "discover#/?_g=(time:(from:'$earliest$',mode:absolute,to:'$latest$'))&_a=" + - '(index:e532ba80-b76f-11e8-a9dc-37914a458883,query:(language:lucene,query:\'airline:"$airline$"\'))', - }, - { - url_name: 'Show dashboard', - time_range: '1h', - url_value: - 'dashboards#/view/52ea8840-bbef-11e8-a04d-b1701b2b977e?_g=' + - "(time:(from:'$earliest$',mode:absolute,to:'$latest$'))&" + - '_a=(filters:!(),query:(language:lucene,query:\'airline:"$airline$"\'))', - }, - { - url_name: 'Show airline', - time_range: 'auto', - url_value: 'http://airlinecodes.info/airline-code-$airline$', - }, - ]; - - const dashboards = [ - { id: 'dash1', title: 'Dashboard 1' }, - { id: 'dash2', title: 'Dashboard 2' }, - ]; - - const dataViewListItems = [ - { id: 'pattern1', title: 'Data view 1' }, - { id: 'pattern2', title: 'Data view 2' }, - ] as DataViewListItem[]; - - const queryEntityFieldNames = ['airline']; - - const props = { - customUrl, - setEditCustomUrl: setEditCustomUrlFn, - savedCustomUrls, - dashboards, - dataViewListItems, - queryEntityFieldNames, - }; - - return shallow(); -} - -describe('CustomUrlEditor', () => { - const setEditCustomUrl = jest.fn(() => {}); - const dashboardUrl = { - label: '', - timeRange: { - type: TIME_RANGE_TYPE.AUTO, - interval: '', - }, - type: URL_TYPE.KIBANA_DASHBOARD, - kibanaSettings: { - queryFieldNames: [], - dashboardId: 'dash1', - }, - }; - - const discoverUrl = { - label: 'Open Discover', - timeRange: { - type: TIME_RANGE_TYPE.INTERVAL, - interval: '', - }, - type: URL_TYPE.KIBANA_DISCOVER, - kibanaSettings: { - queryFieldNames: ['airline'], - discoverIndexPatternId: 'pattern1', - }, - }; - - const otherUrl = { - label: 'Show airline', - timeRange: { - type: TIME_RANGE_TYPE.AUTO, - interval: '', - }, - type: URL_TYPE.OTHER, - otherUrlSettings: { - urlValue: 'https://www.google.co.uk/search?q=airline+code+$airline$', - }, - }; - - test('renders the editor for a new dashboard type URL with no label', () => { - const wrapper = prepareTest(dashboardUrl, setEditCustomUrl); - expect(wrapper).toMatchSnapshot(); - }); - - test('renders the editor for a dashboard type URL with a label', () => { - const dashboardUrlEdit = { - ...dashboardUrl, - label: 'Open Dashboard 1', - }; - const wrapper = prepareTest(dashboardUrlEdit, setEditCustomUrl); - expect(wrapper).toMatchSnapshot(); - }); - - test('renders the editor for a discover type URL with an entity and empty time range interval', () => { - const wrapper = prepareTest(discoverUrl, setEditCustomUrl); - expect(wrapper).toMatchSnapshot(); - }); - - test('renders the editor for a discover type URL with valid time range interval', () => { - const discoverUrlEdit = { - ...discoverUrl, - timeRange: { - type: TIME_RANGE_TYPE.INTERVAL, - interval: '1h', - }, - }; - const wrapper = prepareTest(discoverUrlEdit, setEditCustomUrl); - expect(wrapper).toMatchSnapshot(); - }); - - test('renders the editor for other type of URL with duplicate label', () => { - const wrapper = prepareTest(otherUrl, setEditCustomUrl); - expect(wrapper).toMatchSnapshot(); - }); - - test('renders the editor for other type of URL with unique label', () => { - const otherUrlEdit = { - ...otherUrl, - label: 'View airline', - }; - const wrapper = prepareTest(otherUrlEdit, setEditCustomUrl); - expect(wrapper).toMatchSnapshot(); - }); - - test('calls setEditCustomUrl on updating a custom URL field', () => { - const wrapper = prepareTest(dashboardUrl, setEditCustomUrl); - const labelInput = wrapper.find('EuiFieldText').first(); - labelInput.simulate('change', { target: { value: 'Edit' } }); - wrapper.update(); - expect(setEditCustomUrl).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.tsx b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.tsx index 9eddc02b5e1a4..315c60fab6a6f 100644 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.tsx +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { ChangeEvent, FC } from 'react'; +import React, { ChangeEvent, useMemo, useState, useRef, useEffect, FC } from 'react'; import { EuiComboBox, @@ -25,11 +25,16 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { DataViewListItem } from '@kbn/data-views-plugin/common'; +import { DataView } from '@kbn/data-views-plugin/public'; import { CustomUrlSettings, isValidCustomUrlSettingsTimeRange } from './utils'; import { isValidLabel } from '../../../util/custom_url_utils'; +import { type DataFrameAnalyticsConfig } from '../../../../../common/types/data_frame_analytics'; +import { Job, isAnomalyDetectionJob } from '../../../../../common/types/anomaly_detection_jobs'; import { TIME_RANGE_TYPE, TimeRangeType, URL_TYPE } from './constants'; import { UrlConfig } from '../../../../../common/types/custom_urls'; +import { useMlKibana } from '../../../contexts/kibana'; +import { getDropDownOptions } from './get_dropdown_options'; function getLinkToOptions() { return [ @@ -60,8 +65,8 @@ interface CustomUrlEditorProps { savedCustomUrls: UrlConfig[]; dashboards: Array<{ id: string; title: string }>; dataViewListItems: DataViewListItem[]; - queryEntityFieldNames: string[]; showTimeRangeSelector?: boolean; + job: Job | DataFrameAnalyticsConfig; } /* @@ -73,9 +78,42 @@ export const CustomUrlEditor: FC = ({ savedCustomUrls, dashboards, dataViewListItems, - queryEntityFieldNames, - showTimeRangeSelector = true, + job, }) => { + const [queryEntityFieldNames, setQueryEntityFieldNames] = useState([]); + const isAnomalyJob = useMemo(() => isAnomalyDetectionJob(job), [job]); + + const { + services: { + data: { dataViews }, + }, + } = useMlKibana(); + + const isFirst = useRef(true); + + useEffect(() => { + async function getQueryEntityDropdownOptions() { + let dataViewToUse: DataView | undefined; + const dataViewId = customUrl?.kibanaSettings?.discoverIndexPatternId; + + try { + dataViewToUse = await dataViews.get(dataViewId ?? ''); + } catch (e) { + dataViewToUse = undefined; + } + const dropDownOptions = await getDropDownOptions(isFirst.current, job, dataViewToUse); + setQueryEntityFieldNames(dropDownOptions); + + if (isFirst.current) { + isFirst.current = false; + } + } + + if (job !== undefined) { + getQueryEntityDropdownOptions(); + } + }, [dataViews, job, customUrl?.kibanaSettings?.discoverIndexPatternId]); + if (customUrl === undefined) { return null; } @@ -112,6 +150,7 @@ export const CustomUrlEditor: FC = ({ kibanaSettings: { ...kibanaSettings, discoverIndexPatternId: e.target.value, + queryFieldNames: [], }, }); }; @@ -307,58 +346,57 @@ export const CustomUrlEditor: FC = ({ )} - {(type === URL_TYPE.KIBANA_DASHBOARD || type === URL_TYPE.KIBANA_DISCOVER) && - showTimeRangeSelector && ( - <> - - - + {(type === URL_TYPE.KIBANA_DASHBOARD || type === URL_TYPE.KIBANA_DISCOVER) && isAnomalyJob && ( + <> + + + + + } + className="url-time-range" + display="rowCompressed" + > + + + + {timeRange.type === TIME_RANGE_TYPE.INTERVAL && ( + } className="url-time-range" + error={invalidIntervalError} + isInvalid={isInvalidTimeRange} display="rowCompressed" > - - {timeRange.type === TIME_RANGE_TYPE.INTERVAL && ( - - - } - className="url-time-range" - error={invalidIntervalError} - isInvalid={isInvalidTimeRange} - display="rowCompressed" - > - - - - )} - - - )} + )} + + + )} {type === URL_TYPE.OTHER && ( 0) { indicesName = job.dest.index; + backupIndicesName = job.source.index[0]; query = job.source?.query ?? {}; jobId = job.id; } const defaultDataViewId = dataViews.find((dv) => dv.title === indicesName)?.id; - kibanaSettings.discoverIndexPatternId = defaultDataViewId; + if (defaultDataViewId === undefined && backupIndicesName !== undefined) { + backupDataViewId = dataViews.find((dv) => dv.title === backupIndicesName)?.id; + } + kibanaSettings.discoverIndexPatternId = defaultDataViewId ?? backupDataViewId ?? ''; kibanaSettings.filters = defaultDataViewId === null ? [] : getFiltersForDSLQuery(query, defaultDataViewId, jobId); @@ -134,17 +141,23 @@ export function getNewCustomUrlDefaults( // Returns the list of supported field names that can be used // to add to the query used when linking to a Kibana dashboard or Discover. export function getSupportedFieldNames( - job: DataFrameAnalyticsConfig, + job: DataFrameAnalyticsConfig | Job, dataView: DataView ): string[] { - const resultsField = job.dest.results_field; const sortedFields = dataView.fields.getAll().sort((a, b) => a.name.localeCompare(b.name)) ?? []; - const categoryFields = sortedFields.filter( - (f) => + let filterFunction: (field: DataViewField) => boolean = (field: DataViewField) => + categoryFieldTypes.some((type) => { + return field.esTypes?.includes(type); + }); + + if (isDataFrameAnalyticsConfigs(job)) { + const resultsField = job.dest.results_field; + filterFunction = (f) => categoryFieldTypes.some((type) => { return f.esTypes?.includes(type); - }) && !f.name.startsWith(resultsField ?? DEFAULT_RESULTS_FIELD) - ); + }) && !f.name.startsWith(resultsField ?? DEFAULT_RESULTS_FIELD); + } + const categoryFields = sortedFields.filter(filterFunction); return categoryFields.map((field) => field.name); } diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx b/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx index 7ebab7b3a1359..9d3db04fa40de 100644 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx @@ -22,7 +22,6 @@ import { EuiModalFooter, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { DataView } from '@kbn/data-views-plugin/public'; import { i18n } from '@kbn/i18n'; import { withKibana } from '@kbn/kibana-react-plugin/public'; @@ -31,8 +30,6 @@ import { MlKibanaReactContextValue } from '../../contexts/kibana'; import { CustomUrlEditor, CustomUrlList } from './custom_url_editor'; import { getNewCustomUrlDefaults, - getQueryEntityFieldNames, - getSupportedFieldNames, isValidCustomUrlSettings, buildCustomUrlFromSettings, getTestUrl, @@ -43,22 +40,8 @@ import { loadDataViewListItems, } from '../../jobs/jobs_list/components/edit_job_flyout/edit_utils'; import { openCustomUrlWindow } from '../../util/custom_url_utils'; -import { Job, isAnomalyDetectionJob } from '../../../../common/types/anomaly_detection_jobs'; import { UrlConfig } from '../../../../common/types/custom_urls'; import type { CustomUrlsWrapperProps } from './custom_urls_wrapper'; -import { - isDataFrameAnalyticsConfigs, - type DataFrameAnalyticsConfig, -} from '../../../../common/types/data_frame_analytics'; - -function getDropDownOptions(job: Job | DataFrameAnalyticsConfig, dataView?: DataView) { - if (isAnomalyDetectionJob(job)) { - return getQueryEntityFieldNames(job); - } else if (isDataFrameAnalyticsConfigs(job) && dataView !== undefined) { - return getSupportedFieldNames(job, dataView); - } - return []; -} const MAX_NUMBER_DASHBOARDS = 1000; @@ -66,14 +49,12 @@ interface CustomUrlsState { customUrls: UrlConfig[]; dashboards: Array<{ id: string; title: string }>; dataViewListItems: DataViewListItem[]; - queryEntityFieldNames: string[]; editorOpen: boolean; editorSettings?: CustomUrlSettings; supportedFilterFields: string[]; } interface CustomUrlsProps extends CustomUrlsWrapperProps { kibana: MlKibanaReactContextValue; - dataView?: DataView; currentTimeFilter?: EsQueryTimeRange; } @@ -85,7 +66,6 @@ class CustomUrlsUI extends Component { customUrls: [], dashboards: [], dataViewListItems: [], - queryEntityFieldNames: [], editorOpen: false, supportedFilterFields: [], }; @@ -95,8 +75,6 @@ class CustomUrlsUI extends Component { return { job: props.job, customUrls: props.jobCustomUrls, - // For DFA uses the destination index Data View to get the query entities and falls back to source index Data View. - queryEntityFieldNames: getDropDownOptions(props.job, props.dataView), }; } @@ -223,25 +201,17 @@ class CustomUrlsUI extends Component { }; renderEditor() { - const { - customUrls, - editorOpen, - editorSettings, - dashboards, - dataViewListItems, - queryEntityFieldNames, - } = this.state; + const { customUrls, editorOpen, editorSettings, dashboards, dataViewListItems } = this.state; const editMode = this.props.editMode ?? 'inline'; const editor = ( ); diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls_wrapper.tsx b/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls_wrapper.tsx index a4a125d7cb52f..175b16dca74ea 100644 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls_wrapper.tsx @@ -5,16 +5,11 @@ * 2.0. */ -import React, { useEffect, useState, FC } from 'react'; -import { DataView } from '@kbn/data-views-plugin/public'; +import React, { FC } from 'react'; import { useMlKibana } from '../../contexts/kibana'; import { Job } from '../../../../common/types/anomaly_detection_jobs'; import { UrlConfig } from '../../../../common/types/custom_urls'; -import { getDataViewIdFromName } from '../../util/index_utils'; -import { - isDataFrameAnalyticsConfigs, - type DataFrameAnalyticsConfig, -} from '../../../../common/types/data_frame_analytics'; +import { type DataFrameAnalyticsConfig } from '../../../../common/types/data_frame_analytics'; import { CustomUrls } from './custom_urls'; export interface CustomUrlsWrapperProps { @@ -25,12 +20,9 @@ export interface CustomUrlsWrapperProps { } export const CustomUrlsWrapper: FC = (props) => { - const [dataView, setDataView] = useState(); - const { services: { data: { - dataViews, query: { timefilter: { timefilter }, }, @@ -38,40 +30,5 @@ export const CustomUrlsWrapper: FC = (props) => { }, } = useMlKibana(); - useEffect(() => { - let active = true; - - async function loadDataView() { - if (isDataFrameAnalyticsConfigs(props.job)) { - const destIndex = props.job.dest.index; - const sourceIndex = props.job.source.index[0]; - let dataViewIdSource: string | null; - let dataViewIdDest: string | null; - let dv: DataView | undefined; - - try { - dataViewIdSource = await getDataViewIdFromName(sourceIndex); - dataViewIdDest = await getDataViewIdFromName(destIndex); - dv = await dataViews.get(dataViewIdDest ?? dataViewIdSource ?? ''); - - if (dv === undefined) { - dv = await dataViews.get(dataViewIdSource ?? ''); - } - if (!active) return; - setDataView(dv); - } catch (e) { - dv = undefined; - } - - return dv; - } - } - - loadDataView(); - return () => { - active = false; - }; - }, [dataViews, props.job]); - - return ; + return ; };