diff --git a/jest.config.integration.js b/jest.config.integration.js index 50767932a52d7..b6ecb4569b643 100644 --- a/jest.config.integration.js +++ b/jest.config.integration.js @@ -13,7 +13,6 @@ module.exports = { rootDir: '.', roots: ['/src', '/packages'], testMatch: ['**/integration_tests**/*.test.{js,mjs,ts,tsx}'], - testRunner: 'jasmine2', testPathIgnorePatterns: preset.testPathIgnorePatterns.filter( (pattern) => !pattern.includes('integration_tests') ), diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index 3ed164088bf5c..de9e4d4496f3b 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -15,13 +15,13 @@ import { import { createElement } from 'react'; import { BehaviorSubject, Subject } from 'rxjs'; import { bufferCount, take, takeUntil } from 'rxjs/operators'; -import { shallow, mount } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import { httpServiceMock } from '../http/http_service.mock'; import { overlayServiceMock } from '../overlays/overlay_service.mock'; import { MockLifecycle } from './test_types'; import { ApplicationService } from './application_service'; -import { App, PublicAppInfo, AppNavLinkStatus, AppStatus, AppUpdater } from './types'; +import { App, AppDeepLink, AppNavLinkStatus, AppStatus, AppUpdater, PublicAppInfo } from './types'; import { act } from 'react-dom/test-utils'; const createApp = (props: Partial): App => { @@ -365,6 +365,85 @@ describe('#setup()', () => { expect(MockHistory.push).toHaveBeenCalledWith('/app/app1', undefined); MockHistory.push.mockClear(); }); + + it('preserves the deep links if the update does not modify them', async () => { + const setup = service.setup(setupDeps); + + const pluginId = Symbol('plugin'); + const updater$ = new BehaviorSubject((app) => ({})); + + const deepLinks: AppDeepLink[] = [ + { + id: 'foo', + title: 'Foo', + searchable: true, + navLinkStatus: AppNavLinkStatus.visible, + path: '/foo', + }, + { + id: 'bar', + title: 'Bar', + searchable: false, + navLinkStatus: AppNavLinkStatus.hidden, + path: '/bar', + }, + ]; + + setup.register(pluginId, createApp({ id: 'app1', deepLinks, updater$ })); + + const { applications$ } = await service.start(startDeps); + + updater$.next((app) => ({ defaultPath: '/foo' })); + + let appInfos = await applications$.pipe(take(1)).toPromise(); + + expect(appInfos.get('app1')!.deepLinks).toEqual([ + { + deepLinks: [], + id: 'foo', + keywords: [], + navLinkStatus: 1, + path: '/foo', + searchable: true, + title: 'Foo', + }, + { + deepLinks: [], + id: 'bar', + keywords: [], + navLinkStatus: 3, + path: '/bar', + searchable: false, + title: 'Bar', + }, + ]); + + updater$.next((app) => ({ + deepLinks: [ + { + id: 'bar', + title: 'Bar', + searchable: false, + navLinkStatus: AppNavLinkStatus.hidden, + path: '/bar', + }, + ], + })); + + appInfos = await applications$.pipe(take(1)).toPromise(); + + expect(appInfos.get('app1')!.deepLinks).toEqual([ + { + deepLinks: [], + id: 'bar', + keywords: [], + navLinkStatus: 3, + path: '/bar', + searchable: false, + title: 'Bar', + }, + ]); + }); }); }); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 8c6090caabce1..2e804bf2f5413 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -54,6 +54,7 @@ function filterAvailable(m: Map, capabilities: Capabilities) { ) ); } + const findMounter = (mounters: Map, appRoute?: string) => [...mounters].find(([, mounter]) => mounter.appRoute === appRoute); @@ -414,13 +415,11 @@ const updateStatus = (app: App, statusUpdaters: AppUpdaterWrapper[]): App => { changes.navLinkStatus ?? AppNavLinkStatus.default, fields.navLinkStatus ?? AppNavLinkStatus.default ), - // deepLinks take the last defined update - deepLinks: fields.deepLinks - ? populateDeepLinkDefaults(fields.deepLinks) - : changes.deepLinks, + ...(fields.deepLinks ? { deepLinks: populateDeepLinkDefaults(fields.deepLinks) } : {}), }; } }); + return { ...app, ...changes, diff --git a/src/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts index 7571184363d2e..dfc47098724cc 100644 --- a/src/core/server/http/integration_tests/request.test.ts +++ b/src/core/server/http/integration_tests/request.test.ts @@ -163,24 +163,24 @@ describe('KibanaRequest', () => { describe('events', () => { describe('aborted$', () => { - it('emits once and completes when request aborted', async (done) => { + it('emits once and completes when request aborted', async () => { expect.assertions(1); const { server: innerServer, createRouter } = await server.setup(setupDeps); const router = createRouter('/'); const nextSpy = jest.fn(); - router.get({ path: '/', validate: false }, async (context, request, res) => { - request.events.aborted$.subscribe({ - next: nextSpy, - complete: () => { - expect(nextSpy).toHaveBeenCalledTimes(1); - done(); - }, - }); - // prevents the server to respond - await delay(30000); - return res.ok({ body: 'ok' }); + const done = new Promise((resolve) => { + router.get({ path: '/', validate: false }, async (context, request, res) => { + request.events.aborted$.subscribe({ + next: nextSpy, + complete: resolve, + }); + + // prevents the server to respond + await delay(30000); + return res.ok({ body: 'ok' }); + }); }); await server.start(); @@ -191,6 +191,8 @@ describe('KibanaRequest', () => { .end(); setTimeout(() => incomingRequest.abort(), 50); + await done; + expect(nextSpy).toHaveBeenCalledTimes(1); }); it('completes & does not emit when request handled', async () => { @@ -299,25 +301,24 @@ describe('KibanaRequest', () => { expect(completeSpy).toHaveBeenCalledTimes(1); }); - it('emits once and completes when response is aborted', async (done) => { + it('emits once and completes when response is aborted', async () => { expect.assertions(2); const { server: innerServer, createRouter } = await server.setup(setupDeps); const router = createRouter('/'); const nextSpy = jest.fn(); - router.get({ path: '/', validate: false }, async (context, req, res) => { - req.events.completed$.subscribe({ - next: nextSpy, - complete: () => { - expect(nextSpy).toHaveBeenCalledTimes(1); - done(); - }, - }); + const done = new Promise((resolve) => { + router.get({ path: '/', validate: false }, async (context, req, res) => { + req.events.completed$.subscribe({ + next: nextSpy, + complete: resolve, + }); - expect(nextSpy).not.toHaveBeenCalled(); - await delay(30000); - return res.ok({ body: 'ok' }); + expect(nextSpy).not.toHaveBeenCalled(); + await delay(30000); + return res.ok({ body: 'ok' }); + }); }); await server.start(); @@ -327,6 +328,8 @@ describe('KibanaRequest', () => { // end required to send request .end(); setTimeout(() => incomingRequest.abort(), 50); + await done; + expect(nextSpy).toHaveBeenCalledTimes(1); }); }); }); diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts index f4e0dd8fffcab..4c9e37d17f2e7 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts @@ -95,6 +95,12 @@ describe('migration v2', () => { }, ], }, + // reporting loads headless browser, that prevents nodejs process from exiting. + xpack: { + reporting: { + enabled: false, + }, + }, }, { oss, diff --git a/src/core/server/ui_settings/integration_tests/index.test.ts b/src/core/server/ui_settings/integration_tests/index.test.ts index 6c7cdfa43cf57..61e55284a20b8 100644 --- a/src/core/server/ui_settings/integration_tests/index.test.ts +++ b/src/core/server/ui_settings/integration_tests/index.test.ts @@ -17,7 +17,7 @@ const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo. const savedObjectIndex = `.kibana_${kibanaVersion}_001`; describe('uiSettings/routes', function () { - jest.setTimeout(10000); + jest.setTimeout(120_000); beforeAll(startServers); /* eslint-disable jest/valid-describe */ diff --git a/src/core/server/ui_settings/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts index b18d9926649aa..96ba08a0728ab 100644 --- a/src/core/server/ui_settings/integration_tests/lib/servers.ts +++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts @@ -75,8 +75,10 @@ export function getServices() { export async function stopServers() { services = null!; - if (servers) { + if (esServer) { await esServer.stop(); + } + if (kbn) { await kbn.stop(); } } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx index 3b07ab5f8cb66..f4ba46ca9e6d4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx @@ -114,6 +114,11 @@ export const EngineRouter: React.FC = () => { )} + {canManageEngineSearchUi && ( + + + + )} {/* TODO: Remove layout once page template migration is over */} }> {canManageEngineCurations && ( @@ -141,11 +146,6 @@ export const EngineRouter: React.FC = () => { )} - {canManageEngineSearchUi && ( - - - - )} {canViewMetaEngineSourceEngines && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.test.tsx new file mode 100644 index 0000000000000..39f0cb376b325 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.test.tsx @@ -0,0 +1,27 @@ +/* + * 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 } from 'enzyme'; + +import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; + +import { EmptyState } from './empty_state'; + +describe('EmptyState', () => { + it('renders', () => { + const wrapper = shallow() + .find(EuiEmptyPrompt) + .dive(); + + expect(wrapper.find('h2').text()).toEqual('Add documents to generate a Search UI'); + expect(wrapper.find(EuiButton).prop('href')).toEqual( + expect.stringContaining('/reference-ui-guide.html') + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.tsx new file mode 100644 index 0000000000000..b7665a58de300 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.tsx @@ -0,0 +1,46 @@ +/* + * 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 { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { DOCS_PREFIX } from '../../../routes'; + +export const EmptyState: React.FC = () => ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.searchUI.empty.title', { + defaultMessage: 'Add documents to generate a Search UI', + })} + + } + body={ +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.searchUI.empty.description', { + defaultMessage: + 'A schema will be automatically created for you after you index some documents.', + })} +

