diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md
index 548fa66e6e51..df302e9f3b0d 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md
@@ -41,6 +41,7 @@ export declare class SearchSource
| [getSearchRequestBody()](./kibana-plugin-plugins-data-public.searchsource.getsearchrequestbody.md) | | Returns body contents of the search request, often referred as query DSL. |
| [getSerializedFields()](./kibana-plugin-plugins-data-public.searchsource.getserializedfields.md) | | serializes search source fields (which can later be passed to [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md)) |
| [onRequestStart(handler)](./kibana-plugin-plugins-data-public.searchsource.onrequeststart.md) | | Add a handler that will be notified whenever requests start |
+| [removeField(field)](./kibana-plugin-plugins-data-public.searchsource.removefield.md) | | remove field |
| [serialize()](./kibana-plugin-plugins-data-public.searchsource.serialize.md) | | Serializes the instance to a JSON string and a set of referenced objects. Use this method to get a representation of the search source which can be stored in a saved object.The references returned by this function can be mixed with other references in the same object, however make sure there are no name-collisions. The references will be named kibanaSavedObjectMeta.searchSourceJSON.index
and kibanaSavedObjectMeta.searchSourceJSON.filter[<number>].meta.index
.Using createSearchSource
, the instance can be re-created. |
| [setField(field, value)](./kibana-plugin-plugins-data-public.searchsource.setfield.md) | | sets value to a single search source field |
| [setFields(newFields)](./kibana-plugin-plugins-data-public.searchsource.setfields.md) | | Internal, do not use. Overrides all search source fields with the new field array. |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.removefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.removefield.md
new file mode 100644
index 000000000000..1e6b63be997f
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.removefield.md
@@ -0,0 +1,24 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [removeField](./kibana-plugin-plugins-data-public.searchsource.removefield.md)
+
+## SearchSource.removeField() method
+
+remove field
+
+Signature:
+
+```typescript
+removeField(field: K): this;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| field | K
| |
+
+Returns:
+
+`this`
+
diff --git a/src/plugins/data/common/search/search_source/mocks.ts b/src/plugins/data/common/search/search_source/mocks.ts
index ea7d6b4441cc..dd2b0eaccc86 100644
--- a/src/plugins/data/common/search/search_source/mocks.ts
+++ b/src/plugins/data/common/search/search_source/mocks.ts
@@ -28,6 +28,7 @@ export const searchSourceInstanceMock: MockedKeys = {
setPreferredSearchStrategyId: jest.fn(),
setFields: jest.fn().mockReturnThis(),
setField: jest.fn().mockReturnThis(),
+ removeField: jest.fn().mockReturnThis(),
getId: jest.fn(),
getFields: jest.fn(),
getField: jest.fn(),
diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts
index 98d66310c040..e7bdcb159f3c 100644
--- a/src/plugins/data/common/search/search_source/search_source.test.ts
+++ b/src/plugins/data/common/search/search_source/search_source.test.ts
@@ -82,6 +82,15 @@ describe('SearchSource', () => {
});
});
+ describe('#removeField()', () => {
+ test('remove property', () => {
+ const searchSource = new SearchSource({}, searchSourceDependencies);
+ searchSource.setField('aggs', 5);
+ searchSource.removeField('aggs');
+ expect(searchSource.getField('aggs')).toBeFalsy();
+ });
+ });
+
describe(`#setField('index')`, () => {
describe('auto-sourceFiltering', () => {
describe('new index pattern assigned', () => {
diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts
index 9bc65ca34198..79ef3a3f11ca 100644
--- a/src/plugins/data/common/search/search_source/search_source.ts
+++ b/src/plugins/data/common/search/search_source/search_source.ts
@@ -142,10 +142,18 @@ export class SearchSource {
*/
setField(field: K, value: SearchSourceFields[K]) {
if (value == null) {
- delete this.fields[field];
- } else {
- this.fields[field] = value;
+ return this.removeField(field);
}
+ this.fields[field] = value;
+ return this;
+ }
+
+ /**
+ * remove field
+ * @param field: field name
+ */
+ removeField(field: K) {
+ delete this.fields[field];
return this;
}
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index 12d6fc5ad32c..7c4d3ee27cf4 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -2169,6 +2169,7 @@ export class SearchSource {
// (undocumented)
history: SearchRequest[];
onRequestStart(handler: (searchSource: SearchSource, options?: ISearchOptions) => Promise): void;
+ removeField(field: K): this;
serialize(): {
searchSourceJSON: string;
references: import("src/core/server").SavedObjectReference[];
diff --git a/src/plugins/discover/public/__mocks__/config.ts b/src/plugins/discover/public/__mocks__/config.ts
new file mode 100644
index 000000000000..a6cdfedd795b
--- /dev/null
+++ b/src/plugins/discover/public/__mocks__/config.ts
@@ -0,0 +1,30 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { IUiSettingsClient } from '../../../../core/public';
+
+export const configMock = ({
+ get: (key: string) => {
+ if (key === 'defaultIndex') {
+ return 'the-index-pattern-id';
+ }
+
+ return '';
+ },
+} as unknown) as IUiSettingsClient;
diff --git a/src/plugins/discover/public/__mocks__/index_pattern.ts b/src/plugins/discover/public/__mocks__/index_pattern.ts
new file mode 100644
index 000000000000..696079ec72a7
--- /dev/null
+++ b/src/plugins/discover/public/__mocks__/index_pattern.ts
@@ -0,0 +1,74 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { IndexPattern, indexPatterns } from '../kibana_services';
+import { IIndexPatternFieldList } from '../../../data/common/index_patterns/fields';
+
+const fields = [
+ {
+ name: '_index',
+ type: 'string',
+ scripted: false,
+ filterable: true,
+ },
+ {
+ name: 'message',
+ type: 'string',
+ scripted: false,
+ filterable: false,
+ },
+ {
+ name: 'extension',
+ type: 'string',
+ scripted: false,
+ filterable: true,
+ },
+ {
+ name: 'bytes',
+ type: 'number',
+ scripted: false,
+ filterable: true,
+ },
+ {
+ name: 'scripted',
+ type: 'number',
+ scripted: true,
+ filterable: false,
+ },
+] as IIndexPatternFieldList;
+
+fields.getByName = (name: string) => {
+ return fields.find((field) => field.name === name);
+};
+
+const indexPattern = ({
+ id: 'the-index-pattern-id',
+ title: 'the-index-pattern-title',
+ metaFields: ['_index', '_score'],
+ flattenHit: undefined,
+ formatHit: jest.fn((hit) => hit._source),
+ fields,
+ getComputedFields: () => ({}),
+ getSourceFiltering: () => ({}),
+ getFieldByName: () => ({}),
+} as unknown) as IndexPattern;
+
+indexPattern.flattenHit = indexPatterns.flattenHitWrapper(indexPattern, indexPattern.metaFields);
+
+export const indexPatternMock = indexPattern;
diff --git a/src/plugins/discover/public/__mocks__/index_patterns.ts b/src/plugins/discover/public/__mocks__/index_patterns.ts
new file mode 100644
index 000000000000..f413a111a1d7
--- /dev/null
+++ b/src/plugins/discover/public/__mocks__/index_patterns.ts
@@ -0,0 +1,32 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { IndexPatternsService } from '../../../data/common';
+import { indexPatternMock } from './index_pattern';
+
+export const indexPatternsMock = ({
+ getCache: () => {
+ return [indexPatternMock];
+ },
+ get: (id: string) => {
+ if (id === 'the-index-pattern-id') {
+ return indexPatternMock;
+ }
+ },
+} as unknown) as IndexPatternsService;
diff --git a/src/plugins/discover/public/__mocks__/saved_search.ts b/src/plugins/discover/public/__mocks__/saved_search.ts
new file mode 100644
index 000000000000..11f36fdfde67
--- /dev/null
+++ b/src/plugins/discover/public/__mocks__/saved_search.ts
@@ -0,0 +1,41 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { SavedSearch } from '../saved_searches';
+
+export const savedSearchMock = ({
+ id: 'the-saved-search-id',
+ type: 'search',
+ attributes: {
+ title: 'the-saved-search-title',
+ kibanaSavedObjectMeta: {
+ searchSourceJSON:
+ '{"highlightAll":true,"version":true,"query":{"query":"foo : \\"bar\\" ","language":"kuery"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}',
+ },
+ },
+ references: [
+ {
+ name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
+ type: 'index-pattern',
+ id: 'the-index-pattern-id',
+ },
+ ],
+ migrationVersion: { search: '7.5.0' },
+ error: undefined,
+} as unknown) as SavedSearch;
diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js
index 9319c58db3e3..272c2f2ca618 100644
--- a/src/plugins/discover/public/application/angular/discover.js
+++ b/src/plugins/discover/public/application/angular/discover.js
@@ -18,8 +18,7 @@
*/
import _ from 'lodash';
-import React from 'react';
-import { Subscription, Subject, merge } from 'rxjs';
+import { merge, Subject, Subscription } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import moment from 'moment';
import dateMath from '@elastic/datemath';
@@ -28,31 +27,52 @@ import { getState, splitState } from './discover_state';
import { RequestAdapter } from '../../../../inspector/public';
import {
+ connectToQueryState,
esFilters,
indexPatterns as indexPatternsUtils,
- connectToQueryState,
syncQueryStateWithUrl,
} from '../../../../data/public';
-import { SavedObjectSaveModal, showSaveModal } from '../../../../saved_objects/public';
-import { getSortArray, getSortForSearchSource } from './doc_table';
+import { getSortArray } from './doc_table';
import { createFixedScroll } from './directives/fixed_scroll';
import * as columnActions from './doc_table/actions/columns';
import indexTemplateLegacy from './discover_legacy.html';
-import { showOpenSearchPanel } from '../components/top_nav/show_open_search_panel';
import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util';
import { discoverResponseHandler } from './response_handler';
import {
+ getAngularModule,
+ getHeaderActionMenuMounter,
getRequestInspectorStats,
getResponseInspectorStats,
getServices,
- getHeaderActionMenuMounter,
getUrlTracker,
- unhashUrl,
+ redirectWhenMissing,
subscribeWithScope,
tabifyAggResponse,
- getAngularModule,
- redirectWhenMissing,
} from '../../kibana_services';
+import {
+ getRootBreadcrumbs,
+ getSavedSearchBreadcrumbs,
+ setBreadcrumbsTitle,
+} from '../helpers/breadcrumbs';
+import { validateTimeRange } from '../helpers/validate_time_range';
+import { popularizeField } from '../helpers/popularize_field';
+import { getSwitchIndexPatternAppState } from '../helpers/get_switch_index_pattern_app_state';
+import { addFatalError } from '../../../../kibana_legacy/public';
+import { METRIC_TYPE } from '@kbn/analytics';
+import { SEARCH_SESSION_ID_QUERY_PARAM } from '../../url_generator';
+import { removeQueryParam, getQueryParams } from '../../../../kibana_utils/public';
+import {
+ DEFAULT_COLUMNS_SETTING,
+ MODIFY_COLUMNS_ON_SWITCH,
+ SAMPLE_SIZE_SETTING,
+ SEARCH_ON_PAGE_LOAD_SETTING,
+} from '../../../common';
+import { resolveIndexPattern, loadIndexPattern } from '../helpers/resolve_index_pattern';
+import { getTopNavLinks } from '../components/top_nav/get_top_nav_links';
+import { updateSearchSource } from '../helpers/update_search_source';
+import { calcFieldCounts } from '../helpers/calc_field_counts';
+
+const services = getServices();
const {
core,
@@ -61,30 +81,11 @@ const {
history: getHistory,
indexPatterns,
filterManager,
- share,
timefilter,
toastNotifications,
uiSettings: config,
trackUiMetric,
-} = getServices();
-
-import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../helpers/breadcrumbs';
-import { validateTimeRange } from '../helpers/validate_time_range';
-import { popularizeField } from '../helpers/popularize_field';
-import { getSwitchIndexPatternAppState } from '../helpers/get_switch_index_pattern_app_state';
-import { getIndexPatternId } from '../helpers/get_index_pattern_id';
-import { addFatalError } from '../../../../kibana_legacy/public';
-import {
- DEFAULT_COLUMNS_SETTING,
- SAMPLE_SIZE_SETTING,
- SORT_DEFAULT_ORDER_SETTING,
- SEARCH_ON_PAGE_LOAD_SETTING,
- DOC_HIDE_TIME_COLUMN_SETTING,
- MODIFY_COLUMNS_ON_SWITCH,
-} from '../../../common';
-import { METRIC_TYPE } from '@kbn/analytics';
-import { SEARCH_SESSION_ID_QUERY_PARAM } from '../../url_generator';
-import { removeQueryParam, getQueryParams } from '../../../../kibana_utils/public';
+} = services;
const fetchStatuses = {
UNINITIALIZED: 'uninitialized',
@@ -132,24 +133,7 @@ app.config(($routeProvider) => {
const { appStateContainer } = getState({ history });
const { index } = appStateContainer.getState();
return Promise.props({
- ip: indexPatterns.getCache().then((indexPatternList) => {
- /**
- * In making the indexPattern modifiable it was placed in appState. Unfortunately,
- * the load order of AppState conflicts with the load order of many other things
- * so in order to get the name of the index we should use, and to switch to the
- * default if necessary, we parse the appState with a temporary State object and
- * then destroy it immediatly after we're done
- *
- * @type {State}
- */
- const id = getIndexPatternId(index, indexPatternList, config.get('defaultIndex'));
- return Promise.props({
- list: indexPatternList,
- loaded: indexPatterns.get(id),
- stateVal: index,
- stateValFound: !!index && id === index,
- });
- }),
+ ip: loadIndexPattern(index, data.indexPatterns, config),
savedSearch: getServices()
.getSavedSearchById(savedSearchId)
.then((savedSearch) => {
@@ -204,7 +188,11 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
let inspectorRequest;
const savedSearch = $route.current.locals.savedObjects.savedSearch;
$scope.searchSource = savedSearch.searchSource;
- $scope.indexPattern = resolveIndexPatternLoading();
+ $scope.indexPattern = resolveIndexPattern(
+ $route.current.locals.savedObjects.ip,
+ $scope.searchSource,
+ toastNotifications
+ );
//used for functional testing
$scope.fetchCounter = 0;
@@ -216,22 +204,22 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
// used for restoring background session
let isInitialSearch = true;
+ const state = getState({
+ getStateDefaults,
+ storeInSessionStorage: config.get('state:storeInSessionStorage'),
+ history,
+ toasts: core.notifications.toasts,
+ });
const {
appStateContainer,
startSync: startStateSync,
stopSync: stopStateSync,
setAppState,
replaceUrlAppState,
- isAppStateDirty,
kbnUrlStateStorage,
getPreviousAppState,
- resetInitialAppState,
- } = getState({
- defaultAppState: getStateDefaults(),
- storeInSessionStorage: config.get('state:storeInSessionStorage'),
- history,
- toasts: core.notifications.toasts,
- });
+ } = state;
+
if (appStateContainer.getState().index !== $scope.indexPattern.id) {
//used index pattern is different than the given by url/state which is invalid
setAppState({ index: $scope.indexPattern.id });
@@ -349,145 +337,36 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
unlistenHistoryBasePath();
});
- const getTopNavLinks = () => {
- const newSearch = {
- id: 'new',
- label: i18n.translate('discover.localMenu.localMenu.newSearchTitle', {
- defaultMessage: 'New',
- }),
- description: i18n.translate('discover.localMenu.newSearchDescription', {
- defaultMessage: 'New Search',
- }),
- run: function () {
- $scope.$evalAsync(() => {
- history.push('/');
- });
- },
- testId: 'discoverNewButton',
- };
+ const getFieldCounts = async () => {
+ // the field counts aren't set until we have the data back,
+ // so we wait for the fetch to be done before proceeding
+ if ($scope.fetchStatus === fetchStatuses.COMPLETE) {
+ return $scope.fieldCounts;
+ }
- const saveSearch = {
- id: 'save',
- label: i18n.translate('discover.localMenu.saveTitle', {
- defaultMessage: 'Save',
- }),
- description: i18n.translate('discover.localMenu.saveSearchDescription', {
- defaultMessage: 'Save Search',
- }),
- testId: 'discoverSaveButton',
- run: async () => {
- const onSave = ({
- newTitle,
- newCopyOnSave,
- isTitleDuplicateConfirmed,
- onTitleDuplicate,
- }) => {
- const currentTitle = savedSearch.title;
- savedSearch.title = newTitle;
- savedSearch.copyOnSave = newCopyOnSave;
- const saveOptions = {
- confirmOverwrite: false,
- isTitleDuplicateConfirmed,
- onTitleDuplicate,
- };
- return saveDataSource(saveOptions).then((response) => {
- // If the save wasn't successful, put the original values back.
- if (!response.id || response.error) {
- savedSearch.title = currentTitle;
- } else {
- resetInitialAppState();
- }
- return response;
- });
- };
-
- const saveModal = (
- {}}
- title={savedSearch.title}
- showCopyOnSave={!!savedSearch.id}
- objectType="search"
- description={i18n.translate('discover.localMenu.saveSaveSearchDescription', {
- defaultMessage:
- 'Save your Discover search so you can use it in visualizations and dashboards',
- })}
- showDescription={false}
- />
- );
- showSaveModal(saveModal, core.i18n.Context);
- },
- };
-
- const openSearch = {
- id: 'open',
- label: i18n.translate('discover.localMenu.openTitle', {
- defaultMessage: 'Open',
- }),
- description: i18n.translate('discover.localMenu.openSavedSearchDescription', {
- defaultMessage: 'Open Saved Search',
- }),
- testId: 'discoverOpenButton',
- run: () => {
- showOpenSearchPanel({
- makeUrl: (searchId) => `#/view/${encodeURIComponent(searchId)}`,
- I18nContext: core.i18n.Context,
- });
- },
- };
-
- const shareSearch = {
- id: 'share',
- label: i18n.translate('discover.localMenu.shareTitle', {
- defaultMessage: 'Share',
- }),
- description: i18n.translate('discover.localMenu.shareSearchDescription', {
- defaultMessage: 'Share Search',
- }),
- testId: 'shareTopNavButton',
- run: async (anchorElement) => {
- const sharingData = await this.getSharingData();
- share.toggleShareContextMenu({
- anchorElement,
- allowEmbed: false,
- allowShortUrl: uiCapabilities.discover.createShortUrl,
- shareableUrl: unhashUrl(window.location.href),
- objectId: savedSearch.id,
- objectType: 'search',
- sharingData: {
- ...sharingData,
- title: savedSearch.title,
- },
- isDirty: !savedSearch.id || isAppStateDirty(),
- });
- },
- };
-
- const inspectSearch = {
- id: 'inspect',
- label: i18n.translate('discover.localMenu.inspectTitle', {
- defaultMessage: 'Inspect',
- }),
- description: i18n.translate('discover.localMenu.openInspectorForSearchDescription', {
- defaultMessage: 'Open Inspector for search',
- }),
- testId: 'openInspectorButton',
- run() {
- getServices().inspector.open(inspectorAdapters, {
- title: savedSearch.title,
- });
- },
- };
-
- return [
- newSearch,
- ...(uiCapabilities.discover.save ? [saveSearch] : []),
- openSearch,
- shareSearch,
- inspectSearch,
- ];
+ return await new Promise((resolve) => {
+ const unwatch = $scope.$watch('fetchStatus', (newValue) => {
+ if (newValue === fetchStatuses.COMPLETE) {
+ unwatch();
+ resolve($scope.fieldCounts);
+ }
+ });
+ });
};
- $scope.topNavMenu = getTopNavLinks();
+
+ $scope.topNavMenu = getTopNavLinks({
+ getFieldCounts,
+ indexPattern: $scope.indexPattern,
+ inspectorAdapters,
+ navigateTo: (path) => {
+ $scope.$evalAsync(() => {
+ history.push(path);
+ });
+ },
+ savedSearch,
+ services,
+ state,
+ });
$scope.searchSource
.setField('index', $scope.indexPattern)
@@ -511,96 +390,8 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
const pageTitleSuffix = savedSearch.id && savedSearch.title ? `: ${savedSearch.title}` : '';
chrome.docTitle.change(`Discover${pageTitleSuffix}`);
- const discoverBreadcrumbsTitle = i18n.translate('discover.discoverBreadcrumbTitle', {
- defaultMessage: 'Discover',
- });
- if (savedSearch.id && savedSearch.title) {
- chrome.setBreadcrumbs([
- {
- text: discoverBreadcrumbsTitle,
- href: '#/',
- },
- { text: savedSearch.title },
- ]);
- } else {
- chrome.setBreadcrumbs([
- {
- text: discoverBreadcrumbsTitle,
- },
- ]);
- }
-
- const getFieldCounts = async () => {
- // the field counts aren't set until we have the data back,
- // so we wait for the fetch to be done before proceeding
- if ($scope.fetchStatus === fetchStatuses.COMPLETE) {
- return $scope.fieldCounts;
- }
-
- return await new Promise((resolve) => {
- const unwatch = $scope.$watch('fetchStatus', (newValue) => {
- if (newValue === fetchStatuses.COMPLETE) {
- unwatch();
- resolve($scope.fieldCounts);
- }
- });
- });
- };
-
- const getSharingDataFields = async (selectedFields, timeFieldName, hideTimeColumn) => {
- if (selectedFields.length === 1 && selectedFields[0] === '_source') {
- const fieldCounts = await getFieldCounts();
- return {
- searchFields: null,
- selectFields: _.keys(fieldCounts).sort(),
- };
- }
-
- const fields =
- timeFieldName && !hideTimeColumn ? [timeFieldName, ...selectedFields] : selectedFields;
- return {
- searchFields: fields,
- selectFields: fields,
- };
- };
-
- this.getSharingData = async () => {
- const searchSource = $scope.searchSource.createCopy();
-
- const { searchFields, selectFields } = await getSharingDataFields(
- $scope.state.columns,
- $scope.indexPattern.timeFieldName,
- config.get(DOC_HIDE_TIME_COLUMN_SETTING)
- );
- searchSource.setField('fields', searchFields);
- searchSource.setField(
- 'sort',
- getSortForSearchSource(
- $scope.state.sort,
- $scope.indexPattern,
- config.get(SORT_DEFAULT_ORDER_SETTING)
- )
- );
- searchSource.setField('highlight', null);
- searchSource.setField('highlightAll', null);
- searchSource.setField('aggs', null);
- searchSource.setField('size', null);
-
- const body = await searchSource.getSearchRequestBody();
- return {
- searchRequest: {
- index: searchSource.getField('index').title,
- body,
- },
- fields: selectFields,
- metaFields: $scope.indexPattern.metaFields,
- conflictedTypesFields: $scope.indexPattern.fields
- .filter((f) => f.type === 'conflict')
- .map((f) => f.name),
- indexPatternId: searchSource.getField('index').id,
- };
- };
+ setBreadcrumbsTitle(savedSearch, chrome);
function getStateDefaults() {
const query = $scope.searchSource.getField('query') || data.query.queryString.getDefaultQuery();
@@ -739,57 +530,6 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
});
});
- async function saveDataSource(saveOptions) {
- await $scope.updateDataSource();
-
- savedSearch.columns = $scope.state.columns;
- savedSearch.sort = $scope.state.sort;
-
- try {
- const id = await savedSearch.save(saveOptions);
- $scope.$evalAsync(() => {
- if (id) {
- toastNotifications.addSuccess({
- title: i18n.translate('discover.notifications.savedSearchTitle', {
- defaultMessage: `Search '{savedSearchTitle}' was saved`,
- values: {
- savedSearchTitle: savedSearch.title,
- },
- }),
- 'data-test-subj': 'saveSearchSuccess',
- });
-
- if (savedSearch.id !== $route.current.params.id) {
- history.push(`/view/${encodeURIComponent(savedSearch.id)}`);
- } else {
- // Update defaults so that "reload saved query" functions correctly
- setAppState(getStateDefaults());
- chrome.docTitle.change(savedSearch.lastSavedTitle);
- chrome.setBreadcrumbs([
- {
- text: discoverBreadcrumbsTitle,
- href: '#/',
- },
- { text: savedSearch.title },
- ]);
- }
- }
- });
- return { id };
- } catch (saveError) {
- toastNotifications.addDanger({
- title: i18n.translate('discover.notifications.notSavedSearchTitle', {
- defaultMessage: `Search '{savedSearchTitle}' was not saved.`,
- values: {
- savedSearchTitle: savedSearch.title,
- },
- }),
- text: saveError.message,
- });
- return { error: saveError };
- }
- }
-
$scope.opts.fetch = $scope.fetch = function () {
// ignore requests to fetch before the app inits
if (!init.complete) return;
@@ -907,16 +647,11 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
$scope.hits = resp.hits.total;
$scope.rows = resp.hits.hits;
- // if we haven't counted yet, reset the counts
- const counts = ($scope.fieldCounts = $scope.fieldCounts || {});
-
- $scope.rows.forEach((hit) => {
- const fields = Object.keys($scope.indexPattern.flattenHit(hit));
- fields.forEach((fieldName) => {
- counts[fieldName] = (counts[fieldName] || 0) + 1;
- });
- });
-
+ $scope.fieldCounts = calcFieldCounts(
+ $scope.fieldCounts || {},
+ resp.hits.hits,
+ $scope.indexPattern
+ );
$scope.fetchStatus = fetchStatuses.COMPLETE;
}
@@ -944,13 +679,6 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
};
};
- $scope.toMoment = function (datetime) {
- if (!datetime) {
- return;
- }
- return moment(datetime).format(config.get('dateFormat'));
- };
-
$scope.resetQuery = function () {
history.push(
$route.current.params.id ? `/view/${encodeURIComponent($route.current.params.id)}` : '/'
@@ -979,20 +707,11 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
};
$scope.updateDataSource = () => {
- const { indexPattern, searchSource } = $scope;
- searchSource
- .setField('index', $scope.indexPattern)
- .setField('size', $scope.opts.sampleSize)
- .setField(
- 'sort',
- getSortForSearchSource(
- $scope.state.sort,
- indexPattern,
- config.get(SORT_DEFAULT_ORDER_SETTING)
- )
- )
- .setField('query', data.query.queryString.getQuery() || null)
- .setField('filter', filterManager.getFilters());
+ updateSearchSource($scope.searchSource, {
+ indexPattern: $scope.indexPattern,
+ services,
+ sort: $scope.state.sort,
+ });
return Promise.resolve();
};
@@ -1044,11 +763,6 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
const columns = columnActions.moveColumn($scope.state.columns, columnName, newIndex);
setAppState({ columns });
};
-
- $scope.scrollToTop = function () {
- $window.scrollTo(0, 0);
- };
-
async function setupVisualization() {
// If no timefield has been specified we don't create a histogram of messages
if (!getTimeField()) return;
@@ -1085,62 +799,6 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
});
}
- function getIndexPatternWarning(index) {
- return i18n.translate('discover.valueIsNotConfiguredIndexPatternIDWarningTitle', {
- defaultMessage: '{stateVal} is not a configured index pattern ID',
- values: {
- stateVal: `"${index}"`,
- },
- });
- }
-
- function resolveIndexPatternLoading() {
- const {
- loaded: loadedIndexPattern,
- stateVal,
- stateValFound,
- } = $route.current.locals.savedObjects.ip;
-
- const ownIndexPattern = $scope.searchSource.getOwnField('index');
-
- if (ownIndexPattern && !stateVal) {
- return ownIndexPattern;
- }
-
- if (stateVal && !stateValFound) {
- const warningTitle = getIndexPatternWarning();
-
- if (ownIndexPattern) {
- toastNotifications.addWarning({
- title: warningTitle,
- text: i18n.translate('discover.showingSavedIndexPatternWarningDescription', {
- defaultMessage:
- 'Showing the saved index pattern: "{ownIndexPatternTitle}" ({ownIndexPatternId})',
- values: {
- ownIndexPatternTitle: ownIndexPattern.title,
- ownIndexPatternId: ownIndexPattern.id,
- },
- }),
- });
- return ownIndexPattern;
- }
-
- toastNotifications.addWarning({
- title: warningTitle,
- text: i18n.translate('discover.showingDefaultIndexPatternWarningDescription', {
- defaultMessage:
- 'Showing the default index pattern: "{loadedIndexPatternTitle}" ({loadedIndexPatternId})',
- values: {
- loadedIndexPatternTitle: loadedIndexPattern.title,
- loadedIndexPatternId: loadedIndexPattern.id,
- },
- }),
- });
- }
-
- return loadedIndexPattern;
- }
-
addHelpMenuToAppChrome(chrome);
init();
diff --git a/src/plugins/discover/public/application/angular/discover_state.test.ts b/src/plugins/discover/public/application/angular/discover_state.test.ts
index b7b36ca96016..2914ce8f17a0 100644
--- a/src/plugins/discover/public/application/angular/discover_state.test.ts
+++ b/src/plugins/discover/public/application/angular/discover_state.test.ts
@@ -29,7 +29,7 @@ describe('Test discover state', () => {
history = createBrowserHistory();
history.push('/');
state = getState({
- defaultAppState: { index: 'test' },
+ getStateDefaults: () => ({ index: 'test' }),
history,
});
await state.replaceUrlAppState({});
@@ -84,7 +84,7 @@ describe('Test discover state with legacy migration', () => {
"/#?_a=(query:(query_string:(analyze_wildcard:!t,query:'type:nice%20name:%22yeah%22')))"
);
state = getState({
- defaultAppState: { index: 'test' },
+ getStateDefaults: () => ({ index: 'test' }),
history,
});
expect(state.appStateContainer.getState()).toMatchInlineSnapshot(`
diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts
index 5ddb6a92b5fd..3c6ef1d3e433 100644
--- a/src/plugins/discover/public/application/angular/discover_state.ts
+++ b/src/plugins/discover/public/application/angular/discover_state.ts
@@ -65,7 +65,7 @@ interface GetStateParams {
/**
* Default state used for merging with with URL state to get the initial state
*/
- defaultAppState?: AppState;
+ getStateDefaults?: () => AppState;
/**
* Determins the use of long vs. short/hashed urls
*/
@@ -123,7 +123,11 @@ export interface GetStateReturn {
/**
* Returns whether the current app state is different to the initial state
*/
- isAppStateDirty: () => void;
+ isAppStateDirty: () => boolean;
+ /**
+ * Reset AppState to default, discarding all changes
+ */
+ resetAppState: () => void;
}
const APP_STATE_URL_KEY = '_a';
@@ -132,11 +136,12 @@ const APP_STATE_URL_KEY = '_a';
* Used to sync URL with UI state
*/
export function getState({
- defaultAppState = {},
+ getStateDefaults,
storeInSessionStorage = false,
history,
toasts,
}: GetStateParams): GetStateReturn {
+ const defaultAppState = getStateDefaults ? getStateDefaults() : {};
const stateStorage = createKbnUrlStateStorage({
useHash: storeInSessionStorage,
history,
@@ -185,6 +190,10 @@ export function getState({
resetInitialAppState: () => {
initialAppState = appStateContainer.getState();
},
+ resetAppState: () => {
+ const defaultState = getStateDefaults ? getStateDefaults() : {};
+ setState(appStateContainerModified, defaultState);
+ },
getPreviousAppState: () => previousAppState,
flushToUrl: () => stateStorage.flush(),
isAppStateDirty: () => !isEqualState(initialAppState, appStateContainer.getState()),
diff --git a/src/plugins/discover/public/application/components/top_nav/__snapshots__/open_search_panel.test.js.snap b/src/plugins/discover/public/application/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap
similarity index 96%
rename from src/plugins/discover/public/application/components/top_nav/__snapshots__/open_search_panel.test.js.snap
rename to src/plugins/discover/public/application/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap
index 42cd8613b1de..2c2674b158bf 100644
--- a/src/plugins/discover/public/application/components/top_nav/__snapshots__/open_search_panel.test.js.snap
+++ b/src/plugins/discover/public/application/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap
@@ -3,7 +3,7 @@
exports[`render 1`] = `
{
+ const topNavLinks = getTopNavLinks({
+ getFieldCounts: jest.fn(),
+ indexPattern: indexPatternMock,
+ inspectorAdapters: inspectorPluginMock,
+ navigateTo: jest.fn(),
+ savedSearch: savedSearchMock,
+ services,
+ state,
+ });
+ expect(topNavLinks).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "description": "New Search",
+ "id": "new",
+ "label": "New",
+ "run": [Function],
+ "testId": "discoverNewButton",
+ },
+ Object {
+ "description": "Save Search",
+ "id": "save",
+ "label": "Save",
+ "run": [Function],
+ "testId": "discoverSaveButton",
+ },
+ Object {
+ "description": "Open Saved Search",
+ "id": "open",
+ "label": "Open",
+ "run": [Function],
+ "testId": "discoverOpenButton",
+ },
+ Object {
+ "description": "Share Search",
+ "id": "share",
+ "label": "Share",
+ "run": [Function],
+ "testId": "shareTopNavButton",
+ },
+ Object {
+ "description": "Open Inspector for search",
+ "id": "inspect",
+ "label": "Inspect",
+ "run": [Function],
+ "testId": "openInspectorButton",
+ },
+ ]
+ `);
+});
diff --git a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts
new file mode 100644
index 000000000000..62542e9ace4d
--- /dev/null
+++ b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts
@@ -0,0 +1,148 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { i18n } from '@kbn/i18n';
+import { showOpenSearchPanel } from './show_open_search_panel';
+import { getSharingData } from '../../helpers/get_sharing_data';
+import { unhashUrl } from '../../../../../kibana_utils/public';
+import { DiscoverServices } from '../../../build_services';
+import { Adapters } from '../../../../../inspector/common/adapters';
+import { SavedSearch } from '../../../saved_searches';
+import { onSaveSearch } from './on_save_search';
+import { GetStateReturn } from '../../angular/discover_state';
+import { IndexPattern } from '../../../kibana_services';
+
+/**
+ * Helper function to build the top nav links
+ */
+export const getTopNavLinks = ({
+ getFieldCounts,
+ indexPattern,
+ inspectorAdapters,
+ navigateTo,
+ savedSearch,
+ services,
+ state,
+}: {
+ getFieldCounts: () => Promise>;
+ indexPattern: IndexPattern;
+ inspectorAdapters: Adapters;
+ navigateTo: (url: string) => void;
+ savedSearch: SavedSearch;
+ services: DiscoverServices;
+ state: GetStateReturn;
+}) => {
+ const newSearch = {
+ id: 'new',
+ label: i18n.translate('discover.localMenu.localMenu.newSearchTitle', {
+ defaultMessage: 'New',
+ }),
+ description: i18n.translate('discover.localMenu.newSearchDescription', {
+ defaultMessage: 'New Search',
+ }),
+ run: () => navigateTo('/'),
+ testId: 'discoverNewButton',
+ };
+
+ const saveSearch = {
+ id: 'save',
+ label: i18n.translate('discover.localMenu.saveTitle', {
+ defaultMessage: 'Save',
+ }),
+ description: i18n.translate('discover.localMenu.saveSearchDescription', {
+ defaultMessage: 'Save Search',
+ }),
+ testId: 'discoverSaveButton',
+ run: () => onSaveSearch({ savedSearch, services, indexPattern, navigateTo, state }),
+ };
+
+ const openSearch = {
+ id: 'open',
+ label: i18n.translate('discover.localMenu.openTitle', {
+ defaultMessage: 'Open',
+ }),
+ description: i18n.translate('discover.localMenu.openSavedSearchDescription', {
+ defaultMessage: 'Open Saved Search',
+ }),
+ testId: 'discoverOpenButton',
+ run: () =>
+ showOpenSearchPanel({
+ makeUrl: (searchId) => `#/view/${encodeURIComponent(searchId)}`,
+ I18nContext: services.core.i18n.Context,
+ }),
+ };
+
+ const shareSearch = {
+ id: 'share',
+ label: i18n.translate('discover.localMenu.shareTitle', {
+ defaultMessage: 'Share',
+ }),
+ description: i18n.translate('discover.localMenu.shareSearchDescription', {
+ defaultMessage: 'Share Search',
+ }),
+ testId: 'shareTopNavButton',
+ run: async (anchorElement: HTMLElement) => {
+ if (!services.share) {
+ return;
+ }
+ const sharingData = await getSharingData(
+ savedSearch.searchSource,
+ state.appStateContainer.getState(),
+ services.uiSettings,
+ getFieldCounts
+ );
+ services.share.toggleShareContextMenu({
+ anchorElement,
+ allowEmbed: false,
+ allowShortUrl: !!services.capabilities.discover.createShortUrl,
+ shareableUrl: unhashUrl(window.location.href),
+ objectId: savedSearch.id,
+ objectType: 'search',
+ sharingData: {
+ ...sharingData,
+ title: savedSearch.title,
+ },
+ isDirty: !savedSearch.id || state.isAppStateDirty(),
+ });
+ },
+ };
+
+ const inspectSearch = {
+ id: 'inspect',
+ label: i18n.translate('discover.localMenu.inspectTitle', {
+ defaultMessage: 'Inspect',
+ }),
+ description: i18n.translate('discover.localMenu.openInspectorForSearchDescription', {
+ defaultMessage: 'Open Inspector for search',
+ }),
+ testId: 'openInspectorButton',
+ run: () => {
+ services.inspector.open(inspectorAdapters, {
+ title: savedSearch.title,
+ });
+ },
+ };
+
+ return [
+ newSearch,
+ ...(services.capabilities.discover.save ? [saveSearch] : []),
+ openSearch,
+ shareSearch,
+ inspectSearch,
+ ];
+};
diff --git a/src/plugins/discover/public/application/components/top_nav/on_save_search.test.tsx b/src/plugins/discover/public/application/components/top_nav/on_save_search.test.tsx
new file mode 100644
index 000000000000..b96af355fafd
--- /dev/null
+++ b/src/plugins/discover/public/application/components/top_nav/on_save_search.test.tsx
@@ -0,0 +1,47 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { showSaveModal } from '../../../../../saved_objects/public';
+jest.mock('../../../../../saved_objects/public');
+
+import { onSaveSearch } from './on_save_search';
+import { indexPatternMock } from '../../../__mocks__/index_pattern';
+import { savedSearchMock } from '../../../__mocks__/saved_search';
+import { DiscoverServices } from '../../../build_services';
+import { GetStateReturn } from '../../angular/discover_state';
+import { i18nServiceMock } from '../../../../../../core/public/mocks';
+
+test('onSaveSearch', async () => {
+ const serviceMock = ({
+ core: {
+ i18n: i18nServiceMock.create(),
+ },
+ } as unknown) as DiscoverServices;
+ const stateMock = ({} as unknown) as GetStateReturn;
+
+ await onSaveSearch({
+ indexPattern: indexPatternMock,
+ navigateTo: jest.fn(),
+ savedSearch: savedSearchMock,
+ services: serviceMock,
+ state: stateMock,
+ });
+
+ expect(showSaveModal).toHaveBeenCalled();
+});
diff --git a/src/plugins/discover/public/application/components/top_nav/on_save_search.tsx b/src/plugins/discover/public/application/components/top_nav/on_save_search.tsx
new file mode 100644
index 000000000000..c3343968a468
--- /dev/null
+++ b/src/plugins/discover/public/application/components/top_nav/on_save_search.tsx
@@ -0,0 +1,158 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { SavedObjectSaveModal, showSaveModal } from '../../../../../saved_objects/public';
+import { SavedSearch } from '../../../saved_searches';
+import { IndexPattern } from '../../../../../data/common/index_patterns/index_patterns';
+import { DiscoverServices } from '../../../build_services';
+import { GetStateReturn } from '../../angular/discover_state';
+import { setBreadcrumbsTitle } from '../../helpers/breadcrumbs';
+import { persistSavedSearch } from '../../helpers/persist_saved_search';
+
+async function saveDataSource({
+ indexPattern,
+ navigateTo,
+ savedSearch,
+ saveOptions,
+ services,
+ state,
+}: {
+ indexPattern: IndexPattern;
+ navigateTo: (url: string) => void;
+ savedSearch: SavedSearch;
+ saveOptions: {
+ confirmOverwrite: boolean;
+ isTitleDuplicateConfirmed: boolean;
+ onTitleDuplicate: () => void;
+ };
+ services: DiscoverServices;
+ state: GetStateReturn;
+}) {
+ const prevSavedSearchId = savedSearch.id;
+ function onSuccess(id: string) {
+ if (id) {
+ services.toastNotifications.addSuccess({
+ title: i18n.translate('discover.notifications.savedSearchTitle', {
+ defaultMessage: `Search '{savedSearchTitle}' was saved`,
+ values: {
+ savedSearchTitle: savedSearch.title,
+ },
+ }),
+ 'data-test-subj': 'saveSearchSuccess',
+ });
+
+ if (savedSearch.id !== prevSavedSearchId) {
+ navigateTo(`/view/${encodeURIComponent(savedSearch.id)}`);
+ } else {
+ // Update defaults so that "reload saved query" functions correctly
+ state.resetAppState();
+ services.chrome.docTitle.change(savedSearch.lastSavedTitle!);
+ setBreadcrumbsTitle(savedSearch, services.chrome);
+ }
+ }
+ }
+
+ function onError(error: Error) {
+ services.toastNotifications.addDanger({
+ title: i18n.translate('discover.notifications.notSavedSearchTitle', {
+ defaultMessage: `Search '{savedSearchTitle}' was not saved.`,
+ values: {
+ savedSearchTitle: savedSearch.title,
+ },
+ }),
+ text: error.message,
+ });
+ }
+ return persistSavedSearch(savedSearch, {
+ indexPattern,
+ onError,
+ onSuccess,
+ saveOptions,
+ services,
+ state: state.appStateContainer.getState(),
+ });
+}
+
+export async function onSaveSearch({
+ indexPattern,
+ navigateTo,
+ savedSearch,
+ services,
+ state,
+}: {
+ indexPattern: IndexPattern;
+ navigateTo: (path: string) => void;
+ savedSearch: SavedSearch;
+ services: DiscoverServices;
+ state: GetStateReturn;
+}) {
+ const onSave = async ({
+ newTitle,
+ newCopyOnSave,
+ isTitleDuplicateConfirmed,
+ onTitleDuplicate,
+ }: {
+ newTitle: string;
+ newCopyOnSave: boolean;
+ isTitleDuplicateConfirmed: boolean;
+ onTitleDuplicate: () => void;
+ }) => {
+ const currentTitle = savedSearch.title;
+ savedSearch.title = newTitle;
+ savedSearch.copyOnSave = newCopyOnSave;
+ const saveOptions = {
+ confirmOverwrite: false,
+ isTitleDuplicateConfirmed,
+ onTitleDuplicate,
+ };
+ const response = await saveDataSource({
+ indexPattern,
+ saveOptions,
+ services,
+ navigateTo,
+ savedSearch,
+ state,
+ });
+ // If the save wasn't successful, put the original values back.
+ if (!response.id || response.error) {
+ savedSearch.title = currentTitle;
+ } else {
+ state.resetInitialAppState();
+ }
+ return response;
+ };
+
+ const saveModal = (
+ {}}
+ title={savedSearch.title}
+ showCopyOnSave={!!savedSearch.id}
+ objectType="search"
+ description={i18n.translate('discover.localMenu.saveSaveSearchDescription', {
+ defaultMessage:
+ 'Save your Discover search so you can use it in visualizations and dashboards',
+ })}
+ showDescription={false}
+ />
+ );
+ showSaveModal(saveModal, services.core.i18n.Context);
+}
diff --git a/src/plugins/discover/public/application/components/top_nav/open_search_panel.test.js b/src/plugins/discover/public/application/components/top_nav/open_search_panel.test.tsx
similarity index 89%
rename from src/plugins/discover/public/application/components/top_nav/open_search_panel.test.js
rename to src/plugins/discover/public/application/components/top_nav/open_search_panel.test.tsx
index 50ab02c8e273..4b06964c7bc3 100644
--- a/src/plugins/discover/public/application/components/top_nav/open_search_panel.test.js
+++ b/src/plugins/discover/public/application/components/top_nav/open_search_panel.test.tsx
@@ -24,7 +24,7 @@ jest.mock('../../../kibana_services', () => {
return {
getServices: () => ({
core: { uiSettings: {}, savedObjects: {} },
- addBasePath: (path) => path,
+ addBasePath: (path: string) => path,
}),
};
});
@@ -32,6 +32,6 @@ jest.mock('../../../kibana_services', () => {
import { OpenSearchPanel } from './open_search_panel';
test('render', () => {
- const component = shallow( {}} makeUrl={() => {}} />);
+ const component = shallow();
expect(component).toMatchSnapshot();
});
diff --git a/src/plugins/discover/public/application/components/top_nav/open_search_panel.js b/src/plugins/discover/public/application/components/top_nav/open_search_panel.tsx
similarity index 94%
rename from src/plugins/discover/public/application/components/top_nav/open_search_panel.js
rename to src/plugins/discover/public/application/components/top_nav/open_search_panel.tsx
index 9a6840c29bf1..62441f7d827d 100644
--- a/src/plugins/discover/public/application/components/top_nav/open_search_panel.js
+++ b/src/plugins/discover/public/application/components/top_nav/open_search_panel.tsx
@@ -16,9 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-
import React from 'react';
-import PropTypes from 'prop-types';
import rison from 'rison-node';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -37,7 +35,12 @@ import { getServices } from '../../../kibana_services';
const SEARCH_OBJECT_TYPE = 'search';
-export function OpenSearchPanel(props) {
+interface OpenSearchPanelProps {
+ onClose: () => void;
+ makeUrl: (id: string) => string;
+}
+
+export function OpenSearchPanel(props: OpenSearchPanelProps) {
const {
core: { uiSettings, savedObjects },
addBasePath,
@@ -102,8 +105,3 @@ export function OpenSearchPanel(props) {
);
}
-
-OpenSearchPanel.propTypes = {
- onClose: PropTypes.func.isRequired,
- makeUrl: PropTypes.func.isRequired,
-};
diff --git a/src/plugins/discover/public/application/components/top_nav/show_open_search_panel.js b/src/plugins/discover/public/application/components/top_nav/show_open_search_panel.tsx
similarity index 87%
rename from src/plugins/discover/public/application/components/top_nav/show_open_search_panel.js
rename to src/plugins/discover/public/application/components/top_nav/show_open_search_panel.tsx
index e40d700b4888..d9a5cdcb063d 100644
--- a/src/plugins/discover/public/application/components/top_nav/show_open_search_panel.js
+++ b/src/plugins/discover/public/application/components/top_nav/show_open_search_panel.tsx
@@ -19,11 +19,18 @@
import React from 'react';
import ReactDOM from 'react-dom';
+import { I18nStart } from 'kibana/public';
import { OpenSearchPanel } from './open_search_panel';
let isOpen = false;
-export function showOpenSearchPanel({ makeUrl, I18nContext }) {
+export function showOpenSearchPanel({
+ makeUrl,
+ I18nContext,
+}: {
+ makeUrl: (path: string) => string;
+ I18nContext: I18nStart['Context'];
+}) {
if (isOpen) {
return;
}
diff --git a/src/plugins/discover/public/application/helpers/breadcrumbs.ts b/src/plugins/discover/public/application/helpers/breadcrumbs.ts
index 17492b02f7ea..96a9f546a063 100644
--- a/src/plugins/discover/public/application/helpers/breadcrumbs.ts
+++ b/src/plugins/discover/public/application/helpers/breadcrumbs.ts
@@ -17,7 +17,9 @@
* under the License.
*/
+import { ChromeStart } from 'kibana/public';
import { i18n } from '@kbn/i18n';
+import { SavedSearch } from '../../saved_searches';
export function getRootBreadcrumbs() {
return [
@@ -38,3 +40,29 @@ export function getSavedSearchBreadcrumbs($route: any) {
},
];
}
+
+/**
+ * Helper function to set the Discover's breadcrumb
+ * if there's an active savedSearch, its title is appended
+ */
+export function setBreadcrumbsTitle(savedSearch: SavedSearch, chrome: ChromeStart) {
+ const discoverBreadcrumbsTitle = i18n.translate('discover.discoverBreadcrumbTitle', {
+ defaultMessage: 'Discover',
+ });
+
+ if (savedSearch.id && savedSearch.title) {
+ chrome.setBreadcrumbs([
+ {
+ text: discoverBreadcrumbsTitle,
+ href: '#/',
+ },
+ { text: savedSearch.title },
+ ]);
+ } else {
+ chrome.setBreadcrumbs([
+ {
+ text: discoverBreadcrumbsTitle,
+ },
+ ]);
+ }
+}
diff --git a/src/plugins/discover/public/application/helpers/calc_field_counts.test.ts b/src/plugins/discover/public/application/helpers/calc_field_counts.test.ts
new file mode 100644
index 000000000000..ce3319bf8a66
--- /dev/null
+++ b/src/plugins/discover/public/application/helpers/calc_field_counts.test.ts
@@ -0,0 +1,58 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { calcFieldCounts } from './calc_field_counts';
+import { indexPatternMock } from '../../__mocks__/index_pattern';
+
+describe('calcFieldCounts', () => {
+ test('returns valid field count data', async () => {
+ const rows = [
+ { _id: 1, _source: { message: 'test1', bytes: 20 } },
+ { _id: 2, _source: { name: 'test2', extension: 'jpg' } },
+ ];
+ const result = calcFieldCounts({}, rows, indexPatternMock);
+ expect(result).toMatchInlineSnapshot(`
+ Object {
+ "_index": 2,
+ "_score": 2,
+ "bytes": 1,
+ "extension": 1,
+ "message": 1,
+ "name": 1,
+ }
+ `);
+ });
+ test('updates field count data', async () => {
+ const rows = [
+ { _id: 1, _source: { message: 'test1', bytes: 20 } },
+ { _id: 2, _source: { name: 'test2', extension: 'jpg' } },
+ ];
+ const result = calcFieldCounts({ message: 2 }, rows, indexPatternMock);
+ expect(result).toMatchInlineSnapshot(`
+ Object {
+ "_index": 2,
+ "_score": 2,
+ "bytes": 1,
+ "extension": 1,
+ "message": 3,
+ "name": 1,
+ }
+ `);
+ });
+});
diff --git a/src/plugins/discover/public/application/helpers/calc_field_counts.ts b/src/plugins/discover/public/application/helpers/calc_field_counts.ts
new file mode 100644
index 000000000000..02c0299995e1
--- /dev/null
+++ b/src/plugins/discover/public/application/helpers/calc_field_counts.ts
@@ -0,0 +1,38 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { IndexPattern } from '../../kibana_services';
+
+/**
+ * This function is recording stats of the available fields, for usage in sidebar and sharing
+ * Note that this values aren't displayed, but used for internal calculations
+ */
+export function calcFieldCounts(
+ counts = {} as Record,
+ rows: Array>,
+ indexPattern: IndexPattern
+) {
+ for (const hit of rows) {
+ const fields = Object.keys(indexPattern.flattenHit(hit));
+ for (const fieldName of fields) {
+ counts[fieldName] = (counts[fieldName] || 0) + 1;
+ }
+ }
+
+ return counts;
+}
diff --git a/src/plugins/discover/public/application/helpers/get_index_pattern_id.ts b/src/plugins/discover/public/application/helpers/get_index_pattern_id.ts
deleted file mode 100644
index 601f892e3c56..000000000000
--- a/src/plugins/discover/public/application/helpers/get_index_pattern_id.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { IIndexPattern } from '../../../../data/common/index_patterns';
-
-export function findIndexPatternById(
- indexPatterns: IIndexPattern[],
- id: string
-): IIndexPattern | undefined {
- if (!Array.isArray(indexPatterns) || !id) {
- return;
- }
- return indexPatterns.find((o) => o.id === id);
-}
-
-/**
- * Checks if the given defaultIndex exists and returns
- * the first available index pattern id if not
- */
-export function getFallbackIndexPatternId(
- indexPatterns: IIndexPattern[],
- defaultIndex: string = ''
-): string {
- if (defaultIndex && findIndexPatternById(indexPatterns, defaultIndex)) {
- return defaultIndex;
- }
- return !indexPatterns || !indexPatterns.length || !indexPatterns[0].id ? '' : indexPatterns[0].id;
-}
-
-/**
- * A given index pattern id is checked for existence and a fallback is provided if it doesn't exist
- * The provided defaultIndex is usually configured in Advanced Settings, if it's also invalid
- * the first entry of the given list of Indexpatterns is used
- */
-export function getIndexPatternId(
- id: string = '',
- indexPatterns: IIndexPattern[],
- defaultIndex: string = ''
-): string {
- if (!id || !findIndexPatternById(indexPatterns, id)) {
- return getFallbackIndexPatternId(indexPatterns, defaultIndex);
- }
- return id;
-}
diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts
new file mode 100644
index 000000000000..8ce9789d1dc8
--- /dev/null
+++ b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts
@@ -0,0 +1,70 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { getSharingData } from './get_sharing_data';
+import { IUiSettingsClient } from 'kibana/public';
+import { createSearchSourceMock } from '../../../../data/common/search/search_source/mocks';
+import { indexPatternMock } from '../../__mocks__/index_pattern';
+
+describe('getSharingData', () => {
+ test('returns valid data for sharing', async () => {
+ const searchSourceMock = createSearchSourceMock({ index: indexPatternMock });
+ const result = await getSharingData(
+ searchSourceMock,
+ { columns: [] },
+ ({
+ get: () => {
+ return false;
+ },
+ } as unknown) as IUiSettingsClient,
+ () => Promise.resolve({})
+ );
+ expect(result).toMatchInlineSnapshot(`
+ Object {
+ "conflictedTypesFields": Array [],
+ "fields": Array [],
+ "indexPatternId": "the-index-pattern-id",
+ "metaFields": Array [
+ "_index",
+ "_score",
+ ],
+ "searchRequest": Object {
+ "body": Object {
+ "_source": Object {
+ "includes": Array [],
+ },
+ "docvalue_fields": Array [],
+ "query": Object {
+ "bool": Object {
+ "filter": Array [],
+ "must": Array [],
+ "must_not": Array [],
+ "should": Array [],
+ },
+ },
+ "script_fields": Object {},
+ "sort": Array [],
+ "stored_fields": Array [],
+ },
+ "index": "the-index-pattern-title",
+ },
+ }
+ `);
+ });
+});
diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.ts
new file mode 100644
index 000000000000..0edaa356cba7
--- /dev/null
+++ b/src/plugins/discover/public/application/helpers/get_sharing_data.ts
@@ -0,0 +1,88 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { IUiSettingsClient } from 'kibana/public';
+import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common';
+import { getSortForSearchSource } from '../angular/doc_table';
+import { SearchSource } from '../../../../data/common';
+import { AppState } from '../angular/discover_state';
+import { SortOrder } from '../../saved_searches/types';
+
+const getSharingDataFields = async (
+ getFieldCounts: () => Promise>,
+ selectedFields: string[],
+ timeFieldName: string,
+ hideTimeColumn: boolean
+) => {
+ if (selectedFields.length === 1 && selectedFields[0] === '_source') {
+ const fieldCounts = await getFieldCounts();
+ return {
+ searchFields: undefined,
+ selectFields: Object.keys(fieldCounts).sort(),
+ };
+ }
+
+ const fields =
+ timeFieldName && !hideTimeColumn ? [timeFieldName, ...selectedFields] : selectedFields;
+ return {
+ searchFields: fields,
+ selectFields: fields,
+ };
+};
+
+/**
+ * Preparing data to share the current state as link or CSV/Report
+ */
+export async function getSharingData(
+ currentSearchSource: SearchSource,
+ state: AppState,
+ config: IUiSettingsClient,
+ getFieldCounts: () => Promise>
+) {
+ const searchSource = currentSearchSource.createCopy();
+ const index = searchSource.getField('index')!;
+
+ const { searchFields, selectFields } = await getSharingDataFields(
+ getFieldCounts,
+ state.columns || [],
+ index.timeFieldName || '',
+ config.get(DOC_HIDE_TIME_COLUMN_SETTING)
+ );
+ searchSource.setField('fields', searchFields);
+ searchSource.setField(
+ 'sort',
+ getSortForSearchSource(state.sort as SortOrder[], index, config.get(SORT_DEFAULT_ORDER_SETTING))
+ );
+ searchSource.removeField('highlight');
+ searchSource.removeField('highlightAll');
+ searchSource.removeField('aggs');
+ searchSource.removeField('size');
+
+ const body = await searchSource.getSearchRequestBody();
+
+ return {
+ searchRequest: {
+ index: index.title,
+ body,
+ },
+ fields: selectFields,
+ metaFields: index.metaFields,
+ conflictedTypesFields: index.fields.filter((f) => f.type === 'conflict').map((f) => f.name),
+ indexPatternId: index.id,
+ };
+}
diff --git a/src/plugins/discover/public/application/helpers/persist_saved_search.ts b/src/plugins/discover/public/application/helpers/persist_saved_search.ts
new file mode 100644
index 000000000000..8e956eff598f
--- /dev/null
+++ b/src/plugins/discover/public/application/helpers/persist_saved_search.ts
@@ -0,0 +1,65 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { updateSearchSource } from './update_search_source';
+import { IndexPattern } from '../../../../data/public';
+import { SavedSearch } from '../../saved_searches';
+import { AppState } from '../angular/discover_state';
+import { SortOrder } from '../../saved_searches/types';
+import { SavedObjectSaveOpts } from '../../../../saved_objects/public';
+import { DiscoverServices } from '../../build_services';
+
+/**
+ * Helper function to update and persist the given savedSearch
+ */
+export async function persistSavedSearch(
+ savedSearch: SavedSearch,
+ {
+ indexPattern,
+ onError,
+ onSuccess,
+ services,
+ saveOptions,
+ state,
+ }: {
+ indexPattern: IndexPattern;
+ onError: (error: Error, savedSearch: SavedSearch) => void;
+ onSuccess: (id: string) => void;
+ saveOptions: SavedObjectSaveOpts;
+ services: DiscoverServices;
+ state: AppState;
+ }
+) {
+ updateSearchSource(savedSearch.searchSource, {
+ indexPattern,
+ services,
+ sort: state.sort as SortOrder[],
+ });
+
+ savedSearch.columns = state.columns || [];
+ savedSearch.sort = (state.sort as SortOrder[]) || [];
+
+ try {
+ const id = await savedSearch.save(saveOptions);
+ onSuccess(id);
+ return { id };
+ } catch (saveError) {
+ onError(saveError, savedSearch);
+ return { error: saveError };
+ }
+}
diff --git a/src/plugins/discover/public/application/helpers/resolve_index_pattern.test.ts b/src/plugins/discover/public/application/helpers/resolve_index_pattern.test.ts
new file mode 100644
index 000000000000..826f738c381a
--- /dev/null
+++ b/src/plugins/discover/public/application/helpers/resolve_index_pattern.test.ts
@@ -0,0 +1,56 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+ loadIndexPattern,
+ getFallbackIndexPatternId,
+ IndexPatternSavedObject,
+} from './resolve_index_pattern';
+import { indexPatternsMock } from '../../__mocks__/index_patterns';
+import { indexPatternMock } from '../../__mocks__/index_pattern';
+import { configMock } from '../../__mocks__/config';
+
+describe('Resolve index pattern tests', () => {
+ test('returns valid data for an existing index pattern', async () => {
+ const indexPatternId = 'the-index-pattern-id';
+ const result = await loadIndexPattern(indexPatternId, indexPatternsMock, configMock);
+ expect(result.loaded).toEqual(indexPatternMock);
+ expect(result.stateValFound).toEqual(true);
+ expect(result.stateVal).toEqual(indexPatternId);
+ });
+ test('returns fallback data for an invalid index pattern', async () => {
+ const indexPatternId = 'invalid-id';
+ const result = await loadIndexPattern(indexPatternId, indexPatternsMock, configMock);
+ expect(result.loaded).toEqual(indexPatternMock);
+ expect(result.stateValFound).toBe(false);
+ expect(result.stateVal).toBe(indexPatternId);
+ });
+ test('getFallbackIndexPatternId with an empty indexPatterns array', async () => {
+ const result = await getFallbackIndexPatternId([], '');
+ expect(result).toBe('');
+ });
+ test('getFallbackIndexPatternId with an indexPatterns array', async () => {
+ const list = await indexPatternsMock.getCache();
+ const result = await getFallbackIndexPatternId(
+ (list as unknown) as IndexPatternSavedObject[],
+ ''
+ );
+ expect(result).toBe('the-index-pattern-id');
+ });
+});
diff --git a/src/plugins/discover/public/application/helpers/resolve_index_pattern.ts b/src/plugins/discover/public/application/helpers/resolve_index_pattern.ts
new file mode 100644
index 000000000000..61f7f087501b
--- /dev/null
+++ b/src/plugins/discover/public/application/helpers/resolve_index_pattern.ts
@@ -0,0 +1,158 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { i18n } from '@kbn/i18n';
+import { IUiSettingsClient, SavedObject, ToastsStart } from 'kibana/public';
+import { IndexPattern } from '../../kibana_services';
+import { IndexPatternsService, SearchSource } from '../../../../data/common';
+
+export type IndexPatternSavedObject = SavedObject & { title: string };
+
+interface IndexPatternData {
+ /**
+ * List of existing index patterns
+ */
+ list: IndexPatternSavedObject[];
+ /**
+ * Loaded index pattern (might be default index pattern if requested was not found)
+ */
+ loaded: IndexPattern;
+ /**
+ * Id of the requested index pattern
+ */
+ stateVal: string;
+ /**
+ * Determines if requested index pattern was found
+ */
+ stateValFound: boolean;
+}
+
+export function findIndexPatternById(
+ indexPatterns: IndexPatternSavedObject[],
+ id: string
+): IndexPatternSavedObject | undefined {
+ if (!Array.isArray(indexPatterns) || !id) {
+ return;
+ }
+ return indexPatterns.find((o) => o.id === id);
+}
+
+/**
+ * Checks if the given defaultIndex exists and returns
+ * the first available index pattern id if not
+ */
+export function getFallbackIndexPatternId(
+ indexPatterns: IndexPatternSavedObject[],
+ defaultIndex: string = ''
+): string {
+ if (defaultIndex && findIndexPatternById(indexPatterns, defaultIndex)) {
+ return defaultIndex;
+ }
+ return indexPatterns && indexPatterns[0]?.id ? indexPatterns[0].id : '';
+}
+
+/**
+ * A given index pattern id is checked for existence and a fallback is provided if it doesn't exist
+ * The provided defaultIndex is usually configured in Advanced Settings, if it's also invalid
+ * the first entry of the given list of Indexpatterns is used
+ */
+export function getIndexPatternId(
+ id: string = '',
+ indexPatterns: IndexPatternSavedObject[] = [],
+ defaultIndex: string = ''
+): string {
+ if (!id || !findIndexPatternById(indexPatterns, id)) {
+ return getFallbackIndexPatternId(indexPatterns, defaultIndex);
+ }
+ return id;
+}
+
+/**
+ * Function to load the given index pattern by id, providing a fallback if it doesn't exist
+ */
+export async function loadIndexPattern(
+ id: string,
+ indexPatterns: IndexPatternsService,
+ config: IUiSettingsClient
+): Promise {
+ const indexPatternList = ((await indexPatterns.getCache()) as unknown) as IndexPatternSavedObject[];
+
+ const actualId = getIndexPatternId(id, indexPatternList, config.get('defaultIndex'));
+ return {
+ list: indexPatternList || [],
+ loaded: await indexPatterns.get(actualId),
+ stateVal: id,
+ stateValFound: !!id && actualId === id,
+ };
+}
+
+/**
+ * Function used in the discover controller to message the user about the state of the current
+ * index pattern
+ */
+export function resolveIndexPattern(
+ ip: IndexPatternData,
+ searchSource: SearchSource,
+ toastNotifications: ToastsStart
+) {
+ const { loaded: loadedIndexPattern, stateVal, stateValFound } = ip;
+
+ const ownIndexPattern = searchSource.getOwnField('index');
+
+ if (ownIndexPattern && !stateVal) {
+ return ownIndexPattern;
+ }
+
+ if (stateVal && !stateValFound) {
+ const warningTitle = i18n.translate('discover.valueIsNotConfiguredIndexPatternIDWarningTitle', {
+ defaultMessage: '{stateVal} is not a configured index pattern ID',
+ values: {
+ stateVal: `"${stateVal}"`,
+ },
+ });
+
+ if (ownIndexPattern) {
+ toastNotifications.addWarning({
+ title: warningTitle,
+ text: i18n.translate('discover.showingSavedIndexPatternWarningDescription', {
+ defaultMessage:
+ 'Showing the saved index pattern: "{ownIndexPatternTitle}" ({ownIndexPatternId})',
+ values: {
+ ownIndexPatternTitle: ownIndexPattern.title,
+ ownIndexPatternId: ownIndexPattern.id,
+ },
+ }),
+ });
+ return ownIndexPattern;
+ }
+
+ toastNotifications.addWarning({
+ title: warningTitle,
+ text: i18n.translate('discover.showingDefaultIndexPatternWarningDescription', {
+ defaultMessage:
+ 'Showing the default index pattern: "{loadedIndexPatternTitle}" ({loadedIndexPatternId})',
+ values: {
+ loadedIndexPatternTitle: loadedIndexPattern.title,
+ loadedIndexPatternId: loadedIndexPattern.id,
+ },
+ }),
+ });
+ }
+
+ return loadedIndexPattern;
+}
diff --git a/src/plugins/discover/public/application/helpers/update_search_source.test.ts b/src/plugins/discover/public/application/helpers/update_search_source.test.ts
new file mode 100644
index 000000000000..91832325432e
--- /dev/null
+++ b/src/plugins/discover/public/application/helpers/update_search_source.test.ts
@@ -0,0 +1,51 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { updateSearchSource } from './update_search_source';
+import { createSearchSourceMock } from '../../../../data/common/search/search_source/mocks';
+import { indexPatternMock } from '../../__mocks__/index_pattern';
+import { IUiSettingsClient } from 'kibana/public';
+import { DiscoverServices } from '../../build_services';
+import { dataPluginMock } from '../../../../data/public/mocks';
+import { SAMPLE_SIZE_SETTING } from '../../../common';
+import { SortOrder } from '../../saved_searches/types';
+
+describe('updateSearchSource', () => {
+ test('updates a given search source', async () => {
+ const searchSourceMock = createSearchSourceMock({});
+ const sampleSize = 250;
+ const result = updateSearchSource(searchSourceMock, {
+ indexPattern: indexPatternMock,
+ services: ({
+ data: dataPluginMock.createStartContract(),
+ uiSettings: ({
+ get: (key: string) => {
+ if (key === SAMPLE_SIZE_SETTING) {
+ return sampleSize;
+ }
+ return false;
+ },
+ } as unknown) as IUiSettingsClient,
+ } as unknown) as DiscoverServices,
+ sort: [] as SortOrder[],
+ });
+ expect(result.getField('index')).toEqual(indexPatternMock);
+ expect(result.getField('size')).toEqual(sampleSize);
+ });
+});
diff --git a/src/plugins/discover/public/application/helpers/update_search_source.ts b/src/plugins/discover/public/application/helpers/update_search_source.ts
new file mode 100644
index 000000000000..324dc8a48457
--- /dev/null
+++ b/src/plugins/discover/public/application/helpers/update_search_source.ts
@@ -0,0 +1,54 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { getSortForSearchSource } from '../angular/doc_table';
+import { SAMPLE_SIZE_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common';
+import { IndexPattern, ISearchSource } from '../../../../data/common/';
+import { SortOrder } from '../../saved_searches/types';
+import { DiscoverServices } from '../../build_services';
+
+/**
+ * Helper function to update the given searchSource before fetching/sharing/persisting
+ */
+export function updateSearchSource(
+ searchSource: ISearchSource,
+ {
+ indexPattern,
+ services,
+ sort,
+ }: {
+ indexPattern: IndexPattern;
+ services: DiscoverServices;
+ sort: SortOrder[];
+ }
+) {
+ const { uiSettings, data } = services;
+ const usedSort = getSortForSearchSource(
+ sort,
+ indexPattern,
+ uiSettings.get(SORT_DEFAULT_ORDER_SETTING)
+ );
+
+ searchSource
+ .setField('index', indexPattern)
+ .setField('size', uiSettings.get(SAMPLE_SIZE_SETTING))
+ .setField('sort', usedSort)
+ .setField('query', data.query.queryString.getQuery() || null)
+ .setField('filter', data.query.filterManager.getFilters());
+ return searchSource;
+}
diff --git a/src/plugins/discover/public/saved_searches/types.ts b/src/plugins/discover/public/saved_searches/types.ts
index 13361cb647dd..d5e5dd765a36 100644
--- a/src/plugins/discover/public/saved_searches/types.ts
+++ b/src/plugins/discover/public/saved_searches/types.ts
@@ -17,18 +17,21 @@
* under the License.
*/
-import { ISearchSource } from '../../../data/public';
+import { SearchSource } from '../../../data/public';
+import { SavedObjectSaveOpts } from '../../../saved_objects/public';
export type SortOrder = [string, string];
export interface SavedSearch {
readonly id: string;
title: string;
- searchSource: ISearchSource;
+ searchSource: SearchSource;
description?: string;
columns: string[];
sort: SortOrder[];
destroy: () => void;
+ save: (saveOptions: SavedObjectSaveOpts) => Promise;
lastSavedTitle?: string;
+ copyOnSave?: boolean;
}
export interface SavedSearchLoader {
get: (id: string) => Promise;