diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index 96888a07be68f..47c79c429f662 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -29,6 +29,7 @@ export const discoverServiceMock = ({ location: { search: '', }, + listen: jest.fn(), }), data: dataPlugin, docLinks: docLinksServiceMock.createStartContract(), @@ -68,6 +69,9 @@ export const discoverServiceMock = ({ return true; }, }, + http: { + basePath: '/', + }, indexPatternFieldEditor: { openEditor: jest.fn(), userPermissions: { diff --git a/src/plugins/discover/public/application/angular/context.html b/src/plugins/discover/public/application/angular/context.html deleted file mode 100644 index 6cb5088f66605..0000000000000 --- a/src/plugins/discover/public/application/angular/context.html +++ /dev/null @@ -1,5 +0,0 @@ - - diff --git a/src/plugins/discover/public/application/angular/context.js b/src/plugins/discover/public/application/angular/context.js deleted file mode 100644 index 43e0c26b168f5..0000000000000 --- a/src/plugins/discover/public/application/angular/context.js +++ /dev/null @@ -1,43 +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 { i18n } from '@kbn/i18n'; -import { getAngularModule, getServices } from '../../kibana_services'; -import contextAppRouteTemplate from './context.html'; -import { getRootBreadcrumbs } from '../helpers/breadcrumbs'; - -const k7Breadcrumbs = () => { - return [ - ...getRootBreadcrumbs(), - { - text: i18n.translate('discover.context.breadcrumb', { - defaultMessage: 'Surrounding documents', - }), - }, - ]; -}; - -getAngularModule().config(($routeProvider) => { - $routeProvider.when('/context/:indexPatternId/:id*', { - controller: function ($routeParams, $scope, $route) { - this.indexPattern = $route.current.locals.indexPattern.ip; - this.anchorId = $routeParams.id; - this.indexPatternId = $route.current.params.indexPatternId; - }, - k7Breadcrumbs, - controllerAs: 'contextAppRoute', - reloadOnSearch: false, - resolve: { - indexPattern: ($route, Promise) => { - const indexPattern = getServices().indexPatterns.get($route.current.params.indexPatternId); - return Promise.props({ ip: indexPattern }); - }, - }, - template: contextAppRouteTemplate, - }); -}); diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index aa1344a67fbec..e2a0a19b80cf0 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -6,106 +6,14 @@ * Side Public License, v 1. */ -import { i18n } from '@kbn/i18n'; -import { getState } from '../apps/main/services/discover_state'; -import indexTemplateLegacy from './discover_legacy.html'; -import { - getAngularModule, - getServices, - getUrlTracker, - redirectWhenMissing, -} from '../../kibana_services'; -import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../helpers/breadcrumbs'; -import { loadIndexPattern, resolveIndexPattern } from '../apps/main/utils/resolve_index_pattern'; +import { getAngularModule, getServices } from '../../kibana_services'; const services = getServices(); -const { - core, - capabilities, - chrome, - data, - history: getHistory, - toastNotifications, - uiSettings: config, -} = getServices(); +const { history: getHistory } = getServices(); const app = getAngularModule(); -app.config(($routeProvider) => { - const defaults = { - requireDefaultIndex: true, - requireUICapability: 'discover.show', - k7Breadcrumbs: ($route, $injector) => - $injector.invoke($route.current.params.id ? getSavedSearchBreadcrumbs : getRootBreadcrumbs), - badge: () => { - if (capabilities.discover.save) { - return undefined; - } - - return { - text: i18n.translate('discover.badge.readOnly.text', { - defaultMessage: 'Read only', - }), - tooltip: i18n.translate('discover.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save searches', - }), - iconType: 'glasses', - }; - }, - }; - const discoverRoute = { - ...defaults, - template: indexTemplateLegacy, - reloadOnSearch: false, - resolve: { - savedObjects: function ($route, Promise) { - const history = getHistory(); - const savedSearchId = $route.current.params.id; - return data.indexPatterns.ensureDefaultIndexPattern(history).then(() => { - const { appStateContainer } = getState({ history, uiSettings: config }); - const { index } = appStateContainer.getState(); - return Promise.props({ - ip: loadIndexPattern(index, data.indexPatterns, config), - savedSearch: getServices() - .getSavedSearchById(savedSearchId) - .then((savedSearch) => { - if (savedSearchId) { - chrome.recentlyAccessed.add( - savedSearch.getFullPath(), - savedSearch.title, - savedSearchId - ); - } - return savedSearch; - }) - .catch( - redirectWhenMissing({ - history, - navigateToApp: core.application.navigateToApp, - mapping: { - search: '/', - 'index-pattern': { - app: 'management', - path: `kibana/objects/savedSearches/${$route.current.params.id}`, - }, - }, - toastNotifications, - onBeforeRedirect() { - getUrlTracker().setTrackedUrl('/'); - }, - }) - ), - }); - }); - }, - }, - }; - - $routeProvider.when('/view/:id?', discoverRoute); - $routeProvider.when('/', discoverRoute); -}); - app.directive('discoverApp', function () { return { restrict: 'E', @@ -114,21 +22,12 @@ app.directive('discoverApp', function () { }; }); -function discoverController($route, $scope) { - const savedSearch = $route.current.locals.savedObjects.savedSearch; - $scope.indexPattern = resolveIndexPattern( - $route.current.locals.savedObjects.ip, - savedSearch.searchSource, - toastNotifications - ); - +function discoverController(_, $scope) { const history = getHistory(); $scope.opts = { - savedSearch, history, services, - indexPatternList: $route.current.locals.savedObjects.ip.list, navigateTo: (path) => { $scope.$evalAsync(() => { history.push(path); @@ -136,8 +35,5 @@ function discoverController($route, $scope) { }, }; - $scope.$on('$destroy', () => { - savedSearch.destroy(); - data.search.session.clear(); - }); + $scope.$on('$destroy', () => {}); } diff --git a/src/plugins/discover/public/application/angular/doc.html b/src/plugins/discover/public/application/angular/doc.html deleted file mode 100644 index dcd5760eff155..0000000000000 --- a/src/plugins/discover/public/application/angular/doc.html +++ /dev/null @@ -1,8 +0,0 @@ -
- -
diff --git a/src/plugins/discover/public/application/angular/doc.ts b/src/plugins/discover/public/application/angular/doc.ts deleted file mode 100644 index 27af3a96bbc84..0000000000000 --- a/src/plugins/discover/public/application/angular/doc.ts +++ /dev/null @@ -1,61 +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 { getAngularModule, getServices } from '../../kibana_services'; -import { getRootBreadcrumbs } from '../helpers/breadcrumbs'; -import html from './doc.html'; -import { Doc } from '../components/doc/doc'; - -interface LazyScope extends ng.IScope { - [key: string]: unknown; -} - -const { timefilter } = getServices(); -const app = getAngularModule(); -// eslint-disable-next-line @typescript-eslint/no-explicit-any -app.directive('discoverDoc', function (reactDirective: any) { - return reactDirective( - Doc, - [ - ['id', { watchDepth: 'value' }], - ['index', { watchDepth: 'value' }], - ['indexPatternId', { watchDepth: 'reference' }], - ['indexPatternService', { watchDepth: 'reference' }], - ], - { restrict: 'E' } - ); -}); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -app.config(($routeProvider: any) => { - $routeProvider - .when('/doc/:indexPattern/:index/:type', { - redirectTo: '/doc/:indexPattern/:index', - }) - // the new route, es 7 deprecated types, es 8 removed them - .when('/doc/:indexPattern/:index', { - // have to be written as function expression, because it's not compiled in dev mode - // eslint-disable-next-line @typescript-eslint/no-explicit-any, object-shorthand - controller: function ($scope: LazyScope, $route: any) { - timefilter.disableAutoRefreshSelector(); - timefilter.disableTimeRangeSelector(); - $scope.id = $route.current.params.id; - $scope.index = $route.current.params.index; - $scope.indexPatternId = $route.current.params.indexPattern; - $scope.indexPatternService = getServices().indexPatterns; - }, - template: html, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - k7Breadcrumbs: ($route: any) => [ - ...getRootBreadcrumbs(), - { - text: `${$route.current.params.index}#${$route.current.params.id}`, - }, - ], - }); -}); diff --git a/src/plugins/discover/public/application/angular/get_inner_angular.ts b/src/plugins/discover/public/application/angular/get_inner_angular.ts index 5f459c369ce4d..5d2da54980801 100644 --- a/src/plugins/discover/public/application/angular/get_inner_angular.ts +++ b/src/plugins/discover/public/application/angular/get_inner_angular.ts @@ -16,7 +16,7 @@ import 'angular-sanitize'; import { EuiIcon } from '@elastic/eui'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { CoreStart, PluginInitializerContext } from 'kibana/public'; -import { DataPublicPluginStart } from '../../../../data/public'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; import { Storage } from '../../../../kibana_utils/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../navigation/public'; import { createContextAppLegacy } from '../components/context_app/context_app_legacy_directive'; @@ -30,7 +30,6 @@ import { import { PromiseServiceCreator } from './helpers'; import { DiscoverStartPlugins } from '../../plugin'; import { getScopedHistory } from '../../kibana_services'; -import { createDiscoverDirective } from './create_discover_directive'; /** * returns the main inner angular module, it contains all the parts of Angular Discover @@ -94,7 +93,6 @@ export function initializeInnerAngularModule( return angular .module(name, [ 'ngSanitize', - 'ngRoute', 'react', 'ui.bootstrap', 'discoverI18n', @@ -104,8 +102,7 @@ export function initializeInnerAngularModule( 'discoverDocTable', ]) .config(watchMultiDecorator) - .run(registerListenEventListener) - .directive('discover', createDiscoverDirective); + .run(registerListenEventListener); } function createLocalPromiseModule() { diff --git a/src/plugins/discover/public/application/angular/index.ts b/src/plugins/discover/public/application/angular/index.ts index c4f6415c771f9..643823a15ffcd 100644 --- a/src/plugins/discover/public/application/angular/index.ts +++ b/src/plugins/discover/public/application/angular/index.ts @@ -8,10 +8,6 @@ // required for i18nIdDirective import 'angular-sanitize'; -// required for ngRoute -import 'angular-route'; -import './discover'; import './doc'; import './context'; -import './redirect'; diff --git a/src/plugins/discover/public/application/angular/redirect.ts b/src/plugins/discover/public/application/angular/redirect.ts deleted file mode 100644 index 5014376ff13af..0000000000000 --- a/src/plugins/discover/public/application/angular/redirect.ts +++ /dev/null @@ -1,29 +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 { getAngularModule, getServices, getUrlTracker } from '../../kibana_services'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -getAngularModule().config(($routeProvider: any) => { - $routeProvider.otherwise({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - resolveRedirectTo: ($rootScope: any) => { - const path = window.location.hash.substr(1); - getUrlTracker().restorePreviousUrl(); - $rootScope.$applyAsync(() => { - const { urlForwarding } = getServices(); - const { navigated } = urlForwarding.navigateToLegacyKibanaUrl(path); - if (!navigated) { - urlForwarding.navigateToDefaultApp(); - } - }); - // prevent angular from completing the navigation - return new Promise(() => {}); - }, - }); -}); diff --git a/src/plugins/discover/public/application/application.ts b/src/plugins/discover/public/application/application.ts index af3a23860d042..cca5c1f112bb8 100644 --- a/src/plugins/discover/public/application/application.ts +++ b/src/plugins/discover/public/application/application.ts @@ -7,25 +7,34 @@ */ import './index.scss'; -import angular from 'angular'; +import { renderApp as renderReactApp } from './index'; /** * Here's where Discover's inner angular is mounted and rendered */ export async function renderApp(moduleName: string, element: HTMLElement) { - await import('./angular'); - const $injector = mountDiscoverApp(moduleName, element); - return () => $injector.get('$rootScope').$destroy(); + const app = mountDiscoverApp(moduleName, element); + return () => { + app(); + }; } -function mountDiscoverApp(moduleName: string, element: HTMLElement) { - const mountpoint = document.createElement('div'); +function buildDiscoverElement(mountpoint: HTMLElement) { + // due to legacy angular tags, we need some manual DOM intervention here const appWrapper = document.createElement('div'); - appWrapper.setAttribute('ng-view', ''); + const discoverApp = document.createElement('discover-app'); + const discover = document.createElement('discover'); + appWrapper.appendChild(discoverApp); + discoverApp.append(discover); mountpoint.appendChild(appWrapper); - // bootstrap angular into detached element and attach it later to - // make angular-within-angular possible - const $injector = angular.bootstrap(mountpoint, [moduleName]); + return discover; +} + +function mountDiscoverApp(moduleName: string, element: HTMLElement) { + const mountpoint = document.createElement('div'); + const discoverElement = buildDiscoverElement(mountpoint); + // @ts-expect-error + const app = renderReactApp({ element: discoverElement }); element.appendChild(mountpoint); - return $injector; + return app; } diff --git a/src/plugins/discover/public/application/apps/context/context_app_route.tsx b/src/plugins/discover/public/application/apps/context/context_app_route.tsx new file mode 100644 index 0000000000000..7ced3955c70e7 --- /dev/null +++ b/src/plugins/discover/public/application/apps/context/context_app_route.tsx @@ -0,0 +1,53 @@ +/* + * 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 React, { useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { DiscoverServices } from '../../../build_services'; +import { ContextApp } from '../../components/context_app/context_app'; +import { getRootBreadcrumbs } from '../../helpers/breadcrumbs'; +import { LoadingIndicator } from '../../components/common/loading_indicator'; +import { useIndexPattern } from '../../helpers/use_index_pattern'; + +export interface ContextAppProps { + /** + * Kibana core services used by discover + */ + services: DiscoverServices; +} + +export interface ContextUrlParams { + indexPatternId: string; + id: string; +} + +export function ContextAppRoute(props: ContextAppProps) { + const { services } = props; + const { chrome } = services; + + const { indexPatternId, id } = useParams(); + + useEffect(() => { + chrome.setBreadcrumbs([ + ...getRootBreadcrumbs(), + { + text: i18n.translate('discover.context.breadcrumb', { + defaultMessage: 'Surrounding documents', + }), + }, + ]); + }, [chrome]); + + const indexPattern = useIndexPattern(services.indexPatterns, indexPatternId); + + if (!indexPattern) { + return ; + } + + return ; +} diff --git a/src/plugins/discover/public/application/angular/create_discover_directive.ts b/src/plugins/discover/public/application/apps/context/index.ts similarity index 52% rename from src/plugins/discover/public/application/angular/create_discover_directive.ts rename to src/plugins/discover/public/application/apps/context/index.ts index ae0d978322bcd..a8e457dc926e7 100644 --- a/src/plugins/discover/public/application/angular/create_discover_directive.ts +++ b/src/plugins/discover/public/application/apps/context/index.ts @@ -5,12 +5,5 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { DiscoverMainApp } from '../apps/main'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createDiscoverDirective(reactDirective: any) { - return reactDirective(DiscoverMainApp, [ - ['indexPattern', { watchDepth: 'reference' }], - ['opts', { watchDepth: 'reference' }], - ]); -} +export { ContextAppRoute } from './context_app_route'; diff --git a/src/plugins/discover/public/application/apps/doc/index.ts b/src/plugins/discover/public/application/apps/doc/index.ts new file mode 100644 index 0000000000000..c62b954c8b307 --- /dev/null +++ b/src/plugins/discover/public/application/apps/doc/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export { SingleDocRoute } from './single_doc_route'; diff --git a/src/plugins/discover/public/application/apps/doc/single_doc_route.tsx b/src/plugins/discover/public/application/apps/doc/single_doc_route.tsx new file mode 100644 index 0000000000000..9088464980c26 --- /dev/null +++ b/src/plugins/discover/public/application/apps/doc/single_doc_route.tsx @@ -0,0 +1,71 @@ +/* + * 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 React, { useEffect } from 'react'; +import { useLocation, useParams } from 'react-router-dom'; +import { DiscoverServices } from '../../../build_services'; +import { getRootBreadcrumbs } from '../../helpers/breadcrumbs'; +import { Doc } from '../../components/doc/doc'; +import { LoadingIndicator } from '../../components/common/loading_indicator'; +import { useIndexPattern } from '../../helpers/use_index_pattern'; + +export interface SingleDocRouteProps { + /** + * Kibana core services used by discover + */ + services: DiscoverServices; +} + +export interface DocUrlParams { + indexPatternId: string; + index: string; +} + +function useQuery() { + return new URLSearchParams(useLocation().search); +} + +export function SingleDocRoute(props: SingleDocRouteProps) { + const { services } = props; + const { chrome, timefilter, indexPatterns } = services; + + const { indexPatternId, index } = useParams(); + + const query = useQuery(); + const docId = query.get('id') || ''; + + useEffect(() => { + chrome.setBreadcrumbs([ + ...getRootBreadcrumbs(), + { + text: `${index}#${docId}`, + }, + ]); + }, [chrome, index, docId]); + + useEffect(() => { + timefilter.disableAutoRefreshSelector(); + timefilter.disableTimeRangeSelector(); + }); + + const indexPattern = useIndexPattern(services.indexPatterns, indexPatternId); + + if (!indexPattern) { + return ; + } + + return ( +
+ +
+ ); +} diff --git a/src/plugins/discover/public/application/apps/main/discover_main_app.test.tsx b/src/plugins/discover/public/application/apps/main/discover_main_app.test.tsx index 0caa5f3f527c6..aa5a2bc9bfbad 100644 --- a/src/plugins/discover/public/application/apps/main/discover_main_app.test.tsx +++ b/src/plugins/discover/public/application/apps/main/discover_main_app.test.tsx @@ -26,18 +26,15 @@ describe('DiscoverMainApp', () => { return { ...ip, ...{ attributes: { title: ip.title } } }; }) as unknown) as Array>; - const component = mountWithIntl( - - ); + const props = { + indexPatternList, + services: discoverServiceMock, + savedSearch: savedSearchMock, + navigateTo: jest.fn(), + history, + }; + + const component = mountWithIntl(); expect(findTestSubject(component, 'indexPattern-switch-link').text()).toBe( indexPatternMock.title diff --git a/src/plugins/discover/public/application/apps/main/discover_main_app.tsx b/src/plugins/discover/public/application/apps/main/discover_main_app.tsx index 0195421fd568d..456f4ebfab62f 100644 --- a/src/plugins/discover/public/application/apps/main/discover_main_app.tsx +++ b/src/plugins/discover/public/application/apps/main/discover_main_app.tsx @@ -12,7 +12,7 @@ import { setBreadcrumbsTitle } from '../../helpers/breadcrumbs'; import { addHelpMenuToAppChrome } from '../../components/help_menu/help_menu_util'; import { useDiscoverState } from './services/use_discover_state'; import { useUrl } from './services/use_url'; -import { IndexPattern, IndexPatternAttributes, SavedObject } from '../../../../../data/common'; +import { IndexPatternAttributes, SavedObject } from '../../../../../data/common'; import { DiscoverServices } from '../../../build_services'; import { SavedSearch } from '../../../saved_searches'; @@ -20,37 +20,33 @@ const DiscoverLayoutMemoized = React.memo(DiscoverLayout); export interface DiscoverMainProps { /** - * Current IndexPattern + * Instance of browser history */ - indexPattern: IndexPattern; - - opts: { - /** - * Use angular router for navigation - */ - navigateTo: () => void; - /** - * Instance of browser history - */ - history: History; - /** - * List of available index patterns - */ - indexPatternList: Array>; - /** - * Kibana core services used by discover - */ - services: DiscoverServices; - /** - * Current instance of SavedSearch - */ - savedSearch: SavedSearch; - }; + history: History; + /** + * List of available index patterns + */ + indexPatternList: Array>; + /** + * Kibana core services used by discover + */ + services: DiscoverServices; + /** + * Current instance of SavedSearch + */ + savedSearch: SavedSearch; } export function DiscoverMainApp(props: DiscoverMainProps) { - const { services, history, navigateTo, indexPatternList } = props.opts; + const { services, history, indexPatternList } = props; const { chrome, docLinks, uiSettings: config, data } = services; + const navigateTo = useCallback( + (path: string) => { + history.push(path); + }, + [history] + ); + const savedSearch = props.savedSearch; /** * State related logic @@ -63,15 +59,13 @@ export function DiscoverMainApp(props: DiscoverMainProps) { onUpdateQuery, refetch$, resetSavedSearch, - savedSearch, searchSource, state, stateContainer, } = useDiscoverState({ services, history, - initialIndexPattern: props.indexPattern, - initialSavedSearch: props.opts.savedSearch, + savedSearch, }); /** diff --git a/src/plugins/discover/public/application/apps/main/discover_main_route.tsx b/src/plugins/discover/public/application/apps/main/discover_main_route.tsx new file mode 100644 index 0000000000000..d7b49d0231049 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/discover_main_route.tsx @@ -0,0 +1,146 @@ +/* + * 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 React, { useEffect, useState, memo } from 'react'; +import { History } from 'history'; +import { useParams } from 'react-router-dom'; +import type { SavedObject as SavedObjectDeprecated } from 'src/plugins/saved_objects/public'; +import { IndexPatternAttributes, SavedObject } from 'src/plugins/data/common'; +import { DiscoverServices } from '../../../build_services'; +import { SavedSearch } from '../../../saved_searches'; +import { getState } from './services/discover_state'; +import { loadIndexPattern, resolveIndexPattern } from './utils/resolve_index_pattern'; +import { DiscoverMainApp } from './discover_main_app'; +import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../../helpers/breadcrumbs'; +import { redirectWhenMissing } from '../../../../../kibana_utils/public'; +import { getUrlTracker } from '../../../kibana_services'; +import { LoadingIndicator } from '../../components/common/loading_indicator'; + +const DiscoverMainAppMemoized = memo(DiscoverMainApp); + +export interface DiscoverMainProps { + /** + * Instance of browser history + */ + history: History; + /** + * Kibana core services used by discover + */ + services: DiscoverServices; +} + +interface DiscoverLandingParams { + id: string; +} + +export function DiscoverMainRoute({ services, history }: DiscoverMainProps) { + const { + core, + chrome, + uiSettings: config, + data, + toastNotifications, + http: { basePath }, + } = services; + + const [savedSearch, setSavedSearch] = useState(); + const indexPattern = savedSearch?.searchSource?.getField('index'); + const [indexPatternList, setIndexPatternList] = useState< + Array> + >([]); + + const { id } = useParams(); + + useEffect(() => { + const savedSearchId = id; + + async function loadDefaultOrCurrentIndexPattern(usedSavedSearch: SavedSearch) { + await data.indexPatterns.ensureDefaultIndexPattern(); + const { appStateContainer } = getState({ history, uiSettings: config }); + const { index } = appStateContainer.getState(); + const ip = await loadIndexPattern(index || '', data.indexPatterns, config); + const ipList = ip.list as Array>; + const indexPatternData = await resolveIndexPattern( + ip, + usedSavedSearch.searchSource, + toastNotifications + ); + setIndexPatternList(ipList); + return indexPatternData; + } + + async function loadSavedSearch() { + try { + // force a refresh if a given saved search without id was saved + setSavedSearch(undefined); + const loadedSavedSearch = await services.getSavedSearchById(savedSearchId); + const loadedIndexPattern = await loadDefaultOrCurrentIndexPattern(loadedSavedSearch); + if (loadedSavedSearch && !loadedSavedSearch?.searchSource.getField('index')) { + loadedSavedSearch.searchSource.setField('index', loadedIndexPattern); + } + setSavedSearch(loadedSavedSearch); + if (savedSearchId) { + chrome.recentlyAccessed.add( + ((loadedSavedSearch as unknown) as SavedObjectDeprecated).getFullPath(), + loadedSavedSearch.title, + loadedSavedSearch.id + ); + } + } catch (e) { + redirectWhenMissing({ + history, + navigateToApp: core.application.navigateToApp, + basePath, + mapping: { + search: '/', + 'index-pattern': { + app: 'management', + path: `kibana/objects/savedSearches/${id}`, + }, + }, + toastNotifications, + onBeforeRedirect() { + getUrlTracker().setTrackedUrl('/'); + }, + })(e); + } + } + + loadSavedSearch(); + }, [ + basePath, + chrome.recentlyAccessed, + config, + core.application.navigateToApp, + data.indexPatterns, + history, + id, + services, + toastNotifications, + ]); + + useEffect(() => { + chrome.setBreadcrumbs( + savedSearch && savedSearch.title + ? getSavedSearchBreadcrumbs(savedSearch.title) + : getRootBreadcrumbs() + ); + }, [chrome, savedSearch]); + + if (!indexPattern || !savedSearch) { + return ; + } + + return ( + + ); +} diff --git a/src/plugins/discover/public/application/apps/main/index.ts b/src/plugins/discover/public/application/apps/main/index.ts index af30b0c953434..f38b745da16ab 100644 --- a/src/plugins/discover/public/application/apps/main/index.ts +++ b/src/plugins/discover/public/application/apps/main/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { DiscoverMainApp } from './discover_main_app'; +export { DiscoverMainRoute } from './discover_main_route'; diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts index 4c3d819f063a0..28f5f96acc144 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts @@ -22,14 +22,12 @@ describe('test useDiscoverState', () => { return useDiscoverState({ services: discoverServiceMock, history, - initialIndexPattern: indexPatternMock, - initialSavedSearch: savedSearchMock, + savedSearch: savedSearchMock, }); }); expect(result.current.state.index).toBe(indexPatternMock.id); expect(result.current.stateContainer).toBeInstanceOf(Object); expect(result.current.setState).toBeInstanceOf(Function); - expect(result.current.savedSearch.id).toBe(savedSearchMock.id); expect(result.current.searchSource).toBeInstanceOf(SearchSource); }); @@ -40,8 +38,7 @@ describe('test useDiscoverState', () => { return useDiscoverState({ services: discoverServiceMock, history, - initialIndexPattern: indexPatternMock, - initialSavedSearch: savedSearchMock, + savedSearch: savedSearchMock, }); }); await act(async () => { @@ -57,8 +54,7 @@ describe('test useDiscoverState', () => { return useDiscoverState({ services: discoverServiceMock, history, - initialIndexPattern: indexPatternMock, - initialSavedSearch: savedSearchMock, + savedSearch: savedSearchMock, }); }); diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts index a5a064a8fc1c6..afe010379cff3 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts @@ -10,7 +10,6 @@ import { isEqual } from 'lodash'; import { History } from 'history'; import { getState } from './discover_state'; import { getStateDefaults } from '../utils/get_state_defaults'; -import { IndexPattern } from '../../../../../../data/public'; import { DiscoverServices } from '../../../../build_services'; import { SavedSearch } from '../../../../saved_searches'; import { loadIndexPattern } from '../utils/resolve_index_pattern'; @@ -29,24 +28,22 @@ import { SortPairArr } from '../components/doc_table/lib/get_sort'; export function useDiscoverState({ services, history, - initialIndexPattern, - initialSavedSearch, + savedSearch, }: { services: DiscoverServices; - initialSavedSearch: SavedSearch; + savedSearch: SavedSearch; history: History; - initialIndexPattern: IndexPattern; }) { const { uiSettings: config, data, filterManager, indexPatterns } = services; - const [indexPattern, setIndexPattern] = useState(initialIndexPattern); - const [savedSearch, setSavedSearch] = useState(initialSavedSearch); const useNewFieldsApi = useMemo(() => !config.get(SEARCH_FIELDS_FROM_SOURCE), [config]); const { timefilter } = data.query.timefilter; + const indexPattern = savedSearch.searchSource.getField('index')!; + const searchSource = useMemo(() => { savedSearch.searchSource.setField('index', indexPattern); return savedSearch.searchSource.createChild(); - }, [savedSearch.searchSource, indexPattern]); + }, [savedSearch, indexPattern]); const stateContainer = useMemo( () => @@ -121,12 +118,10 @@ export function useDiscoverState({ * That's because appState is updated before savedSearchData$ * The following line of code catches this, but should be improved */ - reset(); const nextIndexPattern = await loadIndexPattern(nextState.index, indexPatterns, config); + savedSearch.searchSource.setField('index', nextIndexPattern.loaded); - if (nextIndexPattern) { - setIndexPattern(nextIndexPattern.loaded); - } + reset(); } if (chartDisplayChanged || chartIntervalChanged || docTableSortChanged) { @@ -135,7 +130,17 @@ export function useDiscoverState({ setState(nextState); }); return () => unsubscribe(); - }, [config, indexPatterns, appStateContainer, setState, state, refetch$, reset]); + }, [ + config, + indexPatterns, + appStateContainer, + setState, + state, + refetch$, + data$, + reset, + savedSearch.searchSource, + ]); /** * function to revert any changes to a given saved search @@ -151,11 +156,8 @@ export function useDiscoverState({ }); await stateContainer.replaceUrlAppState(newAppState); setState(newAppState); - if (savedSearch.id !== newSavedSearch.id) { - setSavedSearch(newSavedSearch); - } }, - [services, indexPattern, config, data, stateContainer, savedSearch.id] + [services, indexPattern, config, data, stateContainer] ); /** @@ -191,6 +193,20 @@ export function useDiscoverState({ [refetch$, searchSessionManager] ); + useEffect(() => { + if (!savedSearch || !savedSearch.id) { + return; + } + // handling pushing to state of a persisted saved object + const newAppState = getStateDefaults({ + config, + data, + savedSearch, + }); + stateContainer.replaceUrlAppState(newAppState); + setState(newAppState); + }, [config, data, savedSearch, reset, stateContainer]); + /** * Initial data fetching, also triggered when index pattern changes */ @@ -211,7 +227,6 @@ export function useDiscoverState({ resetSavedSearch, onChangeIndexPattern, onUpdateQuery, - savedSearch, searchSource, setState, state, diff --git a/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts b/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts index f4d05e551a4a4..7f252151920fb 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts @@ -10,7 +10,6 @@ import { renderHook } from '@testing-library/react-hooks'; import { createSearchSessionMock } from '../../../../__mocks__/search_session'; import { discoverServiceMock } from '../../../../__mocks__/services'; import { savedSearchMock } from '../../../../__mocks__/saved_search'; -import { indexPatternMock } from '../../../../__mocks__/index_pattern'; import { useSavedSearch } from './use_saved_search'; import { getState } from './discover_state'; import { uiSettingsMock } from '../../../../__mocks__/ui_settings'; @@ -59,8 +58,7 @@ describe('test useSavedSearch', () => { return useDiscoverState({ services: discoverServiceMock, history, - initialIndexPattern: indexPatternMock, - initialSavedSearch: savedSearchMock, + savedSearch: savedSearchMock, }); }); @@ -101,8 +99,7 @@ describe('test useSavedSearch', () => { return useDiscoverState({ services: discoverServiceMock, history, - initialIndexPattern: indexPatternMock, - initialSavedSearch: savedSearchMock, + savedSearch: savedSearchMock, }); }); diff --git a/src/plugins/discover/public/application/apps/not_found/index.ts b/src/plugins/discover/public/application/apps/not_found/index.ts new file mode 100644 index 0000000000000..939af542fdf6d --- /dev/null +++ b/src/plugins/discover/public/application/apps/not_found/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export { NotFoundRoute } from './not_found_route'; diff --git a/src/plugins/discover/public/application/apps/not_found/not_found_route.tsx b/src/plugins/discover/public/application/apps/not_found/not_found_route.tsx new file mode 100644 index 0000000000000..ff515f27201a4 --- /dev/null +++ b/src/plugins/discover/public/application/apps/not_found/not_found_route.tsx @@ -0,0 +1,63 @@ +/* + * 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 React, { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiCallOut } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { toMountPoint } from '../../../../../kibana_react/public'; +import { DiscoverServices } from '../../../build_services'; +import { getUrlTracker } from '../../../kibana_services'; + +export interface NotFoundRouteProps { + /** + * Kibana core services used by discover + */ + services: DiscoverServices; +} +let bannerId: string | undefined; + +export function NotFoundRoute(props: NotFoundRouteProps) { + const { services } = props; + const { urlForwarding } = services; + + useEffect(() => { + const path = window.location.hash.substr(1); + getUrlTracker().restorePreviousUrl(); + const { navigated } = urlForwarding.navigateToLegacyKibanaUrl(path); + if (!navigated) { + urlForwarding.navigateToDefaultApp(); + } + + const bannerMessage = i18n.translate('discover.noMatchRoute.bannerTitleText', { + defaultMessage: 'Page not found', + }); + + bannerId = services.core.overlays.banners.replace( + bannerId, + toMountPoint( + +

+ +

+
+ ) + ); + + // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around + setTimeout(() => { + if (bannerId) { + services.core.overlays.banners.remove(bannerId); + } + }, 15000); + }, [services.core.overlays.banners, services.history, urlForwarding]); + + return null; +} diff --git a/src/plugins/discover/public/application/components/common/__snapshots__/loading_indicator.test.tsx.snap b/src/plugins/discover/public/application/components/common/__snapshots__/loading_indicator.test.tsx.snap new file mode 100644 index 0000000000000..21f8a2b2c3632 --- /dev/null +++ b/src/plugins/discover/public/application/components/common/__snapshots__/loading_indicator.test.tsx.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Loading indicator renders correctly 1`] = ` + + +
+ +
+ + + +
+
+
+
+
+`; diff --git a/src/plugins/discover/public/application/components/common/loading_indicator.test.tsx b/src/plugins/discover/public/application/components/common/loading_indicator.test.tsx new file mode 100644 index 0000000000000..1615333471d87 --- /dev/null +++ b/src/plugins/discover/public/application/components/common/loading_indicator.test.tsx @@ -0,0 +1,17 @@ +/* + * 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 { LoadingIndicator } from './loading_indicator'; +import React from 'react'; +import { mount } from 'enzyme'; + +describe('Loading indicator', () => { + it('renders correctly', () => { + const component = mount(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/discover/public/application/components/common/loading_indicator.tsx b/src/plugins/discover/public/application/components/common/loading_indicator.tsx new file mode 100644 index 0000000000000..5261e374dfaf3 --- /dev/null +++ b/src/plugins/discover/public/application/components/common/loading_indicator.tsx @@ -0,0 +1,19 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import React from 'react'; + +export const LoadingIndicator = () => { + return ( + + + + + + ); +}; diff --git a/src/plugins/discover/public/application/components/context_app/context_app.tsx b/src/plugins/discover/public/application/components/context_app/context_app.tsx index 25590f331839e..4121beab1dd2e 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.tsx @@ -66,7 +66,7 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp * Fetch docs on ui changes */ useEffect(() => { - if (!prevAppState.current) { + if (!prevAppState.current || fetchedState.anchor._id !== anchorId) { fetchAllRows(); } else if (prevAppState.current.predecessorCount !== appState.predecessorCount) { fetchSurroundingRows(SurrDocType.PREDECESSORS); @@ -77,7 +77,15 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp } prevAppState.current = cloneDeep(appState); - }, [appState, indexPatternId, anchorId, fetchContextRows, fetchAllRows, fetchSurroundingRows]); + }, [ + appState, + indexPatternId, + anchorId, + fetchContextRows, + fetchAllRows, + fetchSurroundingRows, + fetchedState.anchor._id, + ]); const { columns, onAddColumn, onRemoveColumn, onSetColumns } = useDataGridColumns({ capabilities, diff --git a/src/plugins/discover/public/application/discover_router.test.tsx b/src/plugins/discover/public/application/discover_router.test.tsx new file mode 100644 index 0000000000000..59aede76c6866 --- /dev/null +++ b/src/plugins/discover/public/application/discover_router.test.tsx @@ -0,0 +1,60 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; +import { Route, RouteProps } from 'react-router-dom'; +import { createSearchSessionMock } from '../__mocks__/search_session'; +import { discoverServiceMock as mockDiscoverServices } from '../__mocks__/services'; +import { discoverRouter } from './discover_router'; +import { DiscoverMainRoute } from './apps/main'; +import { DiscoverMainProps } from './apps/main/discover_main_route'; +import { SingleDocRoute } from './apps/doc'; +import { ContextAppRoute } from './apps/context'; + +const pathMap: Record = {}; +let mainRouteProps: DiscoverMainProps; + +describe('Discover router', () => { + beforeAll(() => { + const { history } = createSearchSessionMock(); + mainRouteProps = { + history, + services: mockDiscoverServices, + }; + const component = shallow(discoverRouter(mockDiscoverServices, history)); + component.find(Route).forEach((route) => { + const routeProps = route.props() as RouteProps; + const path = routeProps.path; + const children = routeProps.children; + if (typeof path === 'string') { + // @ts-expect-error + pathMap[path] = children; + } + }); + }); + + it('should show DiscoverMainRoute component for / route', () => { + expect(pathMap['/']).toMatchObject(); + }); + + it('should show DiscoverMainRoute component for /view/:id route', () => { + expect(pathMap['/view/:id']).toMatchObject(); + }); + + it('should show SingleDocRoute component for /doc/:indexPatternId/:index route', () => { + expect(pathMap['/doc/:indexPatternId/:index']).toMatchObject( + + ); + }); + + it('should show ContextAppRoute component for /context/:indexPatternId/:id route', () => { + expect(pathMap['/context/:indexPatternId/:id']).toMatchObject( + + ); + }); +}); diff --git a/src/plugins/discover/public/application/discover_router.tsx b/src/plugins/discover/public/application/discover_router.tsx new file mode 100644 index 0000000000000..7c7921935a7fa --- /dev/null +++ b/src/plugins/discover/public/application/discover_router.tsx @@ -0,0 +1,52 @@ +/* + * 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 { Redirect, Route, Router, Switch } from 'react-router-dom'; +import React from 'react'; +import { History } from 'history'; +import { KibanaContextProvider } from '../../../kibana_react/public'; +import { ContextAppRoute } from './apps/context'; +import { SingleDocRoute } from './apps/doc'; +import { DiscoverMainRoute } from './apps/main'; +import { NotFoundRoute } from './apps/not_found'; +import { DiscoverServices } from '../build_services'; +import { DiscoverMainProps } from './apps/main/discover_main_route'; + +export const discoverRouter = (services: DiscoverServices, history: History) => { + const mainRouteProps: DiscoverMainProps = { + services, + history, + }; + return ( + + + + } + /> + ( + + )} + /> + } + /> + } /> + } /> + + + + + ); +}; diff --git a/src/plugins/discover/public/application/helpers/breadcrumbs.ts b/src/plugins/discover/public/application/helpers/breadcrumbs.ts index 8a8d0e7027c65..fe420328a3171 100644 --- a/src/plugins/discover/public/application/helpers/breadcrumbs.ts +++ b/src/plugins/discover/public/application/helpers/breadcrumbs.ts @@ -21,12 +21,11 @@ export function getRootBreadcrumbs() { ]; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function getSavedSearchBreadcrumbs($route: any) { +export function getSavedSearchBreadcrumbs(id: string) { return [ ...getRootBreadcrumbs(), { - text: $route.current.locals.savedObjects.savedSearch.id, + text: id, }, ]; } diff --git a/src/plugins/discover/public/application/helpers/use_index_pattern.test.tsx b/src/plugins/discover/public/application/helpers/use_index_pattern.test.tsx new file mode 100644 index 0000000000000..85282afb6fc37 --- /dev/null +++ b/src/plugins/discover/public/application/helpers/use_index_pattern.test.tsx @@ -0,0 +1,19 @@ +/* + * 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 { useIndexPattern } from './use_index_pattern'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { indexPatternsMock } from '../../__mocks__/index_patterns'; +import { renderHook, act } from '@testing-library/react-hooks'; + +describe('Use Index Pattern', () => { + test('returning a valid index pattern', async () => { + const { result } = renderHook(() => useIndexPattern(indexPatternsMock, 'the-index-pattern-id')); + await act(() => Promise.resolve()); + expect(result.current).toBe(indexPatternMock); + }); +}); diff --git a/src/plugins/discover/public/application/helpers/use_index_pattern.tsx b/src/plugins/discover/public/application/helpers/use_index_pattern.tsx new file mode 100644 index 0000000000000..f53d131920c5c --- /dev/null +++ b/src/plugins/discover/public/application/helpers/use_index_pattern.tsx @@ -0,0 +1,22 @@ +/* + * 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 { useEffect, useState } from 'react'; +import { IndexPattern, IndexPatternsContract } from '../../../../data/common'; + +export const useIndexPattern = (indexPatterns: IndexPatternsContract, indexPatternId: string) => { + const [indexPattern, setIndexPattern] = useState(undefined); + + useEffect(() => { + async function loadIndexPattern() { + const ip = await indexPatterns.get(indexPatternId); + setIndexPattern(ip); + } + loadIndexPattern(); + }); + return indexPattern; +}; diff --git a/src/plugins/discover/public/application/index.tsx b/src/plugins/discover/public/application/index.tsx new file mode 100644 index 0000000000000..4ac50eecd518a --- /dev/null +++ b/src/plugins/discover/public/application/index.tsx @@ -0,0 +1,38 @@ +/* + * 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 ReactDOM from 'react-dom'; + +import { AppMountParameters } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { getServices } from '../kibana_services'; +import { discoverRouter } from './discover_router'; + +export const renderApp = ({ element }: AppMountParameters) => { + const services = getServices(); + const { history: getHistory, capabilities, chrome, data } = services; + + const history = getHistory(); + if (!capabilities.discover.save) { + chrome.setBadge({ + text: i18n.translate('discover.badge.readOnly.text', { + defaultMessage: 'Read only', + }), + tooltip: i18n.translate('discover.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save searches', + }), + iconType: 'glasses', + }); + } + const app = discoverRouter(services, history); + ReactDOM.render(app, element); + + return () => { + data.search.session.clear(); + ReactDOM.unmountComponentAtNode(element); + }; +}; diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index b42bf6a81742c..c8e641088afad 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -17,6 +17,7 @@ import { ToastsStart, IUiSettingsClient, PluginInitializerContext, + HttpStart, } from 'kibana/public'; import { FilterManager, @@ -62,6 +63,7 @@ export interface DiscoverServices { uiSettings: IUiSettingsClient; trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; indexPatternFieldEditor: IndexPatternFieldEditorStart; + http: HttpStart; } export async function buildServices( @@ -104,5 +106,6 @@ export async function buildServices( uiSettings: core.uiSettings, trackUiMetric: usageCollection?.reportUiCounter.bind(usageCollection, 'discover'), indexPatternFieldEditor: plugins.indexPatternFieldEditor, + http: core.http, }; } diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index f657b24a5822d..65fc3ce2a82fa 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -336,6 +336,11 @@ export class DiscoverPlugin setHeaderActionMenuMounter(params.setHeaderActionMenu); syncHistoryLocations(); appMounted(); + // dispatch synthetic hash change event to update hash history objects + // this is necessary because hash updates triggered by using popState won't trigger this event naturally. + const unlistenParentHistory = params.history.listen(() => { + window.dispatchEvent(new HashChangeEvent('hashchange')); + }); const { plugins: { data: dataStart }, } = await this.initializeServices(); @@ -349,6 +354,7 @@ export class DiscoverPlugin const unmount = await renderApp(innerAngularName, params.element); return () => { params.element.classList.remove('dscAppWrapper'); + unlistenParentHistory(); unmount(); appUnMounted(); }; diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts index 3542abf9ea863..c245b45917497 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -176,6 +176,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToUrl('discover', '', { basePath: '/s/custom_space_no_index_patterns', ensureCurrentUrl: false, + shouldUseHashForSubUrl: false, }); await testSubjects.existOrFail('homeApp', { timeout: config.get('timeouts.waitFor') }); });