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') });
});