[Discover] Refactor discover.js controller topnav code (#79062)
* Move discover.js functions to helper functions in separate files * Convert to TypeScript * Add unit tests * Add removeField function to SearchSource
This commit is contained in:
parent
4aa1683b3b
commit
f294a9e2ab
|
@ -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 <code>kibanaSavedObjectMeta.searchSourceJSON.index</code> and <code>kibanaSavedObjectMeta.searchSourceJSON.filter[<number>].meta.index</code>.<!-- -->Using <code>createSearchSource</code>, 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. |
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
removeField<K extends keyof SearchSourceFields>(field: K): this;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| field | <code>K</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`this`
|
||||
|
|
@ -28,6 +28,7 @@ export const searchSourceInstanceMock: MockedKeys<ISearchSource> = {
|
|||
setPreferredSearchStrategyId: jest.fn(),
|
||||
setFields: jest.fn().mockReturnThis(),
|
||||
setField: jest.fn().mockReturnThis(),
|
||||
removeField: jest.fn().mockReturnThis(),
|
||||
getId: jest.fn(),
|
||||
getFields: jest.fn(),
|
||||
getField: jest.fn(),
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -142,10 +142,18 @@ export class SearchSource {
|
|||
*/
|
||||
setField<K extends keyof SearchSourceFields>(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<K extends keyof SearchSourceFields>(field: K) {
|
||||
delete this.fields[field];
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -2169,6 +2169,7 @@ export class SearchSource {
|
|||
// (undocumented)
|
||||
history: SearchRequest[];
|
||||
onRequestStart(handler: (searchSource: SearchSource, options?: ISearchOptions) => Promise<unknown>): void;
|
||||
removeField<K extends keyof SearchSourceFields>(field: K): this;
|
||||
serialize(): {
|
||||
searchSourceJSON: string;
|
||||
references: import("src/core/server").SavedObjectReference[];
|
||||
|
|
30
src/plugins/discover/public/__mocks__/config.ts
Normal file
30
src/plugins/discover/public/__mocks__/config.ts
Normal file
|
@ -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;
|
74
src/plugins/discover/public/__mocks__/index_pattern.ts
Normal file
74
src/plugins/discover/public/__mocks__/index_pattern.ts
Normal file
|
@ -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;
|
32
src/plugins/discover/public/__mocks__/index_patterns.ts
Normal file
32
src/plugins/discover/public/__mocks__/index_patterns.ts
Normal file
|
@ -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;
|
41
src/plugins/discover/public/__mocks__/saved_search.ts
Normal file
41
src/plugins/discover/public/__mocks__/saved_search.ts
Normal file
|
@ -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;
|
|
@ -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 = (
|
||||
<SavedObjectSaveModal
|
||||
onSave={onSave}
|
||||
onClose={() => {}}
|
||||
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();
|
||||
|
|
|
@ -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(`
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
exports[`render 1`] = `
|
||||
<EuiFlyout
|
||||
data-test-subj="loadSearchForm"
|
||||
onClose={[Function]}
|
||||
onClose={[MockFunction]}
|
||||
ownFocus={true}
|
||||
>
|
||||
<EuiFlyoutHeader
|
||||
|
@ -54,7 +54,7 @@ exports[`render 1`] = `
|
|||
<EuiButton
|
||||
fill={true}
|
||||
href="/app/management/kibana/objects?_a=(tab:search)"
|
||||
onClick={[Function]}
|
||||
onClick={[MockFunction]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Manage searches"
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 { getTopNavLinks } from './get_top_nav_links';
|
||||
import { inspectorPluginMock } from '../../../../../inspector/public/mocks';
|
||||
import { indexPatternMock } from '../../../__mocks__/index_pattern';
|
||||
import { savedSearchMock } from '../../../__mocks__/saved_search';
|
||||
import { DiscoverServices } from '../../../build_services';
|
||||
import { GetStateReturn } from '../../angular/discover_state';
|
||||
|
||||
const services = ({
|
||||
capabilities: {
|
||||
discover: {
|
||||
save: true,
|
||||
},
|
||||
},
|
||||
} as unknown) as DiscoverServices;
|
||||
|
||||
const state = ({} as unknown) as GetStateReturn;
|
||||
|
||||
test('getTopNavLinks result', () => {
|
||||
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",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
|
@ -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<Record<string, number>>;
|
||||
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,
|
||||
];
|
||||
};
|
|
@ -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();
|
||||
});
|
|
@ -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 = (
|
||||
<SavedObjectSaveModal
|
||||
onSave={onSave}
|
||||
onClose={() => {}}
|
||||
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);
|
||||
}
|
|
@ -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(<OpenSearchPanel onClose={() => {}} makeUrl={() => {}} />);
|
||||
const component = shallow(<OpenSearchPanel onClose={jest.fn()} makeUrl={jest.fn()} />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
|
@ -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) {
|
|||
</EuiFlyout>
|
||||
);
|
||||
}
|
||||
|
||||
OpenSearchPanel.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
makeUrl: PropTypes.func.isRequired,
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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<string, number>,
|
||||
rows: Array<Record<string, any>>,
|
||||
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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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<Record<string, number>>,
|
||||
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<Record<string, number>>
|
||||
) {
|
||||
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,
|
||||
};
|
||||
}
|
|
@ -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 };
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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<IndexPatternData> {
|
||||
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;
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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<string>;
|
||||
lastSavedTitle?: string;
|
||||
copyOnSave?: boolean;
|
||||
}
|
||||
export interface SavedSearchLoader {
|
||||
get: (id: string) => Promise<SavedSearch>;
|
||||
|
|
Loading…
Reference in a new issue