+ } + actions={ + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.searchUI.empty.buttonLabel', { + defaultMessage: 'Read the Search UI guide', + })} + + } + /> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.test.tsx index edec376dd3edd..f9f0dd611b953 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.test.tsx @@ -6,14 +6,17 @@ */ import '../../../__mocks__/shallow_useeffect.mock'; -import '../../__mocks__/engine_logic.mock'; -import { setMockActions } from '../../../__mocks__/kea_logic'; +import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic'; +import { mockEngineValues } from '../../__mocks__/engine_logic.mock'; import React from 'react'; import { shallow } from 'enzyme'; +import { SearchUIForm } from './components/search_ui_form'; +import { SearchUIGraphic } from './components/search_ui_graphic'; + import { SearchUI } from './'; describe('SearchUI', () => { @@ -24,11 +27,13 @@ describe('SearchUI', () => { beforeEach(() => { jest.clearAllMocks(); setMockActions(actions); + setMockValues(mockEngineValues); }); it('renders', () => { - shallow(); - // TODO: Check for form + const wrapper = shallow(); + expect(wrapper.find(SearchUIForm).exists()).toBe(true); + expect(wrapper.find(SearchUIGraphic).exists()).toBe(true); }); it('initializes data on mount', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.tsx index e75bc36177151..0ac59a33068ba 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.tsx @@ -7,25 +7,16 @@ import React, { useEffect } from 'react'; -import { useActions } from 'kea'; +import { useActions, useValues } from 'kea'; -import { - EuiPageHeader, - EuiPageContentBody, - EuiText, - EuiFlexItem, - EuiFlexGroup, - EuiSpacer, - EuiLink, -} from '@elastic/eui'; +import { EuiText, EuiFlexItem, EuiFlexGroup, EuiSpacer, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; - import { DOCS_PREFIX } from '../../routes'; -import { getEngineBreadcrumbs } from '../engine'; +import { EngineLogic, getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; +import { EmptyState } from './components/empty_state'; import { SearchUIForm } from './components/search_ui_form'; import { SearchUIGraphic } from './components/search_ui_graphic'; import { SEARCH_UI_TITLE } from './i18n'; @@ -33,61 +24,62 @@ import { SearchUILogic } from './search_ui_logic'; export const SearchUI: React.FC = () => { const { loadFieldData } = useActions(SearchUILogic); + const { engine } = useValues(EngineLogic); useEffect(() => { loadFieldData(); }, []); return ( - <> - - - - - - - -

- - - - ), - }} - /> -

-

- - - - ), - }} - /> -

-
- - -
- - - -
-
- + } + > + + + +

+ + + + ), + }} + /> +

+

+ + + + ), + }} + /> +

+
+ + +
+ + + +
+
); };