From 4394293e217b5c814e64c56c64048debd13f4b6d Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Mon, 7 Feb 2022 19:45:56 -0500 Subject: [PATCH 1/2] [Dashboard] Remove URL Generator (#121832) * Remove deprecated and unused dashboard URL generator code Co-authored-by: Steph Milovic --- .../lib/build_dashboard_container.ts | 2 +- .../get_dashboard_list_item_link.test.ts | 4 +- .../listing/get_dashboard_list_item_link.ts | 7 +- .../dashboard/public/dashboard_constants.ts | 1 + src/plugins/dashboard/public/index.ts | 10 +- src/plugins/dashboard/public/plugin.tsx | 45 +-- .../dashboard/public/services/share.ts | 6 +- .../dashboard/public/url_generator.test.ts | 356 ------------------ src/plugins/dashboard/public/url_generator.ts | 170 --------- .../use_risky_hosts_dashboard_button_href.ts | 12 +- .../use_risky_hosts_dashboard_links.tsx | 54 +-- 11 files changed, 50 insertions(+), 617 deletions(-) delete mode 100644 src/plugins/dashboard/public/url_generator.test.ts delete mode 100644 src/plugins/dashboard/public/url_generator.ts diff --git a/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts b/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts index 1dd39cc3e5ba9..5752a6445d2a9 100644 --- a/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts +++ b/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts @@ -31,7 +31,7 @@ import { } from '../../services/embeddable'; type BuildDashboardContainerProps = DashboardBuildContext & { - data: DashboardAppServices['data']; // the whole data service is required here because it is required by getUrlGeneratorState + data: DashboardAppServices['data']; // the whole data service is required here because it is required by getLocatorParams savedDashboard: DashboardSavedObject; initialDashboardState: DashboardState; incomingEmbeddable?: EmbeddablePackageState; diff --git a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts index a6f80c157bee8..ce9535e549446 100644 --- a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts +++ b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts @@ -9,9 +9,9 @@ import { getDashboardListItemLink } from './get_dashboard_list_item_link'; import { ApplicationStart } from 'kibana/public'; import { createHashHistory } from 'history'; -import { createKbnUrlStateStorage } from '../../../../kibana_utils/public'; -import { GLOBAL_STATE_STORAGE_KEY } from '../../url_generator'; import { FilterStateStore } from '@kbn/es-query'; +import { createKbnUrlStateStorage } from '../../../../kibana_utils/public'; +import { GLOBAL_STATE_STORAGE_KEY } from '../../dashboard_constants'; const DASHBOARD_ID = '13823000-99b9-11ea-9eb6-d9e8adceb647'; diff --git a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts index 2f19924d45982..8af3f2a10666f 100644 --- a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts +++ b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts @@ -9,8 +9,11 @@ import { ApplicationStart } from 'kibana/public'; import { QueryState } from '../../../../data/public'; import { setStateToKbnUrl } from '../../../../kibana_utils/public'; -import { createDashboardEditUrl, DashboardConstants } from '../../dashboard_constants'; -import { GLOBAL_STATE_STORAGE_KEY } from '../../url_generator'; +import { + DashboardConstants, + createDashboardEditUrl, + GLOBAL_STATE_STORAGE_KEY, +} from '../../dashboard_constants'; import { IKbnUrlStateStorage } from '../../services/kibana_utils'; export const getDashboardListItemLink = ( diff --git a/src/plugins/dashboard/public/dashboard_constants.ts b/src/plugins/dashboard/public/dashboard_constants.ts index 9063b279c25f2..88fbc3b30392f 100644 --- a/src/plugins/dashboard/public/dashboard_constants.ts +++ b/src/plugins/dashboard/public/dashboard_constants.ts @@ -9,6 +9,7 @@ import type { ControlStyle } from '../../controls/public'; export const DASHBOARD_STATE_STORAGE_KEY = '_a'; +export const GLOBAL_STATE_STORAGE_KEY = '_g'; export const DashboardConstants = { LANDING_PAGE_PATH: '/list', diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index f25a92275d723..bff2d4d79108c 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -16,15 +16,7 @@ export { } from './application'; export { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; -export type { - DashboardSetup, - DashboardStart, - DashboardUrlGenerator, - DashboardFeatureFlagConfig, -} from './plugin'; - -export type { DashboardUrlGeneratorState } from './url_generator'; -export { DASHBOARD_APP_URL_GENERATOR, createDashboardUrlGenerator } from './url_generator'; +export type { DashboardSetup, DashboardStart, DashboardFeatureFlagConfig } from './plugin'; export type { DashboardAppLocator, DashboardAppLocatorParams } from './locator'; export type { DashboardSavedObject } from './saved_dashboards'; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 6554520fca101..2f63062ccf60c 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -34,7 +34,7 @@ import { PresentationUtilPluginStart } from './services/presentation_util'; import { FeatureCatalogueCategory, HomePublicPluginSetup } from './services/home'; import { NavigationPublicPluginStart as NavigationStart } from './services/navigation'; import { DataPublicPluginSetup, DataPublicPluginStart } from './services/data'; -import { SharePluginSetup, SharePluginStart, UrlGeneratorContract } from './services/share'; +import { SharePluginSetup, SharePluginStart } from './services/share'; import type { SavedObjectTaggingOssPluginStart } from './services/saved_objects_tagging_oss'; import type { ScreenshotModePluginSetup, @@ -70,29 +70,15 @@ import { CopyToDashboardAction, DashboardCapabilities, } from './application'; -import { - createDashboardUrlGenerator, - DASHBOARD_APP_URL_GENERATOR, - DashboardUrlGeneratorState, -} from './url_generator'; import { DashboardAppLocatorDefinition, DashboardAppLocator } from './locator'; import { createSavedDashboardLoader } from './saved_dashboards'; import { DashboardConstants } from './dashboard_constants'; import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder'; -import { UrlGeneratorState } from '../../share/public'; import { ExportCSVAction } from './application/actions/export_csv_action'; import { dashboardFeatureCatalog } from './dashboard_strings'; import { replaceUrlHashQuery } from '../../kibana_utils/public'; import { SpacesPluginStart } from './services/spaces'; -declare module '../../share/public' { - export interface UrlGeneratorStateMapping { - [DASHBOARD_APP_URL_GENERATOR]: UrlGeneratorState; - } -} - -export type DashboardUrlGenerator = UrlGeneratorContract; - export interface DashboardFeatureFlagConfig { allowByValueEmbeddables: boolean; } @@ -134,15 +120,6 @@ export interface DashboardStart { getDashboardContainerByValueRenderer: () => ReturnType< typeof createDashboardContainerByValueRenderer >; - /** - * @deprecated Use dashboard locator instead. Dashboard locator is available - * under `.locator` key. This dashboard URL generator will be removed soon. - * - * ```ts - * plugins.dashboard.locator.getLocation({ ... }); - * ``` - */ - dashboardUrlGenerator?: DashboardUrlGenerator; locator?: DashboardAppLocator; dashboardFeatureFlagConfig: DashboardFeatureFlagConfig; } @@ -157,11 +134,6 @@ export class DashboardPlugin private stopUrlTracking: (() => void) | undefined = undefined; private currentHistory: ScopedHistory | undefined = undefined; private dashboardFeatureFlagConfig?: DashboardFeatureFlagConfig; - - /** - * @deprecated Use locator instead. - */ - private dashboardUrlGenerator?: DashboardUrlGenerator; private locator?: DashboardAppLocator; public setup( @@ -178,20 +150,6 @@ export class DashboardPlugin ): DashboardSetup { this.dashboardFeatureFlagConfig = this.initializerContext.config.get(); - const startServices = core.getStartServices(); - - if (share) { - this.dashboardUrlGenerator = share.urlGenerators.registerUrlGenerator( - createDashboardUrlGenerator(async () => { - const [coreStart, , selfStart] = await startServices; - return { - appBasePath: coreStart.application.getUrlForApp('dashboards'), - useHashedUrl: coreStart.uiSettings.get('state:storeInSessionStorage'), - savedDashboardLoader: selfStart.getSavedDashboardLoader(), - }; - }) - ); - } const getPlaceholderEmbeddableStartServices = async () => { const [coreStart] = await core.getStartServices(); @@ -458,7 +416,6 @@ export class DashboardPlugin factory: dashboardContainerFactory as DashboardContainerFactory, }); }, - dashboardUrlGenerator: this.dashboardUrlGenerator, locator: this.locator, dashboardFeatureFlagConfig: this.dashboardFeatureFlagConfig!, }; diff --git a/src/plugins/dashboard/public/services/share.ts b/src/plugins/dashboard/public/services/share.ts index 7ed9b86571596..77a9f44a3cf00 100644 --- a/src/plugins/dashboard/public/services/share.ts +++ b/src/plugins/dashboard/public/services/share.ts @@ -6,9 +6,5 @@ * Side Public License, v 1. */ -export type { - SharePluginStart, - SharePluginSetup, - UrlGeneratorContract, -} from '../../../share/public'; +export type { SharePluginStart, SharePluginSetup } from '../../../share/public'; export { downloadMultipleAs } from '../../../share/public'; diff --git a/src/plugins/dashboard/public/url_generator.test.ts b/src/plugins/dashboard/public/url_generator.test.ts deleted file mode 100644 index f1035d7cc1389..0000000000000 --- a/src/plugins/dashboard/public/url_generator.test.ts +++ /dev/null @@ -1,356 +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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { createDashboardUrlGenerator } from './url_generator'; -import { hashedItemStore } from '../../kibana_utils/public'; -import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; -import { SavedObjectLoader } from '../../saved_objects/public'; -import { type Filter, FilterStateStore } from '@kbn/es-query'; - -const APP_BASE_PATH: string = 'xyz/app/dashboards'; - -const createMockDashboardLoader = ( - dashboardToFilters: { - [dashboardId: string]: () => Filter[]; - } = {} -) => { - return { - get: async (dashboardId: string) => { - return { - searchSource: { - getField: (field: string) => { - if (field === 'filter') - return dashboardToFilters[dashboardId] ? dashboardToFilters[dashboardId]() : []; - throw new Error( - `createMockDashboardLoader > searchSource > getField > ${field} is not mocked` - ); - }, - }, - }; - }, - } as SavedObjectLoader; -}; - -describe('dashboard url generator', () => { - beforeEach(() => { - // @ts-ignore - hashedItemStore.storage = mockStorage; - }); - - test('creates a link to a saved dashboard', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({}); - expect(url).toMatchInlineSnapshot(`"xyz/app/dashboards#/create?_a=()&_g=()"`); - }); - - test('creates a link with global time range set up', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/create?_a=()&_g=(time:(from:now-15m,mode:relative,to:now))"` - ); - }); - - test('creates a link with filters, time range, refresh interval and query to a saved object', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - refreshInterval: { pause: false, value: 300 }, - dashboardId: '123', - filters: [ - { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'hi' }, - }, - { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'hi' }, - $state: { - store: FilterStateStore.GLOBAL_STATE, - }, - }, - ], - query: { query: 'bye', language: 'kuery' }, - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/view/123?_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),query:(language:kuery,query:bye))&_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))"` - ); - }); - - test('searchSessionId', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - refreshInterval: { pause: false, value: 300 }, - dashboardId: '123', - filters: [], - query: { query: 'bye', language: 'kuery' }, - searchSessionId: '__sessionSearchId__', - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/view/123?_a=(filters:!(),query:(language:kuery,query:bye))&_g=(filters:!(),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))&searchSessionId=__sessionSearchId__"` - ); - }); - - test('savedQuery', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - savedQuery: '__savedQueryId__', - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/create?_a=(savedQuery:__savedQueryId__)&_g=()"` - ); - expect(url).toContain('__savedQueryId__'); - }); - - test('panels', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - panels: [{ fakePanelContent: 'fakePanelContent' } as any], - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/create?_a=(panels:!((fakePanelContent:fakePanelContent)))&_g=()"` - ); - }); - - test('if no useHash setting is given, uses the one was start services', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: true, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - }); - expect(url.indexOf('relative')).toBe(-1); - }); - - test('can override a false useHash ui setting', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - useHash: true, - }); - expect(url.indexOf('relative')).toBe(-1); - }); - - test('can override a true useHash ui setting', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: true, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - useHash: false, - }); - expect(url.indexOf('relative')).toBeGreaterThan(1); - }); - - describe('preserving saved filters', () => { - const savedFilter1 = { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'savedfilter1' }, - }; - - const savedFilter2 = { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'savedfilter2' }, - }; - - const appliedFilter = { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'appliedfilter' }, - }; - - test('attaches filters from destination dashboard', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => [savedFilter1], - ['dashboard2']: () => [savedFilter2], - }), - }) - ); - - const urlToDashboard1 = await generator.createUrl!({ - dashboardId: 'dashboard1', - filters: [appliedFilter], - }); - - expect(urlToDashboard1).toEqual(expect.stringContaining('query:savedfilter1')); - expect(urlToDashboard1).toEqual(expect.stringContaining('query:appliedfilter')); - - const urlToDashboard2 = await generator.createUrl!({ - dashboardId: 'dashboard2', - filters: [appliedFilter], - }); - - expect(urlToDashboard2).toEqual(expect.stringContaining('query:savedfilter2')); - expect(urlToDashboard2).toEqual(expect.stringContaining('query:appliedfilter')); - }); - - test("doesn't fail if can't retrieve filters from destination dashboard", async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => { - throw new Error('Not found'); - }, - }), - }) - ); - - const url = await generator.createUrl!({ - dashboardId: 'dashboard1', - filters: [appliedFilter], - }); - - expect(url).not.toEqual(expect.stringContaining('query:savedfilter1')); - expect(url).toEqual(expect.stringContaining('query:appliedfilter')); - }); - - test('can enforce empty filters', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => [savedFilter1], - }), - }) - ); - - const url = await generator.createUrl!({ - dashboardId: 'dashboard1', - filters: [], - preserveSavedFilters: false, - }); - - expect(url).not.toEqual(expect.stringContaining('query:savedfilter1')); - expect(url).not.toEqual(expect.stringContaining('query:appliedfilter')); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/view/dashboard1?_a=(filters:!())&_g=(filters:!())"` - ); - }); - - test('no filters in result url if no filters applied', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => [savedFilter1], - }), - }) - ); - - const url = await generator.createUrl!({ - dashboardId: 'dashboard1', - }); - expect(url).not.toEqual(expect.stringContaining('filters')); - expect(url).toMatchInlineSnapshot(`"xyz/app/dashboards#/view/dashboard1?_a=()&_g=()"`); - }); - - test('can turn off preserving filters', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => [savedFilter1], - }), - }) - ); - const urlWithPreservedFiltersTurnedOff = await generator.createUrl!({ - dashboardId: 'dashboard1', - filters: [appliedFilter], - preserveSavedFilters: false, - }); - - expect(urlWithPreservedFiltersTurnedOff).not.toEqual( - expect.stringContaining('query:savedfilter1') - ); - expect(urlWithPreservedFiltersTurnedOff).toEqual( - expect.stringContaining('query:appliedfilter') - ); - }); - }); -}); diff --git a/src/plugins/dashboard/public/url_generator.ts b/src/plugins/dashboard/public/url_generator.ts deleted file mode 100644 index 5c0cd32ee5a16..0000000000000 --- a/src/plugins/dashboard/public/url_generator.ts +++ /dev/null @@ -1,170 +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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - TimeRange, - Filter, - Query, - esFilters, - QueryState, - RefreshInterval, -} from '../../data/public'; -import { setStateToKbnUrl } from '../../kibana_utils/public'; -import { UrlGeneratorsDefinition } from '../../share/public'; -import { SavedObjectLoader } from '../../saved_objects/public'; -import { ViewMode } from '../../embeddable/public'; -import { DashboardConstants } from './dashboard_constants'; -import { SavedDashboardPanel } from '../common/types'; - -export const STATE_STORAGE_KEY = '_a'; -export const GLOBAL_STATE_STORAGE_KEY = '_g'; - -export const DASHBOARD_APP_URL_GENERATOR = 'DASHBOARD_APP_URL_GENERATOR'; - -/** - * @deprecated Use dashboard locator instead. - */ -export interface DashboardUrlGeneratorState { - /** - * If given, the dashboard saved object with this id will be loaded. If not given, - * a new, unsaved dashboard will be loaded up. - */ - dashboardId?: string; - /** - * Optionally set the time range in the time picker. - */ - timeRange?: TimeRange; - - /** - * Optionally set the refresh interval. - */ - refreshInterval?: RefreshInterval; - - /** - * Optionally apply filers. NOTE: if given and used in conjunction with `dashboardId`, and the - * saved dashboard has filters saved with it, this will _replace_ those filters. - */ - filters?: Filter[]; - /** - * Optionally set a query. NOTE: if given and used in conjunction with `dashboardId`, and the - * saved dashboard has a query saved with it, this will _replace_ that query. - */ - query?: Query; - /** - * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines - * whether to hash the data in the url to avoid url length issues. - */ - useHash?: boolean; - - /** - * When `true` filters from saved filters from destination dashboard as merged with applied filters - * When `false` applied filters take precedence and override saved filters - * - * true is default - */ - preserveSavedFilters?: boolean; - - /** - * View mode of the dashboard. - */ - viewMode?: ViewMode; - - /** - * Search search session ID to restore. - * (Background search) - */ - searchSessionId?: string; - - /** - * List of dashboard panels - */ - panels?: SavedDashboardPanel[]; - - /** - * Saved query ID - */ - savedQuery?: string; -} - -/** - * @deprecated Use dashboard locator instead. - */ -export const createDashboardUrlGenerator = ( - getStartServices: () => Promise<{ - appBasePath: string; - useHashedUrl: boolean; - savedDashboardLoader: SavedObjectLoader; - }> -): UrlGeneratorsDefinition => ({ - id: DASHBOARD_APP_URL_GENERATOR, - createUrl: async (state) => { - const startServices = await getStartServices(); - const useHash = state.useHash ?? startServices.useHashedUrl; - const appBasePath = startServices.appBasePath; - const hash = state.dashboardId ? `view/${state.dashboardId}` : `create`; - - const getSavedFiltersFromDestinationDashboardIfNeeded = async (): Promise => { - if (state.preserveSavedFilters === false) return []; - if (!state.dashboardId) return []; - try { - const dashboard = await startServices.savedDashboardLoader.get(state.dashboardId); - return dashboard?.searchSource?.getField('filter') ?? []; - } catch (e) { - // in case dashboard is missing, built the url without those filters - // dashboard app will handle redirect to landing page with toast message - return []; - } - }; - - const cleanEmptyKeys = (stateObj: Record) => { - Object.keys(stateObj).forEach((key) => { - if (stateObj[key] === undefined) { - delete stateObj[key]; - } - }); - return stateObj; - }; - - // leave filters `undefined` if no filters was applied - // in this case dashboard will restore saved filters on its own - const filters = state.filters && [ - ...(await getSavedFiltersFromDestinationDashboardIfNeeded()), - ...state.filters, - ]; - - let url = setStateToKbnUrl( - STATE_STORAGE_KEY, - cleanEmptyKeys({ - query: state.query, - filters: filters?.filter((f) => !esFilters.isFilterPinned(f)), - viewMode: state.viewMode, - panels: state.panels, - savedQuery: state.savedQuery, - }), - { useHash }, - `${appBasePath}#/${hash}` - ); - - url = setStateToKbnUrl( - GLOBAL_STATE_STORAGE_KEY, - cleanEmptyKeys({ - time: state.timeRange, - filters: filters?.filter((f) => esFilters.isFilterPinned(f)), - refreshInterval: state.refreshInterval, - }), - { useHash }, - url - ); - - if (state.searchSessionId) { - url = `${url}&${DashboardConstants.SEARCH_SESSION_ID}=${state.searchSessionId}`; - } - - return url; - }, -}); diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts index 555ae7544180b..5bc2087dc63ab 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts @@ -16,13 +16,15 @@ export const DASHBOARD_REQUEST_BODY = { }; export const useRiskyHostsDashboardButtonHref = (to: string, from: string) => { - const createDashboardUrl = useKibana().services.dashboard?.dashboardUrlGenerator?.createUrl; - const savedObjectsClient = useKibana().services.savedObjects.client; + const { + dashboard, + savedObjects: { client: savedObjectsClient }, + } = useKibana().services; const [buttonHref, setButtonHref] = useState(); useEffect(() => { - if (createDashboardUrl && savedObjectsClient) { + if (dashboard?.locator && savedObjectsClient) { savedObjectsClient.find(DASHBOARD_REQUEST_BODY).then( async (DashboardsSO?: { savedObjects?: Array<{ @@ -31,7 +33,7 @@ export const useRiskyHostsDashboardButtonHref = (to: string, from: string) => { }>; }) => { if (DashboardsSO?.savedObjects?.length) { - const dashboardUrl = await createDashboardUrl({ + const dashboardUrl = await dashboard?.locator?.getUrl({ dashboardId: DashboardsSO.savedObjects[0].id, timeRange: { to, @@ -43,7 +45,7 @@ export const useRiskyHostsDashboardButtonHref = (to: string, from: string) => { } ); } - }, [createDashboardUrl, from, savedObjectsClient, to]); + }, [dashboard, from, savedObjectsClient, to]); return { buttonHref, diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx index 002dc18227f6d..5b8bf180da1f8 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx @@ -14,40 +14,48 @@ export const useRiskyHostsDashboardLinks = ( from: string, listItems: LinkPanelListItem[] ) => { - const createDashboardUrl = useKibana().services.dashboard?.locator?.getLocation; + const { dashboard } = useKibana().services; + const dashboardId = useRiskyHostsDashboardId(); const [listItemsWithLinks, setListItemsWithLinks] = useState([]); useEffect(() => { let cancelled = false; const createLinks = async () => { - if (createDashboardUrl && dashboardId) { + if (dashboard?.locator && dashboardId) { const dashboardUrls = await Promise.all( - listItems.map((listItem) => - createDashboardUrl({ - dashboardId, - timeRange: { - to, - from, - }, - filters: [ - { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { match_phrase: { 'host.name': listItem.title } }, - }, - ], - }) + listItems.reduce( + (acc: Array>, listItem) => + dashboard && dashboard.locator + ? [ + ...acc, + dashboard.locator.getUrl({ + dashboardId, + timeRange: { + to, + from, + }, + filters: [ + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { match_phrase: { 'host.name': listItem.title } }, + }, + ], + }), + ] + : acc, + [] ) ); - if (!cancelled) { + if (!cancelled && dashboardUrls.length) { setListItemsWithLinks( listItems.map((item, i) => ({ ...item, - path: dashboardUrls[i] as unknown as string, + path: dashboardUrls[i], })) ); } @@ -59,7 +67,7 @@ export const useRiskyHostsDashboardLinks = ( return () => { cancelled = true; }; - }, [createDashboardUrl, dashboardId, from, listItems, to]); + }, [dashboard, dashboardId, from, listItems, to]); return { listItemsWithLinks }; }; From 97230e94310ceaa0f364f43ed577efadd34c9766 Mon Sep 17 00:00:00 2001 From: Byron Hulcher Date: Mon, 7 Feb 2022 19:51:49 -0500 Subject: [PATCH 2/2] [App Search] New modal to crawl select domains (#124195) --- .../crawl_select_domains_modal.scss | 4 + .../crawl_select_domains_modal.test.tsx | 98 +++++++++++++++ .../crawl_select_domains_modal.tsx | 109 ++++++++++++++++ .../crawl_select_domains_modal_logic.test.ts | 100 +++++++++++++++ .../crawl_select_domains_modal_logic.ts | 67 ++++++++++ .../simplified_selectable.test.tsx | 118 ++++++++++++++++++ .../simplified_selectable.tsx | 90 +++++++++++++ .../crawler_status_indicator.test.tsx | 9 +- .../crawler_status_indicator.tsx | 20 +-- .../start_crawl_context_menu.test.tsx | 76 +++++++++++ .../start_crawl_context_menu.tsx | 79 ++++++++++++ .../components/crawler/crawler_logic.test.ts | 40 +++++- .../components/crawler/crawler_logic.ts | 14 ++- .../crawler/crawler_overview.test.tsx | 7 ++ .../components/crawler/crawler_overview.tsx | 2 + .../crawler/crawler_single_domain.test.tsx | 7 ++ .../crawler/crawler_single_domain.tsx | 2 + .../server/routes/app_search/crawler.test.ts | 13 ++ .../server/routes/app_search/crawler.ts | 7 ++ 19 files changed, 840 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.scss new file mode 100644 index 0000000000000..09abf97829be4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.scss @@ -0,0 +1,4 @@ +.crawlSelectDomainsModal { + width: 50rem; + max-width: 90%; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.test.tsx new file mode 100644 index 0000000000000..79898d9f15e9d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.test.tsx @@ -0,0 +1,98 @@ +/* + * 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 { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiModal, EuiModalFooter, EuiButton, EuiButtonEmpty } from '@elastic/eui'; + +import { rerender } from '../../../../../test_helpers'; + +import { CrawlSelectDomainsModal } from './crawl_select_domains_modal'; +import { SimplifiedSelectable } from './simplified_selectable'; + +const MOCK_VALUES = { + // CrawlerLogic + domains: [{ url: 'https://www.elastic.co' }, { url: 'https://www.swiftype.com' }], + // CrawlSelectDomainsModalLogic + selectedDomainUrls: ['https://www.elastic.co'], + isModalVisible: true, +}; + +const MOCK_ACTIONS = { + // CrawlSelectDomainsModalLogic + hideModal: jest.fn(), + onSelectDomainUrls: jest.fn(), + // CrawlerLogic + startCrawl: jest.fn(), +}; + +describe('CrawlSelectDomainsModal', () => { + let wrapper: ShallowWrapper; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(MOCK_VALUES); + setMockActions(MOCK_ACTIONS); + + wrapper = shallow(); + }); + + it('is empty when the modal is hidden', () => { + setMockValues({ + ...MOCK_VALUES, + isModalVisible: false, + }); + + rerender(wrapper); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('renders as a modal when visible', () => { + expect(wrapper.is(EuiModal)).toBe(true); + }); + + it('can be closed', () => { + expect(wrapper.prop('onClose')).toEqual(MOCK_ACTIONS.hideModal); + expect(wrapper.find(EuiModalFooter).find(EuiButtonEmpty).prop('onClick')).toEqual( + MOCK_ACTIONS.hideModal + ); + }); + + it('allows the user to select domains', () => { + expect(wrapper.find(SimplifiedSelectable).props()).toEqual({ + options: ['https://www.elastic.co', 'https://www.swiftype.com'], + selectedOptions: ['https://www.elastic.co'], + onChange: MOCK_ACTIONS.onSelectDomainUrls, + }); + }); + + describe('submit button', () => { + it('is disabled when no domains are selected', () => { + setMockValues({ + ...MOCK_VALUES, + selectedDomainUrls: [], + }); + + rerender(wrapper); + + expect(wrapper.find(EuiModalFooter).find(EuiButton).prop('disabled')).toEqual(true); + }); + + it('starts a crawl and hides the modal', () => { + wrapper.find(EuiModalFooter).find(EuiButton).simulate('click'); + + expect(MOCK_ACTIONS.startCrawl).toHaveBeenCalledWith({ + domain_allowlist: MOCK_VALUES.selectedDomainUrls, + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.tsx new file mode 100644 index 0000000000000..211266a779df9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.tsx @@ -0,0 +1,109 @@ +/* + * 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 React from 'react'; + +import { useValues, useActions } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiNotificationBadge, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { CANCEL_BUTTON_LABEL } from '../../../../../shared/constants'; + +import { CrawlerLogic } from '../../crawler_logic'; + +import { CrawlSelectDomainsModalLogic } from './crawl_select_domains_modal_logic'; +import { SimplifiedSelectable } from './simplified_selectable'; + +import './crawl_select_domains_modal.scss'; + +export const CrawlSelectDomainsModal: React.FC = () => { + const { domains } = useValues(CrawlerLogic); + const domainUrls = domains.map((domain) => domain.url); + + const crawlSelectDomainsModalLogic = CrawlSelectDomainsModalLogic({ domains }); + const { isDataLoading, isModalVisible, selectedDomainUrls } = useValues( + crawlSelectDomainsModalLogic + ); + const { hideModal, onSelectDomainUrls } = useActions(crawlSelectDomainsModalLogic); + + const { startCrawl } = useActions(CrawlerLogic); + + if (!isModalVisible) { + return null; + } + + return ( + + + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlSelectDomainsModal.modalHeaderTitle', + { + defaultMessage: 'Crawl select domains', + } + )} + + + 0 ? 'accent' : 'subdued'} + > + {selectedDomainUrls.length} + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlSelectDomainsModal.selectedDescriptor', + { + defaultMessage: 'selected', + } + )} + + + + + + + + {CANCEL_BUTTON_LABEL} + { + startCrawl({ domain_allowlist: selectedDomainUrls }); + }} + disabled={selectedDomainUrls.length === 0} + isLoading={isDataLoading} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlSelectDomainsModal.startCrawlButtonLabel', + { + defaultMessage: 'Apply and crawl now', + } + )} + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.test.ts new file mode 100644 index 0000000000000..ef6ef4d09fadb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.test.ts @@ -0,0 +1,100 @@ +/* + * 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 { LogicMounter } from '../../../../../__mocks__/kea_logic'; + +import { CrawlerLogic } from '../../crawler_logic'; + +import { CrawlSelectDomainsModalLogic } from './crawl_select_domains_modal_logic'; + +describe('CrawlSelectDomainsModalLogic', () => { + const { mount } = new LogicMounter(CrawlSelectDomainsModalLogic); + + beforeEach(() => { + jest.clearAllMocks(); + mount(); + }); + + it('has expected default values', () => { + expect(CrawlSelectDomainsModalLogic.values).toEqual({ + isDataLoading: false, + isModalVisible: false, + selectedDomainUrls: [], + }); + }); + + describe('actions', () => { + describe('hideModal', () => { + it('hides the modal', () => { + CrawlSelectDomainsModalLogic.actions.hideModal(); + + expect(CrawlSelectDomainsModalLogic.values.isModalVisible).toBe(false); + }); + }); + + describe('showModal', () => { + it('shows the modal', () => { + CrawlSelectDomainsModalLogic.actions.showModal(); + + expect(CrawlSelectDomainsModalLogic.values.isModalVisible).toBe(true); + }); + + it('resets the selected options', () => { + mount({ + selectedDomainUrls: ['https://www.elastic.co', 'https://www.swiftype.com'], + }); + + CrawlSelectDomainsModalLogic.actions.showModal(); + + expect(CrawlSelectDomainsModalLogic.values.selectedDomainUrls).toEqual([]); + }); + }); + + describe('onSelectDomainUrls', () => { + it('saves the urls', () => { + mount({ + selectedDomainUrls: [], + }); + + CrawlSelectDomainsModalLogic.actions.onSelectDomainUrls([ + 'https://www.elastic.co', + 'https://www.swiftype.com', + ]); + + expect(CrawlSelectDomainsModalLogic.values.selectedDomainUrls).toEqual([ + 'https://www.elastic.co', + 'https://www.swiftype.com', + ]); + }); + }); + + describe('[CrawlerLogic.actionTypes.startCrawl]', () => { + it('enables loading state', () => { + mount({ + isDataLoading: false, + }); + + CrawlerLogic.actions.startCrawl(); + + expect(CrawlSelectDomainsModalLogic.values.isDataLoading).toBe(true); + }); + }); + + describe('[CrawlerLogic.actionTypes.onStartCrawlRequestComplete]', () => { + it('disables loading state and hides the modal', () => { + mount({ + isDataLoading: true, + isModalVisible: true, + }); + + CrawlerLogic.actions.onStartCrawlRequestComplete(); + + expect(CrawlSelectDomainsModalLogic.values.isDataLoading).toBe(false); + expect(CrawlSelectDomainsModalLogic.values.isModalVisible).toBe(false); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.ts new file mode 100644 index 0000000000000..088950cbffd3f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.ts @@ -0,0 +1,67 @@ +/* + * 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 { kea, MakeLogicType } from 'kea'; + +import { CrawlerLogic } from '../../crawler_logic'; + +import { CrawlerDomain } from '../../types'; + +export interface CrawlSelectDomainsLogicProps { + domains: CrawlerDomain[]; +} + +export interface CrawlSelectDomainsLogicValues { + isDataLoading: boolean; + isModalVisible: boolean; + selectedDomainUrls: string[]; +} + +export interface CrawlSelectDomainsModalLogicActions { + hideModal(): void; + onSelectDomainUrls(domainUrls: string[]): { domainUrls: string[] }; + showModal(): void; +} + +export const CrawlSelectDomainsModalLogic = kea< + MakeLogicType< + CrawlSelectDomainsLogicValues, + CrawlSelectDomainsModalLogicActions, + CrawlSelectDomainsLogicProps + > +>({ + path: ['enterprise_search', 'app_search', 'crawler', 'crawl_select_domains_modal'], + actions: () => ({ + hideModal: true, + onSelectDomainUrls: (domainUrls) => ({ domainUrls }), + showModal: true, + }), + reducers: () => ({ + isDataLoading: [ + false, + { + [CrawlerLogic.actionTypes.startCrawl]: () => true, + [CrawlerLogic.actionTypes.onStartCrawlRequestComplete]: () => false, + }, + ], + isModalVisible: [ + false, + { + showModal: () => true, + hideModal: () => false, + [CrawlerLogic.actionTypes.onStartCrawlRequestComplete]: () => false, + }, + ], + selectedDomainUrls: [ + [], + { + showModal: () => [], + onSelectDomainUrls: (_, { domainUrls }) => domainUrls, + }, + ], + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.test.tsx new file mode 100644 index 0000000000000..a90259f8dac3c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.test.tsx @@ -0,0 +1,118 @@ +/* + * 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 React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiSelectable, EuiSelectableList, EuiSelectableSearch } from '@elastic/eui'; + +import { mountWithIntl } from '../../../../../test_helpers'; + +import { SimplifiedSelectable } from './simplified_selectable'; + +describe('SimplifiedSelectable', () => { + let wrapper: ShallowWrapper; + + const MOCK_ON_CHANGE = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + wrapper = shallow( + + ); + }); + + it('combines the options and selected options', () => { + expect(wrapper.find(EuiSelectable).prop('options')).toEqual([ + { + label: 'cat', + checked: 'on', + }, + { + label: 'dog', + }, + { + label: 'fish', + checked: 'on', + }, + ]); + }); + + it('passes newly selected options to the callback', () => { + wrapper.find(EuiSelectable).simulate('change', [ + { + label: 'cat', + checked: 'on', + }, + { + label: 'dog', + }, + { + label: 'fish', + checked: 'on', + }, + ]); + + expect(MOCK_ON_CHANGE).toHaveBeenCalledWith(['cat', 'fish']); + }); + + describe('select all button', () => { + it('it is disabled when all options are already selected', () => { + wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="SelectAllButton"]').prop('disabled')).toEqual(true); + }); + + it('allows the user to select all options', () => { + wrapper.find('[data-test-subj="SelectAllButton"]').simulate('click'); + expect(MOCK_ON_CHANGE).toHaveBeenLastCalledWith(['cat', 'dog', 'fish']); + }); + }); + + describe('deselect all button', () => { + it('it is disabled when all no options are selected', () => { + wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="DeselectAllButton"]').prop('disabled')).toEqual(true); + }); + + it('allows the user to select all options', () => { + wrapper.find('[data-test-subj="DeselectAllButton"]').simulate('click'); + expect(MOCK_ON_CHANGE).toHaveBeenLastCalledWith([]); + }); + }); + + it('renders a search bar and selectable list', () => { + const fullRender = mountWithIntl( + + ); + + expect(fullRender.find(EuiSelectableSearch)).toHaveLength(1); + expect(fullRender.find(EuiSelectableList)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.tsx new file mode 100644 index 0000000000000..07ede1c59971a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.tsx @@ -0,0 +1,90 @@ +/* + * 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 React from 'react'; + +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSelectable } from '@elastic/eui'; +import { EuiSelectableLIOption } from '@elastic/eui/src/components/selectable/selectable_option'; +import { i18n } from '@kbn/i18n'; + +export interface Props { + options: string[]; + selectedOptions: string[]; + onChange(selectedOptions: string[]): void; +} + +export interface OptionMap { + [key: string]: boolean; +} + +export const SimplifiedSelectable: React.FC = ({ options, selectedOptions, onChange }) => { + const selectedOptionsMap: OptionMap = selectedOptions.reduce( + (acc, selectedOption) => ({ + ...acc, + [selectedOption]: true, + }), + {} + ); + + const selectableOptions: Array> = options.map((option) => ({ + label: option, + checked: selectedOptionsMap[option] ? 'on' : undefined, + })); + + return ( + <> + + + onChange(options)} + disabled={selectedOptions.length === options.length} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.simplifiedSelectable.selectAllButtonLabel', + { + defaultMessage: 'Select all', + } + )} + + + + onChange([])} + disabled={selectedOptions.length === 0} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.simplifiedSelectable.deselectAllButtonLabel', + { + defaultMessage: 'Deselect all', + } + )} + + + + { + onChange( + newSelectableOptions.filter((option) => option.checked).map((option) => option.label) + ); + }} + > + {(list, search) => ( + <> + {search} + {list} + + )} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx index c46c360934d0b..cc8b1891838b3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx @@ -16,6 +16,7 @@ import { EuiButton } from '@elastic/eui'; import { CrawlerDomain, CrawlerStatus } from '../../types'; import { CrawlerStatusIndicator } from './crawler_status_indicator'; +import { StartCrawlContextMenu } from './start_crawl_context_menu'; import { StopCrawlPopoverContextMenu } from './stop_crawl_popover_context_menu'; const MOCK_VALUES = { @@ -72,9 +73,7 @@ describe('CrawlerStatusIndicator', () => { }); const wrapper = shallow(); - expect(wrapper.is(EuiButton)).toEqual(true); - expect(wrapper.render().text()).toContain('Start a crawl'); - expect(wrapper.prop('onClick')).toEqual(MOCK_ACTIONS.startCrawl); + expect(wrapper.is(StartCrawlContextMenu)).toEqual(true); }); }); @@ -87,9 +86,7 @@ describe('CrawlerStatusIndicator', () => { }); const wrapper = shallow(); - expect(wrapper.is(EuiButton)).toEqual(true); - expect(wrapper.render().text()).toContain('Retry crawl'); - expect(wrapper.prop('onClick')).toEqual(MOCK_ACTIONS.startCrawl); + expect(wrapper.is(StartCrawlContextMenu)).toEqual(true); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx index c02e45f02c407..d750cf100202f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx @@ -16,14 +16,15 @@ import { i18n } from '@kbn/i18n'; import { CrawlerLogic } from '../../crawler_logic'; import { CrawlerStatus } from '../../types'; +import { StartCrawlContextMenu } from './start_crawl_context_menu'; import { StopCrawlPopoverContextMenu } from './stop_crawl_popover_context_menu'; export const CrawlerStatusIndicator: React.FC = () => { const { domains, mostRecentCrawlRequestStatus } = useValues(CrawlerLogic); - const { startCrawl, stopCrawl } = useActions(CrawlerLogic); + const { stopCrawl } = useActions(CrawlerLogic); const disabledButton = ( - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.crawler.crawlerStatusIndicator.startACrawlButtonLabel', { @@ -40,26 +41,27 @@ export const CrawlerStatusIndicator: React.FC = () => { switch (mostRecentCrawlRequestStatus) { case CrawlerStatus.Success: return ( - - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.crawler.crawlerStatusIndicator.startACrawlButtonLabel', + + /> ); case CrawlerStatus.Failed: case CrawlerStatus.Canceled: return ( - - {i18n.translate( + + /> ); case CrawlerStatus.Pending: case CrawlerStatus.Suspended: diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.test.tsx new file mode 100644 index 0000000000000..6d9f1cd7be64b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.test.tsx @@ -0,0 +1,76 @@ +/* + * 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 { setMockActions } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { ReactWrapper, shallow } from 'enzyme'; + +import { + EuiButton, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiPopover, + EuiResizeObserver, +} from '@elastic/eui'; + +import { mountWithIntl } from '../../../../../test_helpers'; + +import { StartCrawlContextMenu } from './start_crawl_context_menu'; + +const MOCK_ACTIONS = { + startCrawl: jest.fn(), + showModal: jest.fn(), +}; + +describe('StartCrawlContextMenu', () => { + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(MOCK_ACTIONS); + }); + + it('is initially closed', () => { + const wrapper = shallow(); + + expect(wrapper.is(EuiPopover)).toBe(true); + expect(wrapper.prop('isOpen')).toEqual(false); + }); + + describe('user actions', () => { + let wrapper: ReactWrapper; + let menuItems: ReactWrapper; + + beforeEach(() => { + wrapper = mountWithIntl(); + + wrapper.find(EuiButton).simulate('click'); + + menuItems = wrapper + .find(EuiContextMenuPanel) + .find(EuiResizeObserver) + .find(EuiContextMenuItem); + }); + + it('can be opened', () => { + expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(true); + expect(menuItems.length).toEqual(2); + }); + + it('can start crawls', () => { + menuItems.at(0).simulate('click'); + + expect(MOCK_ACTIONS.startCrawl).toHaveBeenCalled(); + }); + + it('can open a modal to start a crawl with selected domains', () => { + menuItems.at(1).simulate('click'); + + expect(MOCK_ACTIONS.showModal).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.tsx new file mode 100644 index 0000000000000..1182a845bd4f7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.tsx @@ -0,0 +1,79 @@ +/* + * 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 React, { useState } from 'react'; + +import { useActions } from 'kea'; + +import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { CrawlerLogic } from '../../crawler_logic'; + +import { CrawlSelectDomainsModalLogic } from '../crawl_select_domains_modal/crawl_select_domains_modal_logic'; + +interface Props { + menuButtonLabel?: string; + fill?: boolean; +} + +export const StartCrawlContextMenu: React.FC = ({ menuButtonLabel, fill }) => { + const { startCrawl } = useActions(CrawlerLogic); + const { showModal: showCrawlSelectDomainsModal } = useActions(CrawlSelectDomainsModalLogic); + + const [isPopoverOpen, setPopover] = useState(false); + + const togglePopover = () => setPopover(!isPopoverOpen); + + const closePopover = () => setPopover(false); + + return ( + + {menuButtonLabel} + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" + > + { + closePopover(); + startCrawl(); + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.startCrawlContextMenu.crawlAllDomainsMenuLabel', + { + defaultMessage: 'Crawl all domains on this engine', + } + )} + , + { + closePopover(); + showCrawlSelectDomainsModal(); + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.startCrawlContextMenu.crawlSelectDomainsMenuLabel', + { + defaultMessage: 'Crawl select domains', + } + )} + , + ]} + /> + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts index e622798e688ab..59ec64c69d5a8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts @@ -226,7 +226,7 @@ describe('CrawlerLogic', () => { CrawlerStatus.Running, CrawlerStatus.Canceling, ].forEach((status) => { - it(`creates a new timeout for status ${status}`, async () => { + it(`creates a new timeout for most recent crawl request status ${status}`, async () => { jest.spyOn(CrawlerLogic.actions, 'createNewTimeoutForCrawlerData'); http.get.mockReturnValueOnce( Promise.resolve({ @@ -260,6 +260,27 @@ describe('CrawlerLogic', () => { expect(CrawlerLogic.actions.fetchCrawlerData).toHaveBeenCalled(); }); }); + + it('clears the timeout if no events are active', async () => { + jest.spyOn(CrawlerLogic.actions, 'clearTimeoutId'); + + http.get.mockReturnValueOnce( + Promise.resolve({ + ...MOCK_SERVER_CRAWLER_DATA, + events: [ + { + status: CrawlerStatus.Failed, + crawl_config: {}, + }, + ], + }) + ); + + CrawlerLogic.actions.fetchCrawlerData(); + await nextTick(); + + expect(CrawlerLogic.actions.clearTimeoutId).toHaveBeenCalled(); + }); }); it('calls flashApiErrors when there is an error on the request for crawler data', async () => { @@ -276,23 +297,36 @@ describe('CrawlerLogic', () => { describe('startCrawl', () => { describe('success path', () => { - it('creates a new crawl request and then fetches the latest crawler data', async () => { + it('creates a new crawl request, fetches latest crawler data, then marks the request complete', async () => { jest.spyOn(CrawlerLogic.actions, 'fetchCrawlerData'); + jest.spyOn(CrawlerLogic.actions, 'onStartCrawlRequestComplete'); http.post.mockReturnValueOnce(Promise.resolve()); CrawlerLogic.actions.startCrawl(); await nextTick(); expect(http.post).toHaveBeenCalledWith( - '/internal/app_search/engines/some-engine/crawler/crawl_requests' + '/internal/app_search/engines/some-engine/crawler/crawl_requests', + { body: JSON.stringify({ overrides: {} }) } ); expect(CrawlerLogic.actions.fetchCrawlerData).toHaveBeenCalled(); + expect(CrawlerLogic.actions.onStartCrawlRequestComplete).toHaveBeenCalled(); }); }); itShowsServerErrorAsFlashMessage(http.post, () => { CrawlerLogic.actions.startCrawl(); }); + + it('marks the request complete even after an error', async () => { + jest.spyOn(CrawlerLogic.actions, 'onStartCrawlRequestComplete'); + http.post.mockReturnValueOnce(Promise.reject()); + + CrawlerLogic.actions.startCrawl(); + await nextTick(); + + expect(CrawlerLogic.actions.onStartCrawlRequestComplete).toHaveBeenCalled(); + }); }); describe('stopCrawl', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts index 08a01af67ece6..d68dbc59f06d0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts @@ -48,7 +48,8 @@ interface CrawlerActions { fetchCrawlerData(): void; onCreateNewTimeout(timeoutId: NodeJS.Timeout): { timeoutId: NodeJS.Timeout }; onReceiveCrawlerData(data: CrawlerData): { data: CrawlerData }; - startCrawl(): void; + onStartCrawlRequestComplete(): void; + startCrawl(overrides?: object): { overrides?: object }; stopCrawl(): void; } @@ -60,7 +61,8 @@ export const CrawlerLogic = kea>({ fetchCrawlerData: true, onCreateNewTimeout: (timeoutId) => ({ timeoutId }), onReceiveCrawlerData: (data) => ({ data }), - startCrawl: () => null, + onStartCrawlRequestComplete: true, + startCrawl: (overrides) => ({ overrides }), stopCrawl: () => null, }, reducers: { @@ -135,15 +137,19 @@ export const CrawlerLogic = kea>({ actions.createNewTimeoutForCrawlerData(POLLING_DURATION_ON_FAILURE); } }, - startCrawl: async () => { + startCrawl: async ({ overrides = {} }) => { const { http } = HttpLogic.values; const { engineName } = EngineLogic.values; try { - await http.post(`/internal/app_search/engines/${engineName}/crawler/crawl_requests`); + await http.post(`/internal/app_search/engines/${engineName}/crawler/crawl_requests`, { + body: JSON.stringify({ overrides }), + }); actions.fetchCrawlerData(); } catch (e) { flashAPIErrors(e); + } finally { + actions.onStartCrawlRequestComplete(); } }, stopCrawl: async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx index 4d72b854bddfb..509346542ae13 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx @@ -23,6 +23,7 @@ import { AddDomainFormSubmitButton } from './components/add_domain/add_domain_fo import { AddDomainLogic } from './components/add_domain/add_domain_logic'; import { CrawlDetailsFlyout } from './components/crawl_details_flyout'; import { CrawlRequestsTable } from './components/crawl_requests_table'; +import { CrawlSelectDomainsModal } from './components/crawl_select_domains_modal/crawl_select_domains_modal'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DomainsTable } from './components/domains_table'; @@ -215,4 +216,10 @@ describe('CrawlerOverview', () => { expect(wrapper.find(AddDomainFormErrors)).toHaveLength(1); }); + + it('contains a modal to start a crawl with selected domains', () => { + const wrapper = shallow(); + + expect(wrapper.find(CrawlSelectDomainsModal)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx index c68e75790f073..f1f25dfb4dc55 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx @@ -24,6 +24,7 @@ import { AddDomainFormSubmitButton } from './components/add_domain/add_domain_fo import { AddDomainLogic } from './components/add_domain/add_domain_logic'; import { CrawlDetailsFlyout } from './components/crawl_details_flyout'; import { CrawlRequestsTable } from './components/crawl_requests_table'; +import { CrawlSelectDomainsModal } from './components/crawl_select_domains_modal/crawl_select_domains_modal'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DomainsTable } from './components/domains_table'; @@ -138,6 +139,7 @@ export const CrawlerOverview: React.FC = () => { )} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx index ed445b923ea2a..addf4093a167b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx @@ -15,6 +15,7 @@ import { shallow } from 'enzyme'; import { getPageHeaderActions } from '../../../test_helpers'; +import { CrawlSelectDomainsModal } from './components/crawl_select_domains_modal/crawl_select_domains_modal'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DeduplicationPanel } from './components/deduplication_panel'; @@ -92,4 +93,10 @@ describe('CrawlerSingleDomain', () => { expect(wrapper.find(DeleteDomainPanel)).toHaveLength(1); }); + + it('contains a modal to start a crawl with selected domains', () => { + const wrapper = shallow(); + + expect(wrapper.find(CrawlSelectDomainsModal)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx index a4b2a9709cd62..63b9c3f080ec2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx @@ -17,6 +17,7 @@ import { EngineLogic, getEngineBreadcrumbs } from '../engine'; import { AppSearchPageTemplate } from '../layout'; import { CrawlRulesTable } from './components/crawl_rules_table'; +import { CrawlSelectDomainsModal } from './components/crawl_select_domains_modal/crawl_select_domains_modal'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DeduplicationPanel } from './components/deduplication_panel'; @@ -78,6 +79,7 @@ export const CrawlerSingleDomain: React.FC = () => { + ); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts index c9212bca322d7..fe225f62d1dce 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts @@ -141,6 +141,19 @@ describe('crawler routes', () => { mockRouter.shouldValidate(request); }); + it('validates correctly with overrides', () => { + const request = { + params: { name: 'some-engine' }, + body: { overrides: { domain_allowlist: [] } }, + }; + mockRouter.shouldValidate(request); + }); + + it('validates correctly with empty overrides', () => { + const request = { params: { name: 'some-engine' }, body: { overrides: {} } }; + mockRouter.shouldValidate(request); + }); + it('fails validation without name', () => { const request = { params: {} }; mockRouter.shouldThrow(request); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts index f0fdc5c16098b..5adffe1ff3ee5 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts @@ -63,6 +63,13 @@ export function registerCrawlerRoutes({ params: schema.object({ name: schema.string(), }), + body: schema.object({ + overrides: schema.maybe( + schema.object({ + domain_allowlist: schema.maybe(schema.arrayOf(schema.string())), + }) + ), + }), }, }, enterpriseSearchRequestHandler.createRequest({