[Discover] Deangularize discover controller (#96766)

Co-authored-by: Tim Roes <mail@timroes.de>
This commit is contained in:
Matthias Wilhelm 2021-06-08 11:27:00 +02:00 committed by GitHub
parent 4fbb6f0baa
commit 65251051fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
169 changed files with 2299 additions and 2048 deletions

View file

@ -45,6 +45,10 @@ export const searchSourceCommonMock: jest.Mocked<ISearchStartSearchSource> = {
export const createSearchSourceMock = (fields?: SearchSourceFields) =>
new SearchSource(fields, {
getConfig: uiSettingsServiceMock.createStartContract().get,
search: jest.fn(),
search: jest
.fn()
.mockReturnValue(
of({ rawResponse: { hits: { hits: [], total: 0 } }, isPartial: false, isRunning: false })
),
onResponse: jest.fn().mockImplementation((req, res) => res),
});

View file

@ -699,7 +699,7 @@ export class SearchSource {
searchRequest.body = searchRequest.body || {};
const { body, index, query, filters, highlightAll } = searchRequest;
searchRequest.indexType = this.getIndexType(index);
const metaFields = getConfig(UI_SETTINGS.META_FIELDS);
const metaFields = getConfig(UI_SETTINGS.META_FIELDS) ?? [];
// get some special field types from the index pattern
const { docvalueFields, scriptFields, storedFields, runtimeFields } = index

View file

@ -7,12 +7,13 @@
*/
import { QueryStringContract } from '.';
import { Observable } from 'rxjs';
const createSetupContractMock = () => {
const queryStringManagerMock: jest.Mocked<QueryStringContract> = {
getQuery: jest.fn(),
setQuery: jest.fn(),
getUpdates$: jest.fn(),
getUpdates$: jest.fn(() => new Observable()),
getDefaultQuery: jest.fn(),
formatQuery: jest.fn(),
clearQuery: jest.fn(),

View file

@ -21,7 +21,7 @@ const createSetupContractMock = () => {
getTimeUpdate$: jest.fn(),
getRefreshIntervalUpdate$: jest.fn(),
getAutoRefreshFetch$: jest.fn(() => new Observable<() => void>()),
getFetch$: jest.fn(),
getFetch$: jest.fn(() => new Observable<() => void>()),
getTime: jest.fn(),
setTime: jest.fn(),
setRefreshInterval: jest.fn(),

View file

@ -65,6 +65,10 @@ fields.getByName = (name: string) => {
return fields.find((field) => field.name === name);
};
fields.getAll = () => {
return fields;
};
const indexPattern = ({
id: 'the-index-pattern-id',
title: 'the-index-pattern-title',

View file

@ -56,6 +56,9 @@ const fields = [
fields.getByName = (name: string) => {
return fields.find((field) => field.name === name);
};
fields.getAll = () => {
return fields;
};
const indexPattern = ({
id: 'index-pattern-with-timefield-id',
@ -72,5 +75,8 @@ const indexPattern = ({
indexPattern.flattenHit = indexPatterns.flattenHitWrapper(indexPattern, indexPattern.metaFields);
indexPattern.isTimeBased = () => !!indexPattern.timeFieldName;
indexPattern.formatField = (hit: Record<string, unknown>, fieldName: string) => {
return fieldName === '_source' ? hit._source : indexPattern.flattenHit(hit)[fieldName];
};
export const indexPatternWithTimefieldMock = indexPattern;

View file

@ -7,6 +7,8 @@
*/
import { SavedSearch } from '../saved_searches';
import { createSearchSourceMock } from '../../../data/public/mocks';
import { indexPatternMock } from './index_pattern';
export const savedSearchMock = ({
id: 'the-saved-search-id',
@ -27,4 +29,5 @@ export const savedSearchMock = ({
],
migrationVersion: { search: '7.5.0' },
error: undefined,
searchSource: createSearchSourceMock({ index: indexPatternMock }),
} as unknown) as SavedSearch;

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { createMemoryHistory } from 'history';
import { dataPluginMock } from '../../../data/public/mocks';
import { DataPublicPluginStart } from '../../../data/public';
import { DiscoverSearchSessionManager } from '../application/apps/main/services/discover_search_session';
export function createSearchSessionMock() {
const history = createMemoryHistory();
const session = dataPluginMock.createStartContract().search.session as jest.Mocked<
DataPublicPluginStart['search']['session']
>;
const searchSessionManager = new DiscoverSearchSessionManager({
history,
session,
});
return {
history,
session,
searchSessionManager,
};
}

View file

@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { DiscoverServices } from '../build_services';
import { dataPluginMock } from '../../../data/public/mocks';
import { chromeServiceMock, coreMock, docLinksServiceMock } from '../../../../core/public/mocks';
import { DEFAULT_COLUMNS_SETTING } from '../../common';
import { savedSearchMock } from './saved_search';
import { UI_SETTINGS } from '../../../data/common';
import { TopNavMenu } from '../../../navigation/public';
const dataPlugin = dataPluginMock.createStartContract();
export const discoverServiceMock = ({
core: coreMock.createStart(),
chrome: chromeServiceMock.createStartContract(),
history: () => ({
location: {
search: '',
},
}),
data: dataPlugin,
docLinks: docLinksServiceMock.createStartContract(),
capabilities: {
visualize: {
show: true,
},
discover: {
save: false,
},
advancedSettings: {
save: true,
},
},
filterManager: dataPlugin.query.filterManager,
uiSettings: {
get: (key: string) => {
if (key === 'fields:popularLimit') {
return 5;
} else if (key === DEFAULT_COLUMNS_SETTING) {
return [];
} else if (key === UI_SETTINGS.META_FIELDS) {
return [];
}
},
},
indexPatternFieldEditor: {
openEditor: jest.fn(),
userPermissions: {
editIndexPattern: jest.fn(),
},
},
getSavedSearchById: (id?: string) => Promise.resolve(savedSearchMock),
navigation: {
ui: { TopNavMenu },
},
metadata: {
branch: 'test',
},
} as unknown) as DiscoverServices;

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { DiscoverMainApp } from '../apps/main';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function createDiscoverDirective(reactDirective: any) {
return reactDirective(DiscoverMainApp, [
['indexPattern', { watchDepth: 'reference' }],
['opts', { watchDepth: 'reference' }],
]);
}

View file

@ -7,7 +7,7 @@
*/
import React, { useState } from 'react';
import { I18nProvider } from '@kbn/i18n/react';
import { DiscoverGrid, DiscoverGridProps } from './discover_grid/discover_grid';
import { DiscoverGrid, DiscoverGridProps } from '../components/discover_grid/discover_grid';
import { getServices } from '../../kibana_services';
import { ElasticSearchHit } from '../doc_views/doc_views_types';

View file

@ -1,57 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import _ from 'lodash';
// Debounce service, angularized version of lodash debounce
// borrowed heavily from https://github.com/shahata/angular-debounce
export function createDebounceProviderTimeout($timeout) {
return function (func, wait, options) {
let timeout;
let args;
let self;
let result;
options = _.defaults(options || {}, {
leading: false,
trailing: true,
invokeApply: true,
});
function debounce() {
self = this;
args = arguments;
const later = function () {
timeout = null;
if (!options.leading || options.trailing) {
result = func.apply(self, args);
}
};
const callNow = options.leading && !timeout;
if (timeout) {
$timeout.cancel(timeout);
}
timeout = $timeout(later, wait, options.invokeApply);
if (callNow) {
result = func.apply(self, args);
}
return result;
}
debounce.cancel = function () {
$timeout.cancel(timeout);
timeout = null;
};
return debounce;
};
}

View file

@ -1,127 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import sinon, { SinonSpy } from 'sinon';
import angular, { auto, ITimeoutService } from 'angular';
import 'angular-mocks';
import 'angular-sanitize';
import 'angular-route';
// @ts-expect-error
import { createDebounceProviderTimeout } from './debounce';
import { coreMock } from '../../../../../../../core/public/mocks';
import { initializeInnerAngularModule } from '../../../../get_inner_angular';
import { navigationPluginMock } from '../../../../../../navigation/public/mocks';
import { dataPluginMock } from '../../../../../../data/public/mocks';
import { initAngularBootstrap } from '../../../../../../kibana_legacy/public';
describe('debounce service', function () {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let debounce: (fn: () => void, timeout: number, options?: any) => any;
let $timeout: ITimeoutService;
let spy: SinonSpy;
beforeEach(() => {
spy = sinon.spy();
initAngularBootstrap();
initializeInnerAngularModule(
'app/discover',
coreMock.createStart(),
navigationPluginMock.createStartContract(),
dataPluginMock.createStartContract()
);
angular.mock.module('app/discover');
angular.mock.inject(($injector: auto.IInjectorService, _$timeout_: ITimeoutService) => {
$timeout = _$timeout_;
debounce = createDebounceProviderTimeout($timeout);
});
});
it('should have a cancel method', function () {
const bouncer = debounce(() => {}, 100);
expect(bouncer).toHaveProperty('cancel');
});
describe('delayed execution', function () {
const sandbox = sinon.createSandbox();
beforeEach(() => sandbox.useFakeTimers());
afterEach(() => sandbox.restore());
it('should delay execution', function () {
const bouncer = debounce(spy, 100);
bouncer();
sinon.assert.notCalled(spy);
$timeout.flush();
sinon.assert.calledOnce(spy);
spy.resetHistory();
});
it('should fire on leading edge', function () {
const bouncer = debounce(spy, 100, { leading: true });
bouncer();
sinon.assert.calledOnce(spy);
$timeout.flush();
sinon.assert.calledTwice(spy);
spy.resetHistory();
});
it('should only fire on leading edge', function () {
const bouncer = debounce(spy, 100, { leading: true, trailing: false });
bouncer();
sinon.assert.calledOnce(spy);
$timeout.flush();
sinon.assert.calledOnce(spy);
spy.resetHistory();
});
it('should reset delayed execution', function () {
const cancelSpy = sinon.spy($timeout, 'cancel');
const bouncer = debounce(spy, 100);
bouncer();
sandbox.clock.tick(1);
bouncer();
sinon.assert.notCalled(spy);
$timeout.flush();
sinon.assert.calledOnce(spy);
sinon.assert.calledOnce(cancelSpy);
spy.resetHistory();
cancelSpy.resetHistory();
});
});
describe('cancel', function () {
it('should cancel the $timeout', function () {
const cancelSpy = sinon.spy($timeout, 'cancel');
const bouncer = debounce(spy, 100);
bouncer();
bouncer.cancel();
sinon.assert.calledOnce(cancelSpy);
// throws if pending timeouts
$timeout.verifyNoPendingTasks();
cancelSpy.resetHistory();
});
});
});

View file

@ -6,47 +6,17 @@
* Side Public License, v 1.
*/
import _ from 'lodash';
import { merge, Subject, Subscription } from 'rxjs';
import { debounceTime, tap, filter } from 'rxjs/operators';
import { i18n } from '@kbn/i18n';
import { createSearchSessionRestorationDataProvider, getState, splitState } from './discover_state';
import { RequestAdapter } from '../../../../inspector/public';
import {
connectToQueryState,
esFilters,
indexPatterns as indexPatternsUtils,
noSearchSessionStorageCapabilityMessage,
syncQueryStateWithUrl,
} from '../../../../data/public';
import { getSortArray } from './doc_table';
import { getState } from '../apps/main/services/discover_state';
import indexTemplateLegacy from './discover_legacy.html';
import { discoverResponseHandler } from './response_handler';
import {
getAngularModule,
getHeaderActionMenuMounter,
getServices,
getUrlTracker,
redirectWhenMissing,
subscribeWithScope,
tabifyAggResponse,
} from '../../kibana_services';
import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../helpers/breadcrumbs';
import { getStateDefaults } from '../helpers/get_state_defaults';
import { getResultState } from '../helpers/get_result_state';
import { validateTimeRange } from '../helpers/validate_time_range';
import { addFatalError } from '../../../../kibana_legacy/public';
import {
SAMPLE_SIZE_SETTING,
SEARCH_FIELDS_FROM_SOURCE,
SEARCH_ON_PAGE_LOAD_SETTING,
} from '../../../common';
import { loadIndexPattern, resolveIndexPattern } from '../helpers/resolve_index_pattern';
import { updateSearchSource } from '../helpers/update_search_source';
import { calcFieldCounts } from '../helpers/calc_field_counts';
import { DiscoverSearchSessionManager } from './discover_search_session';
import { applyAggsToSearchSource, getDimensions } from '../components/histogram';
import { fetchStatuses } from '../components/constants';
import { loadIndexPattern, resolveIndexPattern } from '../apps/main/utils/resolve_index_pattern';
const services = getServices();
@ -56,8 +26,6 @@ const {
chrome,
data,
history: getHistory,
filterManager,
timefilter,
toastNotifications,
uiSettings: config,
} = getServices();
@ -147,390 +115,29 @@ app.directive('discoverApp', function () {
});
function discoverController($route, $scope) {
const { isDefault: isDefaultType } = indexPatternsUtils;
const subscriptions = new Subscription();
const refetch$ = new Subject();
let isChangingIndexPattern = false;
const savedSearch = $route.current.locals.savedObjects.savedSearch;
const persistentSearchSource = savedSearch.searchSource;
$scope.indexPattern = resolveIndexPattern(
$route.current.locals.savedObjects.ip,
persistentSearchSource,
savedSearch.searchSource,
toastNotifications
);
$scope.useNewFieldsApi = !config.get(SEARCH_FIELDS_FROM_SOURCE);
//used for functional testing
$scope.fetchCounter = 0;
const getTimeField = () => {
return isDefaultType($scope.indexPattern) ? $scope.indexPattern.timeFieldName : undefined;
};
const history = getHistory();
const searchSessionManager = new DiscoverSearchSessionManager({
history,
session: data.search.session,
});
const stateContainer = getState({
getStateDefaults: () =>
getStateDefaults({
config,
data,
indexPattern: $scope.indexPattern,
savedSearch,
searchSource: persistentSearchSource,
}),
storeInSessionStorage: config.get('state:storeInSessionStorage'),
history,
toasts: core.notifications.toasts,
uiSettings: config,
});
const {
appStateContainer,
startSync: startStateSync,
stopSync: stopStateSync,
setAppState,
replaceUrlAppState,
kbnUrlStateStorage,
getPreviousAppState,
} = stateContainer;
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 });
}
$scope.state = { ...appStateContainer.getState() };
// syncs `_g` portion of url with query services
const { stop: stopSyncingGlobalStateWithUrl } = syncQueryStateWithUrl(
data.query,
kbnUrlStateStorage
);
// sync initial app filters from state to filterManager
filterManager.setAppFilters(_.cloneDeep(appStateContainer.getState().filters));
data.query.queryString.setQuery(appStateContainer.getState().query);
const stopSyncingQueryAppStateWithStateContainer = connectToQueryState(
data.query,
appStateContainer,
{
filters: esFilters.FilterStateStore.APP_STATE,
query: true,
}
);
const showUnmappedFields = $scope.useNewFieldsApi;
const updateSearchSourceHelper = () => {
const { indexPattern, useNewFieldsApi } = $scope;
const { columns, sort } = $scope.state;
updateSearchSource({
persistentSearchSource,
volatileSearchSource: $scope.volatileSearchSource,
indexPattern,
services,
sort,
columns,
useNewFieldsApi,
showUnmappedFields,
});
};
const appStateUnsubscribe = appStateContainer.subscribe(async (newState) => {
const { state: newStatePartial } = splitState(newState);
const { state: oldStatePartial } = splitState(getPreviousAppState());
if (!_.isEqual(newStatePartial, oldStatePartial)) {
$scope.$evalAsync(async () => {
// NOTE: this is also called when navigating from discover app to context app
if (newStatePartial.index && oldStatePartial.index !== newStatePartial.index) {
//in case of index pattern switch the route has currently to be reloaded, legacy
isChangingIndexPattern = true;
$route.reload();
return;
}
$scope.state = { ...newState };
// detect changes that should trigger fetching of new data
const changes = ['interval', 'sort'].filter(
(prop) => !_.isEqual(newStatePartial[prop], oldStatePartial[prop])
);
if (oldStatePartial.hideChart && !newStatePartial.hideChart) {
// in case the histogram is hidden, no data is requested
// so when changing this state data needs to be fetched
changes.push(true);
}
if (changes.length) {
refetch$.next();
}
});
}
});
// this listener is waiting for such a path http://localhost:5601/app/discover#/
// which could be set through pressing "New" button in top nav or go to "Discover" plugin from the sidebar
// to reload the page in a right way
const unlistenHistoryBasePath = history.listen(({ pathname, search, hash }) => {
if (!search && !hash && pathname === '/') {
$route.reload();
}
});
data.search.session.enableStorage(
createSearchSessionRestorationDataProvider({
appStateContainer,
data,
getSavedSearch: () => savedSearch,
}),
{
isDisabled: () =>
capabilities.discover.storeSearchSession
? { disabled: false }
: {
disabled: true,
reasonText: noSearchSessionStorageCapabilityMessage,
},
}
);
$scope.opts = {
// number of records to fetch, then paginate through
sampleSize: config.get(SAMPLE_SIZE_SETTING),
timefield: getTimeField(),
savedSearch: savedSearch,
savedSearch,
history,
services,
indexPatternList: $route.current.locals.savedObjects.ip.list,
config: config,
setHeaderActionMenu: getHeaderActionMenuMounter(),
filterManager,
setAppState,
data,
stateContainer,
searchSessionManager,
refetch$,
navigateTo: (path) => {
$scope.$evalAsync(() => {
history.push(path);
});
},
};
const inspectorAdapters = ($scope.opts.inspectorAdapters = {
requests: new RequestAdapter(),
});
const shouldSearchOnPageLoad = () => {
// A saved search is created on every page load, so we check the ID to see if we're loading a
// previously saved search or if it is just transient
return (
config.get(SEARCH_ON_PAGE_LOAD_SETTING) ||
savedSearch.id !== undefined ||
timefilter.getRefreshInterval().pause === false ||
searchSessionManager.hasSearchSessionIdInURL()
);
};
$scope.fetchStatus = fetchStatuses.UNINITIALIZED;
$scope.resultState = shouldSearchOnPageLoad() ? 'loading' : 'uninitialized';
let abortController;
$scope.$on('$destroy', () => {
if (abortController) abortController.abort();
savedSearch.destroy();
subscriptions.unsubscribe();
if (!isChangingIndexPattern) {
// HACK:
// do not clear session when changing index pattern due to how state management around it is setup
// it will be cleared by searchSessionManager on controller reload instead
data.search.session.clear();
}
appStateUnsubscribe();
stopStateSync();
stopSyncingGlobalStateWithUrl();
stopSyncingQueryAppStateWithStateContainer();
unlistenHistoryBasePath();
});
$scope.opts.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);
}
});
});
};
$scope.opts.navigateTo = (path) => {
$scope.$evalAsync(() => {
history.push(path);
});
};
persistentSearchSource.setField('index', $scope.indexPattern);
// searchSource which applies time range
const volatileSearchSource = savedSearch.searchSource.create();
if (isDefaultType($scope.indexPattern)) {
volatileSearchSource.setField('filter', () => {
return timefilter.createFilter($scope.indexPattern);
});
}
volatileSearchSource.setParent(persistentSearchSource);
$scope.volatileSearchSource = volatileSearchSource;
$scope.state.index = $scope.indexPattern.id;
$scope.state.sort = getSortArray($scope.state.sort, $scope.indexPattern);
$scope.opts.fetch = $scope.fetch = async function () {
$scope.fetchCounter++;
$scope.fetchError = undefined;
if (!validateTimeRange(timefilter.getTime(), toastNotifications)) {
$scope.resultState = 'none';
}
// Abort any in-progress requests before fetching again
if (abortController) abortController.abort();
abortController = new AbortController();
const searchSessionId = searchSessionManager.getNextSearchSessionId();
updateSearchSourceHelper();
$scope.opts.chartAggConfigs = applyAggsToSearchSource(
getTimeField() && !$scope.state.hideChart,
volatileSearchSource,
$scope.state.interval,
$scope.indexPattern,
data
);
$scope.fetchStatus = fetchStatuses.LOADING;
$scope.resultState = getResultState($scope.fetchStatus, $scope.rows);
inspectorAdapters.requests.reset();
return $scope.volatileSearchSource
.fetch$({
abortSignal: abortController.signal,
sessionId: searchSessionId,
inspector: {
adapter: inspectorAdapters.requests,
title: i18n.translate('discover.inspectorRequestDataTitle', {
defaultMessage: 'data',
}),
description: i18n.translate('discover.inspectorRequestDescription', {
defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.',
}),
},
})
.toPromise()
.then(({ rawResponse }) => onResults(rawResponse))
.catch((error) => {
// If the request was aborted then no need to surface this error in the UI
if (error instanceof Error && error.name === 'AbortError') return;
$scope.fetchStatus = fetchStatuses.NO_RESULTS;
$scope.fetchError = error;
data.search.showError(error);
})
.finally(() => {
$scope.resultState = getResultState($scope.fetchStatus, $scope.rows);
$scope.$apply();
});
};
function onResults(resp) {
if (getTimeField() && !$scope.state.hideChart) {
const tabifiedData = tabifyAggResponse($scope.opts.chartAggConfigs, resp);
$scope.volatileSearchSource.rawResponse = resp;
const dimensions = getDimensions($scope.opts.chartAggConfigs, data);
if (dimensions) {
$scope.histogramData = discoverResponseHandler(tabifiedData, dimensions);
}
}
$scope.hits = resp.hits.total;
$scope.rows = resp.hits.hits;
$scope.fieldCounts = calcFieldCounts(
$scope.fieldCounts || {},
resp.hits.hits,
$scope.indexPattern
);
$scope.fetchStatus = fetchStatuses.COMPLETE;
}
$scope.refreshAppState = async () => {
$scope.hits = [];
$scope.rows = [];
$scope.fieldCounts = {};
await refetch$.next();
};
$scope.resetQuery = function () {
history.push(
$route.current.params.id ? `/view/${encodeURIComponent($route.current.params.id)}` : '/'
);
$route.reload();
};
$scope.newQuery = function () {
history.push('/');
};
$scope.unmappedFieldsConfig = {
showUnmappedFields,
};
// handler emitted by `timefilter.getAutoRefreshFetch$()`
// to notify when data completed loading and to start a new autorefresh loop
let autoRefreshDoneCb;
const fetch$ = merge(
refetch$,
filterManager.getFetches$(),
timefilter.getFetch$(),
timefilter.getAutoRefreshFetch$().pipe(
tap((done) => {
autoRefreshDoneCb = done;
}),
filter(() => $scope.fetchStatus !== fetchStatuses.LOADING)
),
data.query.queryString.getUpdates$(),
searchSessionManager.newSearchSessionIdFromURL$
).pipe(debounceTime(100));
subscriptions.add(
subscribeWithScope(
$scope,
fetch$,
{
next: async () => {
try {
await $scope.fetch();
} finally {
// if there is a saved `autoRefreshDoneCb`, notify auto refresh service that
// the last fetch is completed so it starts the next auto refresh loop if needed
autoRefreshDoneCb?.();
autoRefreshDoneCb = undefined;
}
},
},
(error) => addFatalError(core.fatalErrors, error)
)
);
// Propagate current app state to url, then start syncing and fetching
replaceUrlAppState().then(() => {
startStateSync();
if (shouldSearchOnPageLoad()) {
refetch$.next();
}
data.search.session.clear();
});
}

View file

@ -1,31 +1,11 @@
<discover-app>
<discover
fetch="fetch"
fetch-counter="fetchCounter"
fetch-error="fetchError"
field-counts="fieldCounts"
histogram-data="histogramData"
hits="hits"
angularRoute="angularRoute"
index-pattern="indexPattern"
on-add-column="addColumn"
on-add-filter="filterQuery"
on-change-interval="changeInterval"
on-remove-column="removeColumn"
on-set-columns="setColumns"
on-sort="setSortOrder"
opts="opts"
reset-query="resetQuery"
result-state="resultState"
rows="rows"
search-source="volatileSearchSource"
set-index-pattern="setIndexPattern"
show-save-query="showSaveQuery"
state="state"
time-filter-update-handler="timefilterUpdateHandler"
time-range="timeRange"
top-nav-menu="topNavMenu"
update-query="updateQuery"
update-saved-query-id="updateSavedQueryId"
>
</discover>
</discover-app>

View file

@ -1,22 +1,7 @@
<discover-app>
<discover
fetch="fetch"
fetch-counter="fetchCounter"
fetch-error="fetchError"
field-counts="fieldCounts"
histogram-data="histogramData"
hits="hits"
index-pattern="indexPattern"
opts="opts"
reset-query="resetQuery"
result-state="resultState"
fetch-status="fetchStatus"
rows="rows"
search-source="volatileSearchSource"
state="state"
top-nav-menu="topNavMenu"
use-new-fields-api="useNewFieldsApi"
unmapped-fields-config="unmappedFieldsConfig"
refresh-app-state="refreshAppState">
>
</discover>
</discover-app>
</discover-app>

View file

@ -11,7 +11,7 @@ import { configMock } from '../../../../__mocks__/config';
import { indexPatternMock } from '../../../../__mocks__/index_pattern';
import { indexPatternsMock } from '../../../../__mocks__/index_patterns';
import { Capabilities } from '../../../../../../../core/types';
import { AppState } from '../../discover_state';
import { AppState } from '../../../apps/main/services/discover_state';
function getStateColumnAction(state: {}, setAppState: (state: Partial<AppState>) => void) {
return getStateColumnActions({

View file

@ -11,7 +11,7 @@ import { IndexPattern, IndexPatternsContract } from '../../../../kibana_services
import {
AppState as DiscoverState,
GetStateReturn as DiscoverGetStateReturn,
} from '../../discover_state';
} from '../../../apps/main/services/discover_state';
import {
AppState as ContextState,
GetStateReturn as ContextGetStateReturn,

View file

@ -19,7 +19,7 @@ import { setScopedHistory, setServices, setDocViewsRegistry } from '../../../../
import { coreMock } from '../../../../../../../core/public/mocks';
import { dataPluginMock } from '../../../../../../data/public/mocks';
import { navigationPluginMock } from '../../../../../../navigation/public/mocks';
import { getInnerAngularModule } from '../../../../get_inner_angular';
import { getInnerAngularModule } from '../../get_inner_angular';
import { createBrowserHistory } from 'history';
const fakeRowVals = {

View file

@ -13,8 +13,8 @@ import type { estypes } from '@elastic/elasticsearch';
import { EuiButtonEmpty } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { getServices, IIndexPattern } from '../../../kibana_services';
import { IndexPatternField } from '../../../../../data/common/index_patterns';
import { SkipBottomButton } from '../../components/skip_bottom_button';
import { IndexPatternField } from '../../../../../data/common';
import { SkipBottomButton } from '../../apps/main/components/skip_bottom_button';
export interface DocTableLegacyProps {
columns: string[];
@ -23,7 +23,7 @@ export interface DocTableLegacyProps {
onFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
rows: estypes.Hit[];
indexPattern: IIndexPattern;
minimumVisibleRows: number;
minimumVisibleRows?: number;
onAddColumn?: (column: string) => void;
onBackToTop: () => void;
onSort?: (sort: string[][]) => void;
@ -119,11 +119,9 @@ export function DocTableLegacy(renderProps: DocTableLegacyProps) {
}, [setMinimumVisibleRows, renderProps.rows]);
useEffect(() => {
if (minimumVisibleRows > 50) {
setMinimumVisibleRows(50);
}
setMinimumVisibleRows(50);
setRows(renderProps.rows);
}, [renderProps.rows, minimumVisibleRows, setMinimumVisibleRows]);
}, [renderProps.rows, setMinimumVisibleRows]);
useEffect(() => {
if (ref && ref.current && !scope.current) {
@ -133,7 +131,7 @@ export function DocTableLegacy(renderProps: DocTableLegacyProps) {
});
} else if (scope && scope.current) {
scope.current.renderProps = { ...renderProps, rows, minimumVisibleRows };
scope.current.$apply();
scope.current.$applyAsync();
}
}, [renderProps, minimumVisibleRows, rows]);

View file

@ -18,7 +18,7 @@ import { coreMock } from '../../../../../../core/public/mocks';
import { dataPluginMock } from '../../../../../data/public/mocks';
import { navigationPluginMock } from '../../../../../navigation/public/mocks';
import { setScopedHistory, setServices } from '../../../kibana_services';
import { getInnerAngularModule } from '../../../get_inner_angular';
import { getInnerAngularModule } from '../get_inner_angular';
let $parentScope;

View file

@ -9,29 +9,29 @@
// inner angular imports
// these are necessary to bootstrap the local angular.
// They can stay even after NP cutover
import './application/index.scss';
import '../index.scss';
import angular from 'angular';
// required for `ngSanitize` angular module
import 'angular-sanitize';
import { EuiIcon } from '@elastic/eui';
import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular';
import { CoreStart, PluginInitializerContext } from 'kibana/public';
import { DataPublicPluginStart } from '../../data/public';
import { Storage } from '../../kibana_utils/public';
import { NavigationPublicPluginStart as NavigationStart } from '../../navigation/public';
import { createDocTableDirective } from './application/angular/doc_table';
import { createTableHeaderDirective } from './application/angular/doc_table/components/table_header';
import { DataPublicPluginStart } from '../../../../data/public';
import { Storage } from '../../../../kibana_utils/public';
import { NavigationPublicPluginStart as NavigationStart } from '../../../../navigation/public';
import { createDocTableDirective } from './doc_table';
import { createTableHeaderDirective } from './doc_table/components/table_header';
import {
createToolBarPagerButtonsDirective,
createToolBarPagerTextDirective,
} from './application/angular/doc_table/components/pager';
import { createContextAppLegacy } from './application/components/context_app/context_app_legacy_directive';
import { createTableRowDirective } from './application/angular/doc_table/components/table_row';
import { createPagerFactory } from './application/angular/doc_table/lib/pager/pager_factory';
import { createInfiniteScrollDirective } from './application/angular/doc_table/infinite_scroll';
import { createDocViewerDirective } from './application/angular/doc_viewer';
import { createDiscoverGridDirective } from './application/components/create_discover_grid_directive';
import { createRenderCompleteDirective } from './application/angular/directives/render_complete';
} from './doc_table/components/pager';
import { createContextAppLegacy } from '../components/context_app/context_app_legacy_directive';
import { createTableRowDirective } from './doc_table/components/table_row';
import { createPagerFactory } from './doc_table/lib/pager/pager_factory';
import { createInfiniteScrollDirective } from './doc_table/infinite_scroll';
import { createDocViewerDirective } from './doc_viewer';
import { createDiscoverGridDirective } from './create_discover_grid_directive';
import { createRenderCompleteDirective } from './directives/render_complete';
import {
initAngularBootstrap,
configureAppAngularModule,
@ -39,10 +39,10 @@ import {
PromiseServiceCreator,
registerListenEventListener,
watchMultiDecorator,
} from '../../kibana_legacy/public';
import { DiscoverStartPlugins } from './plugin';
import { getScopedHistory } from './kibana_services';
import { createDiscoverDirective } from './application/components/create_discover_directive';
} from '../../../../kibana_legacy/public';
import { DiscoverStartPlugins } from '../../plugin';
import { getScopedHistory } from '../../kibana_services';
import { createDiscoverDirective } from './create_discover_directive';
/**
* returns the main inner angular module, it contains all the parts of Angular Discover

View file

@ -6,6 +6,5 @@
* Side Public License, v 1.
*/
export { buildPointSeriesData } from './point_series';
export { formatRow, formatTopLevelObject } from './row_formatter';
export { handleSourceColumnState } from './state_helpers';

View file

@ -16,4 +16,3 @@ import './doc';
import './context';
import './doc_viewer';
import './redirect';
import './directives';

View file

@ -1,109 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getServices } from '../../kibana_services';
import { buildPointSeriesData } from './helpers';
function tableResponseHandler(table, dimensions) {
const converted = { tables: [] };
const split = dimensions.splitColumn || dimensions.splitRow;
if (split) {
converted.direction = dimensions.splitRow ? 'row' : 'column';
const splitColumnIndex = split[0].accessor;
const splitColumnFormatter = getServices().data.fieldFormats.deserialize(split[0].format);
const splitColumn = table.columns[splitColumnIndex];
const splitMap = {};
let splitIndex = 0;
table.rows.forEach((row, rowIndex) => {
const splitValue = row[splitColumn.id];
if (!splitMap.hasOwnProperty(splitValue)) {
splitMap[splitValue] = splitIndex++;
const tableGroup = {
$parent: converted,
title: `${splitColumnFormatter.convert(splitValue)}: ${splitColumn.name}`,
name: splitColumn.name,
key: splitValue,
column: splitColumnIndex,
row: rowIndex,
table,
tables: [],
};
tableGroup.tables.push({
$parent: tableGroup,
columns: table.columns,
rows: [],
});
converted.tables.push(tableGroup);
}
const tableIndex = splitMap[splitValue];
converted.tables[tableIndex].tables[0].rows.push(row);
});
} else {
converted.tables.push({
columns: table.columns,
rows: table.rows,
});
}
return converted;
}
function convertTableGroup(tableGroup, convertTable) {
const tables = tableGroup.tables;
if (!tables.length) return;
const firstChild = tables[0];
if (firstChild.columns) {
const chart = convertTable(firstChild);
// if chart is within a split, assign group title to its label
if (tableGroup.$parent) {
chart.label = tableGroup.title;
}
return chart;
}
const out = {};
let outList;
tables.forEach(function (table) {
if (!outList) {
const direction = tableGroup.direction === 'row' ? 'rows' : 'columns';
outList = out[direction] = [];
}
let output;
if ((output = convertTableGroup(table, convertTable))) {
outList.push(output);
}
});
return out;
}
export const discoverResponseHandler = (response, dimensions) => {
const tableGroup = tableResponseHandler(response, dimensions);
let converted = convertTableGroup(tableGroup, (table) => {
return buildPointSeriesData(table, dimensions);
});
if (!converted) {
// mimic a row of tables that doesn't have any tables
// https://github.com/elastic/kibana/blob/7bfb68cd24ed42b1b257682f93c50cd8d73e2520/src/kibana/components/vislib/components/zero_injection/inject_zeros.js#L32
converted = { rows: [] };
}
converted.hits = response.rows.length;
return converted;
};

View file

@ -0,0 +1,145 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useCallback } from 'react';
import moment from 'moment';
import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
import { IUiSettingsClient } from 'kibana/public';
import { i18n } from '@kbn/i18n';
import { HitsCounter } from '../hits_counter';
import { DataPublicPluginStart, IndexPattern, search } from '../../../../../../../data/public';
import { TimechartHeader } from '../timechart_header';
import { SavedSearch } from '../../../../../saved_searches';
import { AppState, GetStateReturn } from '../../services/discover_state';
import { TimechartBucketInterval } from '../timechart_header/timechart_header';
import { Chart as IChart } from './point_series';
import { DiscoverHistogram } from './histogram';
const TimechartHeaderMemoized = React.memo(TimechartHeader);
const DiscoverHistogramMemoized = React.memo(DiscoverHistogram);
export function DiscoverChart({
config,
data,
bucketInterval,
chartData,
hits,
isLegacy,
resetQuery,
savedSearch,
state,
stateContainer,
timefield,
}: {
config: IUiSettingsClient;
data: DataPublicPluginStart;
bucketInterval?: TimechartBucketInterval;
chartData?: IChart;
hits?: number;
indexPattern: IndexPattern;
isLegacy: boolean;
resetQuery: () => void;
savedSearch: SavedSearch;
state: AppState;
stateContainer: GetStateReturn;
timefield?: string;
}) {
const toggleHideChart = useCallback(() => {
stateContainer.setAppState({ hideChart: !state.hideChart });
}, [state, stateContainer]);
const onChangeInterval = useCallback(
(interval: string) => {
if (interval) {
stateContainer.setAppState({ interval });
}
},
[stateContainer]
);
const timefilterUpdateHandler = useCallback(
(ranges: { from: number; to: number }) => {
data.query.timefilter.timefilter.setTime({
from: moment(ranges.from).toISOString(),
to: moment(ranges.to).toISOString(),
mode: 'absolute',
});
},
[data]
);
return (
<EuiFlexGroup direction="column" alignItems="stretch" gutterSize="none" responsive={false}>
<EuiFlexItem grow={false} className="dscResultCount">
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem
grow={false}
className="dscResuntCount__title eui-textTruncate eui-textNoWrap"
>
<HitsCounter
hits={hits}
showResetButton={!!(savedSearch && savedSearch.id)}
onResetQuery={resetQuery}
/>
</EuiFlexItem>
{!state.hideChart && (
<EuiFlexItem className="dscResultCount__actions">
<TimechartHeaderMemoized
data={data}
dateFormat={config.get('dateFormat')}
options={search.aggs.intervalOptions}
onChangeInterval={onChangeInterval}
stateInterval={state.interval || ''}
bucketInterval={bucketInterval}
/>
</EuiFlexItem>
)}
{timefield && (
<EuiFlexItem className="dscResultCount__toggle" grow={false}>
<EuiButtonEmpty
size="xs"
iconType={!state.hideChart ? 'eyeClosed' : 'eye'}
onClick={() => {
toggleHideChart();
}}
data-test-subj="discoverChartToggle"
>
{!state.hideChart
? i18n.translate('discover.hideChart', {
defaultMessage: 'Hide chart',
})
: i18n.translate('discover.showChart', {
defaultMessage: 'Show chart',
})}
</EuiButtonEmpty>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
{!state.hideChart && chartData && (
<EuiFlexItem grow={false}>
<section
aria-label={i18n.translate('discover.histogramOfFoundDocumentsAriaLabel', {
defaultMessage: 'Histogram of found documents',
})}
className="dscTimechart"
>
<div
className={isLegacy ? 'dscHistogram' : 'dscHistogramGrid'}
data-test-subj="discoverChart"
>
<DiscoverHistogramMemoized
chartData={chartData}
timefilterUpdateHandler={timefilterUpdateHandler}
/>
</div>
</section>
<EuiSpacer size="s" />
</EuiFlexItem>
)}
</EuiFlexGroup>
);
}

View file

@ -27,14 +27,14 @@ import {
import { IUiSettingsClient } from 'kibana/public';
import { EuiChartThemeType } from '@elastic/eui/dist/eui_charts_theme';
import { Subscription, combineLatest } from 'rxjs';
import { getServices } from '../../../kibana_services';
import { Chart as IChart } from '../helpers/point_series';
import { getServices } from '../../../../../kibana_services';
import { Chart as IChart } from './point_series';
import {
CurrentTime,
Endzones,
getAdjustedInterval,
renderEndzoneTooltip,
} from '../../../../../charts/public';
} from '../../../../../../../charts/public';
export interface DiscoverHistogramProps {
chartData: IChart;

View file

@ -6,4 +6,4 @@
* Side Public License, v 1.
*/
export { createDebounceProviderTimeout } from './debounce';
export { DiscoverChart } from './discover_chart';

View file

@ -9,8 +9,7 @@
import { uniq } from 'lodash';
import { Duration, Moment } from 'moment';
import { Unit } from '@elastic/datemath';
import { SerializedFieldFormat } from '../../../../../expressions/common/types';
import { SerializedFieldFormat } from '../../../../../../../expressions/common';
export interface Column {
id: string;
@ -26,20 +25,23 @@ export interface Table {
rows: Row[];
}
export interface HistogramParamsBounds {
min: Moment;
max: Moment;
}
interface HistogramParams {
date: true;
interval: Duration;
intervalESValue: number;
intervalESUnit: Unit;
format: string;
bounds: {
min: Moment;
max: Moment;
};
bounds: HistogramParamsBounds;
}
export interface Dimension {
accessor: 0 | 1;
format: SerializedFieldFormat<{ pattern: string }>;
label: string;
}
export interface Dimensions {
@ -68,7 +70,7 @@ export interface Chart {
ordered: Ordered;
}
export const buildPointSeriesData = (table: Table, dimensions: Dimensions) => {
export const buildPointSeriesData = (table: Table, dimensions: Dimensions): Chart => {
const { x, y } = dimensions;
const xAccessor = table.columns[x.accessor].id;
const yAccessor = table.columns[y.accessor].id;

View file

@ -12,13 +12,13 @@ import React from 'react';
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { formatNumWithCommas } from '../../helpers';
import { formatNumWithCommas } from '../../../../helpers';
export interface HitsCounterProps {
/**
* the number of query hits
*/
hits: number;
hits?: number;
/**
* displays the reset button
*/
@ -30,6 +30,9 @@ export interface HitsCounterProps {
}
export function HitsCounter({ hits, showResetButton, onResetQuery }: HitsCounterProps) {
if (typeof hits === 'undefined') {
return null;
}
return (
<I18nProvider>
<EuiFlexGroup

View file

@ -1,4 +1,4 @@
@import '../../../../../core/public/mixins';
@import 'src/core/public/mixins';
discover-app {
flex-grow: 1;

View file

@ -0,0 +1,76 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { Subject, BehaviorSubject } from 'rxjs';
import { mountWithIntl } from '@kbn/test/jest';
import { setHeaderActionMenuMounter } from '../../../../../kibana_services';
import { DiscoverLayout } from './discover_layout';
import { esHits } from '../../../../../__mocks__/es_hits';
import { indexPatternMock } from '../../../../../__mocks__/index_pattern';
import { savedSearchMock } from '../../../../../__mocks__/saved_search';
import { createSearchSourceMock } from '../../../../../../../data/common/search/search_source/mocks';
import { IndexPattern, IndexPatternAttributes } from '../../../../../../../data/common';
import { SavedObject } from '../../../../../../../../core/types';
import { indexPatternWithTimefieldMock } from '../../../../../__mocks__/index_pattern_with_timefield';
import { DiscoverSearchSessionManager } from '../../services/discover_search_session';
import { GetStateReturn } from '../../services/discover_state';
import { DiscoverLayoutProps } from './types';
import { SavedSearchDataSubject } from '../../services/use_saved_search';
import { discoverServiceMock } from '../../../../../__mocks__/services';
import { FetchStatus } from '../../../../types';
setHeaderActionMenuMounter(jest.fn());
function getProps(indexPattern: IndexPattern): DiscoverLayoutProps {
const searchSourceMock = createSearchSourceMock({});
const services = discoverServiceMock;
services.data.query.timefilter.timefilter.getTime = () => {
return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' };
};
const indexPatternList = ([indexPattern].map((ip) => {
return { ...ip, ...{ attributes: { title: ip.title } } };
}) as unknown) as Array<SavedObject<IndexPatternAttributes>>;
const savedSearch$ = new BehaviorSubject({
state: FetchStatus.COMPLETE,
rows: esHits,
fetchCounter: 1,
fieldCounts: {},
hits: Number(esHits.length),
}) as SavedSearchDataSubject;
return {
indexPattern,
indexPatternList,
navigateTo: jest.fn(),
resetQuery: jest.fn(),
savedSearch: savedSearchMock,
savedSearchData$: savedSearch$,
savedSearchRefetch$: new Subject(),
searchSessionManager: {} as DiscoverSearchSessionManager,
searchSource: searchSourceMock,
services,
state: { columns: [] },
stateContainer: {} as GetStateReturn,
};
}
describe('Discover component', () => {
test('selected index pattern without time field displays no chart toggle', () => {
const component = mountWithIntl(<DiscoverLayout {...getProps(indexPatternMock)} />);
expect(component.find('[data-test-subj="discoverChartToggle"]').exists()).toBeFalsy();
});
test('selected index pattern with time field displays chart toggle', () => {
const component = mountWithIntl(
<DiscoverLayout {...getProps(indexPatternWithTimefieldMock)} />
);
expect(component.find('[data-test-subj="discoverChartToggle"]').exists()).toBeTruthy();
});
});

View file

@ -5,10 +5,10 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import './discover.scss';
import './discover_layout.scss';
import React, { useState, useRef, useMemo, useCallback, useEffect } from 'react';
import {
EuiButtonEmpty,
EuiSpacer,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
@ -17,99 +17,141 @@ import {
EuiPage,
EuiPageBody,
EuiPageContent,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import { METRIC_TYPE } from '@kbn/analytics';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import classNames from 'classnames';
import { HitsCounter } from './hits_counter';
import { TimechartHeader } from './timechart_header';
import { DiscoverHistogram, DiscoverUninitialized } from '../angular/directives';
import { DiscoverNoResults } from './no_results';
import { LoadingSpinner } from './loading_spinner/loading_spinner';
import { DocTableLegacy } from '../angular/doc_table/create_doc_table_react';
import { esFilters, IndexPatternField, search } from '../../../../data/public';
import { DiscoverSidebarResponsive } from './sidebar';
import { DiscoverProps } from './types';
import { SortPairArr } from '../angular/doc_table/lib/get_sort';
import { DiscoverNoResults } from '../no_results';
import { LoadingSpinner } from '../loading_spinner/loading_spinner';
import { DocTableLegacy } from '../../../../angular/doc_table/create_doc_table_react';
import {
esFilters,
IndexPatternField,
indexPatterns as indexPatternsUtils,
} from '../../../../../../../data/public';
import { DiscoverSidebarResponsive } from '../sidebar';
import { DiscoverLayoutProps } from './types';
import { SortPairArr } from '../../../../angular/doc_table/lib/get_sort';
import {
DOC_HIDE_TIME_COLUMN_SETTING,
DOC_TABLE_LEGACY,
MODIFY_COLUMNS_ON_SWITCH,
SAMPLE_SIZE_SETTING,
SEARCH_FIELDS_FROM_SOURCE,
} from '../../../common';
import { popularizeField } from '../helpers/popularize_field';
import { DocViewFilterFn } from '../doc_views/doc_views_types';
import { DiscoverGrid } from './discover_grid/discover_grid';
import { DiscoverTopNav } from './discover_topnav';
import { ElasticSearchHit } from '../doc_views/doc_views_types';
import { setBreadcrumbsTitle } from '../helpers/breadcrumbs';
import { addHelpMenuToAppChrome } from './help_menu/help_menu_util';
import { InspectorSession } from '../../../../inspector/public';
import { useDataGridColumns } from '../helpers/use_data_grid_columns';
SORT_DEFAULT_ORDER_SETTING,
} from '../../../../../../common';
import { popularizeField } from '../../../../helpers/popularize_field';
import { DocViewFilterFn } from '../../../../doc_views/doc_views_types';
import { DiscoverGrid } from '../../../../components/discover_grid/discover_grid';
import { DiscoverTopNav } from '../top_nav/discover_topnav';
import { ElasticSearchHit } from '../../../../doc_views/doc_views_types';
import { DiscoverChart } from '../chart';
import { getResultState } from '../../utils/get_result_state';
import { InspectorSession } from '../../../../../../../inspector/public';
import { DiscoverUninitialized } from '../uninitialized/uninitialized';
import { SavedSearchDataMessage } from '../../services/use_saved_search';
import { useDataGridColumns } from '../../../../helpers/use_data_grid_columns';
import { getSwitchIndexPatternAppState } from '../../utils/get_switch_index_pattern_app_state';
import { FetchStatus } from '../../../../types';
const DocTableLegacyMemoized = React.memo(DocTableLegacy);
const SidebarMemoized = React.memo(DiscoverSidebarResponsive);
const DataGridMemoized = React.memo(DiscoverGrid);
const TopNavMemoized = React.memo(DiscoverTopNav);
const TimechartHeaderMemoized = React.memo(TimechartHeader);
const DiscoverHistogramMemoized = React.memo(DiscoverHistogram);
const DiscoverChartMemoized = React.memo(DiscoverChart);
export function Discover({
fetch,
fetchCounter,
fetchError,
fieldCounts,
fetchStatus,
histogramData,
hits,
interface DiscoverLayoutFetchState extends SavedSearchDataMessage {
state: FetchStatus;
fetchCounter: number;
fieldCounts: Record<string, number>;
rows: ElasticSearchHit[];
}
export function DiscoverLayout({
indexPattern,
minimumVisibleRows,
opts,
indexPatternList,
navigateTo,
savedSearchRefetch$,
resetQuery,
resultState,
rows,
savedSearchData$,
savedSearch,
searchSessionManager,
searchSource,
services,
state,
unmappedFieldsConfig,
refreshAppState,
}: DiscoverProps) {
stateContainer,
}: DiscoverLayoutProps) {
const {
trackUiMetric,
capabilities,
indexPatterns,
data,
uiSettings: config,
filterManager,
} = services;
const sampleSize = useMemo(() => config.get(SAMPLE_SIZE_SETTING), [config]);
const [expandedDoc, setExpandedDoc] = useState<ElasticSearchHit | undefined>(undefined);
const [inspectorSession, setInspectorSession] = useState<InspectorSession | undefined>(undefined);
const scrollableDesktop = useRef<HTMLDivElement>(null);
const collapseIcon = useRef<HTMLButtonElement>(null);
const [fetchState, setFetchState] = useState<DiscoverLayoutFetchState>({
state: savedSearchData$.getValue().state,
fetchCounter: 0,
fieldCounts: {},
rows: [],
});
const { state: fetchStatus, fetchCounter, inspectorAdapters, rows } = fetchState;
useEffect(() => {
const subscription = savedSearchData$.subscribe((next) => {
if (
(next.state && next.state !== fetchState.state) ||
(next.fetchCounter && next.fetchCounter !== fetchState.fetchCounter) ||
(next.rows && next.rows !== fetchState.rows) ||
(next.chartData && next.chartData !== fetchState.chartData)
) {
setFetchState({ ...fetchState, ...next });
}
});
return () => {
subscription.unsubscribe();
};
}, [savedSearchData$, fetchState]);
const isMobile = () => {
// collapse icon isn't displayed in mobile view, use it to detect which view is displayed
return collapseIcon && !collapseIcon.current;
};
const toggleHideChart = useCallback(() => {
const newState = { ...state, hideChart: !state.hideChart };
opts.stateContainer.setAppState(newState);
}, [state, opts]);
const hideChart = useMemo(() => state.hideChart, [state]);
const { savedSearch, indexPatternList, config, services, data, setAppState } = opts;
const { trackUiMetric, capabilities, indexPatterns, chrome, docLinks } = services;
const timeField = useMemo(() => {
return indexPatternsUtils.isDefault(indexPattern) ? indexPattern.timeFieldName : undefined;
}, [indexPattern]);
const [isSidebarClosed, setIsSidebarClosed] = useState(false);
const bucketInterval = useMemo(() => {
const bucketAggConfig = opts.chartAggConfigs?.aggs[1];
return bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig)
? bucketAggConfig.buckets?.getInterval()
: undefined;
}, [opts.chartAggConfigs]);
const isLegacy = useMemo(() => services.uiSettings.get(DOC_TABLE_LEGACY), [services]);
const useNewFieldsApi = useMemo(() => !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [
services,
]);
const unmappedFieldsConfig = useMemo(
() => ({
showUnmappedFields: useNewFieldsApi,
}),
[useNewFieldsApi]
);
const resultState = useMemo(() => getResultState(fetchStatus, rows!), [fetchStatus, rows]);
const contentCentered = resultState === 'uninitialized';
const isLegacy = services.uiSettings.get(DOC_TABLE_LEGACY);
const useNewFieldsApi = !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE);
const updateQuery = useCallback(
(_payload, isUpdate?: boolean) => {
if (isUpdate === false) {
opts.searchSessionManager.removeSearchSessionIdFromURL({ replace: false });
opts.refetch$.next();
searchSessionManager.removeSearchSessionIdFromURL({ replace: false });
savedSearchRefetch$.next();
}
},
[opts]
[savedSearchRefetch$, searchSessionManager]
);
const { columns, onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useDataGridColumns({
@ -117,27 +159,21 @@ export function Discover({
config,
indexPattern,
indexPatterns,
setAppState,
setAppState: stateContainer.setAppState,
state,
useNewFieldsApi,
});
useEffect(() => {
const pageTitleSuffix = savedSearch.id && savedSearch.title ? `: ${savedSearch.title}` : '';
chrome.docTitle.change(`Discover${pageTitleSuffix}`);
setBreadcrumbsTitle(savedSearch, chrome);
addHelpMenuToAppChrome(chrome, docLinks);
}, [savedSearch, chrome, docLinks]);
const onOpenInspector = useCallback(() => {
// prevent overlapping
setExpandedDoc(undefined);
const session = services.inspector.open(opts.inspectorAdapters, {
title: savedSearch.title,
});
setInspectorSession(session);
}, [setExpandedDoc, opts.inspectorAdapters, savedSearch, services.inspector]);
if (inspectorAdapters) {
setExpandedDoc(undefined);
const session = services.inspector.open(inspectorAdapters, {
title: savedSearch.title,
});
setInspectorSession(session);
}
}, [setExpandedDoc, inspectorAdapters, savedSearch, services.inspector]);
useEffect(() => {
return () => {
@ -150,9 +186,9 @@ export function Discover({
const onSort = useCallback(
(sort: string[][]) => {
setAppState({ sort });
stateContainer.setAppState({ sort });
},
[setAppState]
[stateContainer]
);
const onAddFilter = useCallback(
@ -160,7 +196,7 @@ export function Discover({
const fieldName = typeof field === 'string' ? field : field.name;
popularizeField(indexPattern, fieldName, indexPatterns);
const newFilters = esFilters.generateFilters(
opts.filterManager,
filterManager,
field,
values,
operation,
@ -169,31 +205,13 @@ export function Discover({
if (trackUiMetric) {
trackUiMetric(METRIC_TYPE.CLICK, 'filter_added');
}
return opts.filterManager.addFilters(newFilters);
return filterManager.addFilters(newFilters);
},
[opts, indexPattern, indexPatterns, trackUiMetric]
[filterManager, indexPattern, indexPatterns, trackUiMetric]
);
const onChangeInterval = useCallback(
(interval: string) => {
if (interval) {
setAppState({ interval });
}
},
[setAppState]
);
const timefilterUpdateHandler = useCallback(
(ranges: { from: number; to: number }) => {
data.query.timefilter.timefilter.setTime({
from: moment(ranges.from).toISOString(),
to: moment(ranges.to).toISOString(),
mode: 'absolute',
});
},
[data]
);
/**
* Legacy function, remove once legacy grid is removed
*/
const onBackToTop = useCallback(() => {
if (scrollableDesktop && scrollableDesktop.current) {
scrollableDesktop.current.focus();
@ -214,28 +232,69 @@ export function Discover({
width: colSettings.width,
};
const newGrid = { ...grid, columns: newColumns };
opts.setAppState({ grid: newGrid });
stateContainer.setAppState({ grid: newGrid });
},
[opts, state]
[stateContainer, state]
);
const onEditRuntimeField = () => {
if (refreshAppState) {
refreshAppState();
}
};
const onEditRuntimeField = useCallback(() => {
savedSearchRefetch$.next('reset');
}, [savedSearchRefetch$]);
const contentCentered = resultState === 'uninitialized';
const showTimeCol = useMemo(
() => !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName,
[config, indexPattern.timeFieldName]
);
const onChangeIndexPattern = useCallback(
async (id: string) => {
const nextIndexPattern = await indexPatterns.get(id);
if (nextIndexPattern && indexPattern) {
/**
* Without resetting the fetch state, e.g. a time column would be displayed when switching
* from a index pattern without to a index pattern with time filter for a brief moment
* That's because appState is updated before savedSearchData$
* The following line of code catches this, but should be improved
*/
savedSearchData$.next({ rows: [], state: FetchStatus.LOADING, fieldCounts: {} });
const nextAppState = getSwitchIndexPatternAppState(
indexPattern,
nextIndexPattern,
state.columns || [],
(state.sort || []) as SortPairArr[],
config.get(MODIFY_COLUMNS_ON_SWITCH),
config.get(SORT_DEFAULT_ORDER_SETTING)
);
stateContainer.setAppState(nextAppState);
}
},
[
config,
indexPattern,
indexPatterns,
savedSearchData$,
state.columns,
state.sort,
stateContainer,
]
);
return (
<I18nProvider>
<EuiPage className="dscPage" data-fetch-counter={fetchCounter}>
<TopNavMemoized
indexPattern={indexPattern}
opts={opts}
onOpenInspector={onOpenInspector}
query={state.query}
navigateTo={navigateTo}
savedQuery={state.savedQuery}
updateQuery={updateQuery}
savedSearch={savedSearch}
searchSource={searchSource}
services={services}
stateContainer={stateContainer}
updateQuery={updateQuery}
/>
<EuiPageBody className="dscPageBody" aria-describedby="savedSearchTitle">
<h1 id="savedSearchTitle" className="euiScreenReaderOnly">
@ -244,18 +303,16 @@ export function Discover({
<EuiFlexGroup className="dscPageBody__contents" gutterSize="none">
<EuiFlexItem grow={false}>
<SidebarMemoized
config={config}
columns={columns}
fieldCounts={fieldCounts}
fieldCounts={fetchState.fieldCounts}
hits={rows}
indexPatternList={indexPatternList}
indexPatterns={indexPatterns}
onAddField={onAddColumn}
onAddFilter={onAddFilter}
onRemoveField={onRemoveColumn}
selectedIndexPattern={searchSource && searchSource.getField('index')}
onChangeIndexPattern={onChangeIndexPattern}
selectedIndexPattern={indexPattern}
services={services}
setAppState={setAppState}
state={state}
isClosed={isSidebarClosed}
trackUiMetric={trackUiMetric}
@ -295,13 +352,15 @@ export function Discover({
>
{resultState === 'none' && (
<DiscoverNoResults
timeFieldName={opts.timefield}
queryLanguage={state.query?.language || ''}
data={opts.data}
error={fetchError}
timeFieldName={timeField}
queryLanguage={state.query?.language ?? ''}
data={data}
error={fetchState.fetchError}
/>
)}
{resultState === 'uninitialized' && <DiscoverUninitialized onRefresh={fetch} />}
{resultState === 'uninitialized' && (
<DiscoverUninitialized onRefresh={() => savedSearchRefetch$.next()} />
)}
{resultState === 'loading' && <LoadingSpinner />}
{resultState === 'ready' && (
<EuiFlexGroup
@ -311,79 +370,22 @@ export function Discover({
gutterSize="none"
responsive={false}
>
<EuiFlexItem grow={false} className="dscResultCount">
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem
grow={false}
className="dscResuntCount__title eui-textTruncate eui-textNoWrap"
>
<HitsCounter
hits={hits > 0 ? hits : 0}
showResetButton={!!(savedSearch && savedSearch.id)}
onResetQuery={resetQuery}
/>
</EuiFlexItem>
{!hideChart && (
<EuiFlexItem className="dscResultCount__actions">
<TimechartHeaderMemoized
data={opts.data}
dateFormat={opts.config.get('dateFormat')}
options={search.aggs.intervalOptions}
onChangeInterval={onChangeInterval}
stateInterval={state.interval || ''}
bucketInterval={bucketInterval}
/>
</EuiFlexItem>
)}
{opts.timefield && (
<EuiFlexItem className="dscResultCount__toggle" grow={false}>
<EuiButtonEmpty
size="xs"
iconType={!hideChart ? 'eyeClosed' : 'eye'}
onClick={() => {
toggleHideChart();
}}
data-test-subj="discoverChartToggle"
>
{!hideChart
? i18n.translate('discover.hideChart', {
defaultMessage: 'Hide chart',
})
: i18n.translate('discover.showChart', {
defaultMessage: 'Show chart',
})}
</EuiButtonEmpty>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiFlexItem grow={false}>
<DiscoverChartMemoized
config={config}
chartData={fetchState.chartData}
bucketInterval={fetchState.bucketInterval}
data={data}
hits={fetchState.hits}
indexPattern={indexPattern}
isLegacy={isLegacy}
state={state}
resetQuery={resetQuery}
savedSearch={savedSearch}
stateContainer={stateContainer}
timefield={timeField}
/>
</EuiFlexItem>
{!hideChart && opts.timefield && (
<EuiFlexItem grow={false}>
<section
aria-label={i18n.translate(
'discover.histogramOfFoundDocumentsAriaLabel',
{
defaultMessage: 'Histogram of found documents',
}
)}
className="dscTimechart"
>
{opts.chartAggConfigs && histogramData && rows.length !== 0 && (
<div
className={isLegacy ? 'dscHistogram' : 'dscHistogramGrid'}
data-test-subj="discoverChart"
>
<DiscoverHistogramMemoized
chartData={histogramData}
timefilterUpdateHandler={timefilterUpdateHandler}
/>
</div>
)}
</section>
<EuiSpacer size="s" />
</EuiFlexItem>
)}
<EuiHorizontalRule margin="none" />
<EuiFlexItem className="eui-yScroll">
@ -403,18 +405,17 @@ export function Discover({
<DocTableLegacyMemoized
columns={columns}
indexPattern={indexPattern}
minimumVisibleRows={minimumVisibleRows}
rows={rows}
sort={state.sort || []}
searchDescription={opts.savedSearch.description}
searchTitle={opts.savedSearch.lastSavedTitle}
searchDescription={savedSearch.description}
searchTitle={savedSearch.lastSavedTitle}
onAddColumn={onAddColumn}
onBackToTop={onBackToTop}
onFilter={onAddFilter}
onMoveColumn={onMoveColumn}
onRemoveColumn={onRemoveColumn}
onSort={onSort}
sampleSize={opts.sampleSize}
sampleSize={sampleSize}
useNewFieldsApi={useNewFieldsApi}
/>
)}
@ -428,18 +429,15 @@ export function Discover({
isLoading={fetchStatus === 'loading'}
rows={rows}
sort={(state.sort as SortPairArr[]) || []}
sampleSize={opts.sampleSize}
searchDescription={opts.savedSearch.description}
searchTitle={opts.savedSearch.lastSavedTitle}
sampleSize={sampleSize}
searchDescription={savedSearch.description}
searchTitle={savedSearch.lastSavedTitle}
setExpandedDoc={setExpandedDoc}
showTimeCol={
!config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) &&
!!indexPattern.timeFieldName
}
showTimeCol={showTimeCol}
services={services}
settings={state.grid}
onFilter={onAddFilter as DocViewFilterFn}
onAddColumn={onAddColumn}
onFilter={onAddFilter as DocViewFilterFn}
onRemoveColumn={onRemoveColumn}
onSetColumns={onSetColumns}
onSort={onSort}

View file

@ -6,5 +6,4 @@
* Side Public License, v 1.
*/
export { DiscoverUninitialized } from './uninitialized';
export { DiscoverHistogram } from './histogram';
export { DiscoverLayout } from './discover_layout';

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
IndexPattern,
IndexPatternAttributes,
SavedObject,
} from '../../../../../../../data/common';
import { ISearchSource } from '../../../../../../../data/public';
import { DiscoverSearchSessionManager } from '../../services/discover_search_session';
import { AppState, GetStateReturn } from '../../services/discover_state';
import { SavedSearchRefetchSubject, SavedSearchDataSubject } from '../../services/use_saved_search';
import { DiscoverServices } from '../../../../../build_services';
import { SavedSearch } from '../../../../../saved_searches';
export interface DiscoverLayoutProps {
indexPattern: IndexPattern;
indexPatternList: Array<SavedObject<IndexPatternAttributes>>;
resetQuery: () => void;
navigateTo: (url: string) => void;
savedSearch: SavedSearch;
savedSearchData$: SavedSearchDataSubject;
savedSearchRefetch$: SavedSearchRefetchSubject;
searchSessionManager: DiscoverSearchSessionManager;
searchSource: ISearchSource;
services: DiscoverServices;
state: AppState;
stateContainer: GetStateReturn;
}

View file

@ -12,7 +12,7 @@ import { findTestSubject } from '@elastic/eui/lib/test';
import { DiscoverNoResults, DiscoverNoResultsProps } from './no_results';
jest.mock('../../../kibana_services', () => {
jest.mock('../../../../../kibana_services', () => {
return {
getServices: () => ({
docLinks: {

View file

@ -9,8 +9,8 @@
import React, { Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { getServices } from '../../../kibana_services';
import { DataPublicPluginStart } from '../../../../../data/public';
import { getServices } from '../../../../../kibana_services';
import { DataPublicPluginStart } from '../../../../../../../data/public';
import { getLuceneQueryMessage, getTimeFieldMessage } from './no_results_helper';
import './_no_results.scss';

View file

@ -8,15 +8,16 @@
import React from 'react';
import { findTestSubject } from '@elastic/eui/lib/test';
// @ts-expect-error
import stubbedLogstashFields from '../../../__fixtures__/logstash_fields';
import { mountWithIntl } from '@kbn/test/jest';
import { DiscoverField } from './discover_field';
import { coreMock } from '../../../../../../core/public/mocks';
import { IndexPatternField } from '../../../../../data/public';
import { getStubIndexPattern } from '../../../../../data/public/test_utils';
jest.mock('../../../kibana_services', () => ({
// @ts-expect-error
import stubbedLogstashFields from '../../../../../__fixtures__/logstash_fields';
import { DiscoverField } from './discover_field';
import { coreMock } from '../../../../../../../../core/public/mocks';
import { IndexPatternField } from '../../../../../../../data/public';
import { getStubIndexPattern } from '../../../../../../../data/public/test_utils';
jest.mock('../../../../../kibana_services', () => ({
getServices: () => ({
history: () => ({
location: {

View file

@ -23,9 +23,9 @@ import { i18n } from '@kbn/i18n';
import { UiCounterMetricType } from '@kbn/analytics';
import classNames from 'classnames';
import { DiscoverFieldDetails } from './discover_field_details';
import { FieldIcon, FieldButton } from '../../../../../kibana_react/public';
import { FieldIcon, FieldButton } from '../../../../../../../kibana_react/public';
import { FieldDetails } from './types';
import { IndexPatternField, IndexPattern } from '../../../../../data/public';
import { IndexPatternField, IndexPattern } from '../../../../../../../data/public';
import { getFieldTypeName } from './lib/get_field_type_name';
import { DiscoverFieldDetailsFooter } from './discover_field_details_footer';

View file

@ -11,7 +11,7 @@ import { EuiText, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@e
import { i18n } from '@kbn/i18n';
import { StringFieldProgressBar } from './string_progress_bar';
import { Bucket } from './types';
import { IndexPatternField } from '../../../../../data/public';
import { IndexPatternField } from '../../../../../../../data/public';
import './discover_field_bucket.scss';
interface Props {

View file

@ -8,13 +8,13 @@
import React from 'react';
import { findTestSubject } from '@elastic/eui/lib/test';
// @ts-expect-error
import stubbedLogstashFields from '../../../__fixtures__/logstash_fields';
import { mountWithIntl } from '@kbn/test/jest';
// @ts-expect-error
import stubbedLogstashFields from '../../../../../__fixtures__/logstash_fields';
import { DiscoverFieldDetails } from './discover_field_details';
import { coreMock } from '../../../../../../core/public/mocks';
import { IndexPatternField } from '../../../../../data/public';
import { getStubIndexPattern } from '../../../../../data/public/test_utils';
import { coreMock } from '../../../../../../../../core/public/mocks';
import { IndexPatternField } from '../../../../../../../data/public';
import { getStubIndexPattern } from '../../../../../../../data/public/test_utils';
const indexPattern = getStubIndexPattern(
'logstash-*',

View file

@ -18,7 +18,7 @@ import {
getVisualizeHref,
} from './lib/visualize_trigger_utils';
import { Bucket, FieldDetails } from './types';
import { IndexPatternField, IndexPattern } from '../../../../../data/public';
import { IndexPatternField, IndexPattern } from '../../../../../../../data/public';
import './discover_field_details.scss';
import { DiscoverFieldDetailsFooter } from './discover_field_details_footer';

View file

@ -8,12 +8,12 @@
import React from 'react';
import { findTestSubject } from '@elastic/eui/lib/test';
// @ts-expect-error
import stubbedLogstashFields from '../../../__fixtures__/logstash_fields';
import { mountWithIntl } from '@kbn/test/jest';
import { coreMock } from '../../../../../../core/public/mocks';
import { IndexPatternField } from '../../../../../data/public';
import { getStubIndexPattern } from '../../../../../data/public/test_utils';
// @ts-expect-error
import stubbedLogstashFields from '../../../../../__fixtures__/logstash_fields';
import { coreMock } from '../../../../../../../../core/public/mocks';
import { IndexPatternField } from '../../../../../../../data/public';
import { getStubIndexPattern } from '../../../../../../../data/public/test_utils';
import { DiscoverFieldDetailsFooter } from './discover_field_details_footer';
const indexPattern = getStubIndexPattern(

View file

@ -9,8 +9,8 @@
import React from 'react';
import { EuiLink, EuiPopoverFooter, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { IndexPatternField } from '../../../../../data/common/index_patterns/fields';
import { IndexPattern } from '../../../../../data/common/index_patterns/index_patterns';
import { IndexPatternField } from '../../../../../../../data/common/index_patterns/fields';
import { IndexPattern } from '../../../../../../../data/common/index_patterns/index_patterns';
import { FieldDetails } from './types';
interface DiscoverFieldDetailsFooterProps {

View file

@ -15,8 +15,7 @@ import { SavedObject } from 'kibana/server';
import { DiscoverIndexPattern, DiscoverIndexPatternProps } from './discover_index_pattern';
import { EuiSelectable } from '@elastic/eui';
import { IndexPattern, IndexPatternAttributes } from 'src/plugins/data/public';
import { configMock } from '../../../__mocks__/config';
import { indexPatternsMock } from '../../../__mocks__/index_patterns';
import { indexPatternsMock } from '../../../../../__mocks__/index_patterns';
const indexPattern = {
id: 'the-index-pattern-id-first',
@ -38,13 +37,11 @@ const indexPattern2 = {
} as SavedObject<IndexPatternAttributes>;
const defaultProps = {
config: configMock,
indexPatternList: [indexPattern1, indexPattern2],
selectedIndexPattern: indexPattern,
state: {},
setAppState: jest.fn(),
useNewFieldsApi: true,
indexPatterns: indexPatternsMock,
onChangeIndexPattern: jest.fn(),
};
function getIndexPatternPickerList(instance: ShallowWrapper) {
@ -72,7 +69,7 @@ describe('DiscoverIndexPattern', () => {
const props = ({
indexPatternList: null,
selectedIndexPattern: null,
setIndexPattern: jest.fn(),
onChangeIndexPattern: jest.fn(),
} as unknown) as DiscoverIndexPatternProps;
expect(shallow(<DiscoverIndexPattern {...props} />)).toMatchSnapshot(`""`);
@ -92,10 +89,6 @@ describe('DiscoverIndexPattern', () => {
await act(async () => {
selectIndexPatternPickerOption(instance, 'test2 title');
});
expect(defaultProps.setAppState).toHaveBeenCalledWith({
index: 'the-index-pattern-id',
columns: [],
sort: [],
});
expect(defaultProps.onChangeIndexPattern).toHaveBeenCalledWith('the-index-pattern-id');
});
});

View file

@ -6,58 +6,35 @@
* Side Public License, v 1.
*/
import React, { useState, useEffect, useCallback } from 'react';
import { IUiSettingsClient, SavedObject } from 'kibana/public';
import {
IndexPattern,
IndexPatternAttributes,
IndexPatternsContract,
} from 'src/plugins/data/public';
import React, { useState, useEffect } from 'react';
import { I18nProvider } from '@kbn/i18n/react';
import { SavedObject } from 'kibana/public';
import { IndexPattern, IndexPatternAttributes } from 'src/plugins/data/public';
import { IndexPatternRef } from './types';
import { ChangeIndexPattern } from './change_indexpattern';
import { getSwitchIndexPatternAppState } from '../../helpers/get_switch_index_pattern_app_state';
import { SortPairArr } from '../../angular/doc_table/lib/get_sort';
import { MODIFY_COLUMNS_ON_SWITCH, SORT_DEFAULT_ORDER_SETTING } from '../../../../common';
import { AppState } from '../../angular/discover_state';
export interface DiscoverIndexPatternProps {
/**
* Client of uiSettings
*/
config: IUiSettingsClient;
/**
* list of available index patterns, if length > 1, component offers a "change" link
*/
indexPatternList: Array<SavedObject<IndexPatternAttributes>>;
/**
* Index patterns service
* Callback function when changing an index pattern
*/
indexPatterns: IndexPatternsContract;
onChangeIndexPattern: (id: string) => void;
/**
* currently selected index pattern, due to angular issues it's undefined at first rendering
*/
selectedIndexPattern: IndexPattern;
/**
* Function to set the current state
*/
setAppState: (state: Partial<AppState>) => void;
/**
* Discover App state
*/
state: AppState;
}
/**
* Component allows you to select an index pattern in discovers side bar
*/
export function DiscoverIndexPattern({
config,
indexPatternList,
onChangeIndexPattern,
selectedIndexPattern,
indexPatterns,
state,
setAppState,
}: DiscoverIndexPatternProps) {
const options: IndexPatternRef[] = (indexPatternList || []).map((entity) => ({
id: entity.id,
@ -65,24 +42,6 @@ export function DiscoverIndexPattern({
}));
const { id: selectedId, title: selectedTitle } = selectedIndexPattern || {};
const setIndexPattern = useCallback(
async (id: string) => {
const nextIndexPattern = await indexPatterns.get(id);
if (nextIndexPattern && selectedIndexPattern) {
const nextAppState = getSwitchIndexPatternAppState(
selectedIndexPattern,
nextIndexPattern,
state.columns || [],
(state.sort || []) as SortPairArr[],
config.get(MODIFY_COLUMNS_ON_SWITCH),
config.get(SORT_DEFAULT_ORDER_SETTING)
);
setAppState(nextAppState);
}
},
[selectedIndexPattern, state, config, indexPatterns, setAppState]
);
const [selected, setSelected] = useState({
id: selectedId,
title: selectedTitle || '',
@ -108,7 +67,7 @@ export function DiscoverIndexPattern({
onChangeIndexPattern={(id) => {
const indexPattern = options.find((pattern) => pattern.id === id);
if (indexPattern) {
setIndexPattern(id);
onChangeIndexPattern(id);
setSelected(indexPattern);
}
}}

View file

@ -6,16 +6,16 @@
* Side Public License, v 1.
*/
import { getStubIndexPattern } from '../../../../../data/public/index_patterns/index_pattern.stub';
import { coreMock } from '../../../../../../core/public/mocks';
import { DiscoverServices } from '../../../build_services';
// @ts-expect-error
import stubbedLogstashFields from '../../../__fixtures__/logstash_fields';
import { mountWithIntl } from '@kbn/test/jest';
import React from 'react';
import { DiscoverIndexPatternManagement } from './discover_index_pattern_management';
import { mountWithIntl } from '@kbn/test/jest';
import { EuiContextMenuPanel, EuiPopover, EuiContextMenuItem } from '@elastic/eui';
import { findTestSubject } from '@kbn/test/jest';
import { getStubIndexPattern } from '../../../../../../../data/public/index_patterns/index_pattern.stub';
import { coreMock } from '../../../../../../../../core/public/mocks';
import { DiscoverServices } from '../../../../../build_services';
// @ts-expect-error
import stubbedLogstashFields from '../../../../../__fixtures__/logstash_fields';
import { DiscoverIndexPatternManagement } from './discover_index_pattern_management';
const mockServices = ({
history: () => ({

View file

@ -9,8 +9,8 @@
import React, { useState } from 'react';
import { EuiButtonIcon, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DiscoverServices } from '../../../build_services';
import { IndexPattern } from '../../../../../data/common/index_patterns/index_patterns';
import { DiscoverServices } from '../../../../../build_services';
import { IndexPattern } from '../../../../../../../data/common/index_patterns/index_patterns';
export interface DiscoverIndexPatternManagementProps {
/**

View file

@ -10,54 +10,23 @@ import { each, cloneDeep } from 'lodash';
import { ReactWrapper } from 'enzyme';
import { findTestSubject } from '@elastic/eui/lib/test';
// @ts-expect-error
import realHits from '../../../__fixtures__/real_hits.js';
import realHits from '../../../../../__fixtures__/real_hits.js';
// @ts-expect-error
import stubbedLogstashFields from '../../../__fixtures__/logstash_fields';
import stubbedLogstashFields from '../../../../../__fixtures__/logstash_fields';
import { mountWithIntl } from '@kbn/test/jest';
import React from 'react';
import { DiscoverSidebarProps } from './discover_sidebar';
import { coreMock } from '../../../../../../core/public/mocks';
import { IndexPatternAttributes } from '../../../../../data/common';
import { getStubIndexPattern } from '../../../../../data/public/test_utils';
import { SavedObject } from '../../../../../../core/types';
import { coreMock } from '../../../../../../../../core/public/mocks';
import { IndexPatternAttributes } from '../../../../../../../data/common';
import { getStubIndexPattern } from '../../../../../../../data/public/test_utils';
import { SavedObject } from '../../../../../../../../core/types';
import { getDefaultFieldFilter } from './lib/field_filter';
import { DiscoverSidebar } from './discover_sidebar';
import { DiscoverServices } from '../../../build_services';
import { ElasticSearchHit } from '../../doc_views/doc_views_types';
import { configMock } from '../../../__mocks__/config';
import { indexPatternsMock } from '../../../__mocks__/index_patterns';
import { ElasticSearchHit } from '../../../../doc_views/doc_views_types';
import { discoverServiceMock as mockDiscoverServices } from '../../../../../__mocks__/services';
const mockServices = ({
history: () => ({
location: {
search: '',
},
}),
capabilities: {
visualize: {
show: true,
},
discover: {
save: false,
},
},
uiSettings: {
get: (key: string) => {
if (key === 'fields:popularLimit') {
return 5;
}
},
},
indexPatternFieldEditor: {
openEditor: jest.fn(),
userPermissions: {
editIndexPattern: jest.fn(),
},
},
} as unknown) as DiscoverServices;
jest.mock('../../../kibana_services', () => ({
getServices: () => mockServices,
jest.mock('../../../../../kibana_services', () => ({
getServices: () => mockDiscoverServices,
}));
jest.mock('./lib/get_index_pattern_field_list', () => ({
@ -92,22 +61,20 @@ function getCompProps(): DiscoverSidebarProps {
}
}
return {
config: configMock,
columns: ['extension'],
fieldCounts,
hits,
indexPatternList,
indexPatterns: indexPatternsMock,
onChangeIndexPattern: jest.fn(),
onAddFilter: jest.fn(),
onAddField: jest.fn(),
onRemoveField: jest.fn(),
selectedIndexPattern: indexPattern,
services: mockServices,
services: mockDiscoverServices,
state: {},
trackUiMetric: jest.fn(),
fieldFilter: getDefaultFieldFilter(),
setFieldFilter: jest.fn(),
setAppState: jest.fn(),
onEditRuntimeField: jest.fn(),
editField: jest.fn(),
};

View file

@ -27,9 +27,9 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { DiscoverField } from './discover_field';
import { DiscoverIndexPattern } from './discover_index_pattern';
import { DiscoverFieldSearch } from './discover_field_search';
import { FIELDS_LIMIT_SETTING } from '../../../../common';
import { FIELDS_LIMIT_SETTING } from '../../../../../../common';
import { groupFields } from './lib/group_fields';
import { IndexPatternField } from '../../../../../data/public';
import { IndexPatternField } from '../../../../../../../data/public';
import { getDetails } from './lib/get_details';
import { FieldFilterState, getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter';
import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list';
@ -68,30 +68,28 @@ export interface DiscoverSidebarProps extends DiscoverSidebarResponsiveProps {
export function DiscoverSidebar({
alwaysShowActionButtons = false,
columns,
config,
fieldCounts,
fieldFilter,
hits,
indexPatternList,
indexPatterns,
onAddField,
onAddFilter,
onRemoveField,
selectedIndexPattern,
services,
setAppState,
setFieldFilter,
state,
trackUiMetric,
useNewFieldsApi = false,
useFlyout = false,
unmappedFieldsConfig,
onEditRuntimeField,
onChangeIndexPattern,
setFieldEditorRef,
closeFlyout,
editField,
}: DiscoverSidebarProps) {
const [fields, setFields] = useState<IndexPatternField[] | null>(null);
const { indexPatternFieldEditor } = services;
const indexPatternFieldEditPermission = indexPatternFieldEditor?.userPermissions.editIndexPattern();
const canEditIndexPatternField = !!indexPatternFieldEditPermission && useNewFieldsApi;
@ -282,12 +280,9 @@ export function DiscoverSidebar({
<EuiFlexGroup direction="row" gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem grow={true}>
<DiscoverIndexPattern
config={config}
selectedIndexPattern={selectedIndexPattern}
indexPatternList={sortBy(indexPatternList, (o) => o.attributes.title)}
indexPatterns={indexPatterns}
state={state}
setAppState={setAppState}
onChangeIndexPattern={onChangeIndexPattern}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
@ -323,12 +318,9 @@ export function DiscoverSidebar({
<EuiFlexGroup direction="row" alignItems="center" gutterSize="s">
<EuiFlexItem grow={true} className="dscSidebar__indexPatternSwitcher">
<DiscoverIndexPattern
config={config}
selectedIndexPattern={selectedIndexPattern}
indexPatternList={sortBy(indexPatternList, (o) => o.attributes.title)}
indexPatterns={indexPatterns}
state={state}
setAppState={setAppState}
onChangeIndexPattern={onChangeIndexPattern}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>

View file

@ -10,23 +10,21 @@ import { each, cloneDeep } from 'lodash';
import { ReactWrapper } from 'enzyme';
import { findTestSubject } from '@elastic/eui/lib/test';
// @ts-expect-error
import realHits from '../../../__fixtures__/real_hits.js';
import realHits from '../../../../../__fixtures__/real_hits.js';
// @ts-expect-error
import stubbedLogstashFields from '../../../__fixtures__/logstash_fields';
import stubbedLogstashFields from '../../../../../__fixtures__/logstash_fields';
import { mountWithIntl } from '@kbn/test/jest';
import React from 'react';
import { coreMock } from '../../../../../../core/public/mocks';
import { IndexPatternAttributes } from '../../../../../data/common';
import { getStubIndexPattern } from '../../../../../data/public/test_utils';
import { SavedObject } from '../../../../../../core/types';
import { coreMock } from '../../../../../../../../core/public/mocks';
import { IndexPatternAttributes } from '../../../../../../../data/common';
import { getStubIndexPattern } from '../../../../../../../data/public/test_utils';
import { SavedObject } from '../../../../../../../../core/types';
import {
DiscoverSidebarResponsive,
DiscoverSidebarResponsiveProps,
} from './discover_sidebar_responsive';
import { DiscoverServices } from '../../../build_services';
import { ElasticSearchHit } from '../../doc_views/doc_views_types';
import { configMock } from '../../../__mocks__/config';
import { indexPatternsMock } from '../../../__mocks__/index_patterns';
import { DiscoverServices } from '../../../../../build_services';
import { ElasticSearchHit } from '../../../../doc_views/doc_views_types';
import { DiscoverSidebar } from './discover_sidebar';
const mockServices = ({
@ -52,7 +50,7 @@ const mockServices = ({
},
} as unknown) as DiscoverServices;
jest.mock('../../../kibana_services', () => ({
jest.mock('../../../../../kibana_services', () => ({
getServices: () => mockServices,
}));
@ -89,17 +87,15 @@ function getCompProps(): DiscoverSidebarResponsiveProps {
}
return {
columns: ['extension'],
config: configMock,
fieldCounts,
hits,
indexPatternList,
indexPatterns: indexPatternsMock,
onChangeIndexPattern: jest.fn(),
onAddFilter: jest.fn(),
onAddField: jest.fn(),
onRemoveField: jest.fn(),
selectedIndexPattern: indexPattern,
services: mockServices,
setAppState: jest.fn(),
state: {},
trackUiMetric: jest.fn(),
onEditRuntimeField: jest.fn(),

View file

@ -11,7 +11,6 @@ import { sortBy } from 'lodash';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { UiCounterMetricType } from '@kbn/analytics';
import { IUiSettingsClient } from 'kibana/public';
import {
EuiTitle,
EuiHideFor,
@ -28,14 +27,14 @@ import {
EuiFlexItem,
} from '@elastic/eui';
import { DiscoverIndexPattern } from './discover_index_pattern';
import { IndexPatternAttributes, IndexPatternsContract } from '../../../../../data/common';
import { SavedObject } from '../../../../../../core/types';
import { IndexPatternField, IndexPattern } from '../../../../../data/public';
import { IndexPatternAttributes } from '../../../../../../../data/common';
import { SavedObject } from '../../../../../../../../core/types';
import { IndexPatternField, IndexPattern } from '../../../../../../../data/public';
import { getDefaultFieldFilter } from './lib/field_filter';
import { DiscoverSidebar } from './discover_sidebar';
import { DiscoverServices } from '../../../build_services';
import { ElasticSearchHit } from '../../doc_views/doc_views_types';
import { AppState } from '../../angular/discover_state';
import { DiscoverServices } from '../../../../../build_services';
import { ElasticSearchHit } from '../../../../doc_views/doc_views_types';
import { AppState } from '../../services/discover_state';
import { DiscoverIndexPatternManagement } from './discover_index_pattern_management';
export interface DiscoverSidebarResponsiveProps {
@ -47,10 +46,6 @@ export interface DiscoverSidebarResponsiveProps {
* the selected columns displayed in the doc table in discover
*/
columns: string[];
/**
* Client of uiSettings
*/
config: IUiSettingsClient;
/**
* a statistics of the distribution of fields in the given hits
*/
@ -63,10 +58,6 @@ export interface DiscoverSidebarResponsiveProps {
* List of available index patterns
*/
indexPatternList: Array<SavedObject<IndexPatternAttributes>>;
/**
* Index patterns service
*/
indexPatterns: IndexPatternsContract;
/**
* Has been toggled closed
*/
@ -79,6 +70,10 @@ export interface DiscoverSidebarResponsiveProps {
* Callback function when adding a filter from sidebar
*/
onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
/**
* Callback function when changing an index pattern
*/
onChangeIndexPattern: (id: string) => void;
/**
* Callback function when removing a field
* @param fieldName
@ -92,10 +87,6 @@ export interface DiscoverSidebarResponsiveProps {
* Discover plugin services;
*/
services: DiscoverServices;
/**
* Function to set the current state
*/
setAppState: (state: Partial<AppState>) => void;
/**
* Discover App state
*/
@ -114,7 +105,6 @@ export interface DiscoverSidebarResponsiveProps {
* Read from the Fields API
*/
useNewFieldsApi?: boolean;
/**
* an object containing properties for proper handling of unmapped fields
*/
@ -215,12 +205,9 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
<EuiFlexGroup direction="row" gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem grow={true}>
<DiscoverIndexPattern
config={props.config}
onChangeIndexPattern={props.onChangeIndexPattern}
selectedIndexPattern={props.selectedIndexPattern}
indexPatternList={sortBy(props.indexPatternList, (o) => o.attributes.title)}
indexPatterns={props.indexPatterns}
state={props.state}
setAppState={props.setAppState}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>

View file

@ -10,12 +10,12 @@
import _ from 'lodash';
// @ts-expect-error
import realHits from '../../../../__fixtures__/real_hits.js';
import realHits from '../../../../../../__fixtures__/real_hits.js';
// @ts-expect-error
import stubbedLogstashFields from '../../../../__fixtures__/logstash_fields';
import { coreMock } from '../../../../../../../core/public/mocks';
import { IndexPattern } from '../../../../../../data/public';
import { getStubIndexPattern } from '../../../../../../data/public/test_utils';
import stubbedLogstashFields from '../../../../../../__fixtures__/logstash_fields';
import { coreMock } from '../../../../../../../../../core/public/mocks';
import { IndexPattern } from '../../../../../../../../data/public';
import { getStubIndexPattern } from '../../../../../../../../data/public/test_utils';
// @ts-expect-error
import { fieldCalculator } from './field_calculator';

View file

@ -7,7 +7,7 @@
*/
import { getDefaultFieldFilter, setFieldFilterProp, isFieldFiltered } from './field_filter';
import { IndexPatternField } from '../../../../../../data/public';
import { IndexPatternField } from '../../../../../../../../data/public';
describe('field_filter', function () {
it('getDefaultFieldFilter should return default filter state', function () {

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { IndexPatternField } from '../../../../../../data/public';
import { IndexPatternField } from '../../../../../../../../data/public';
export interface FieldFilterState {
missing: boolean;

View file

@ -8,8 +8,8 @@
// @ts-expect-error
import { fieldCalculator } from './field_calculator';
import { IndexPattern, IndexPatternField } from '../../../../../../data/public';
import { ElasticSearchHit } from '../../../doc_views/doc_views_types';
import { IndexPattern, IndexPatternField } from '../../../../../../../../data/public';
import { ElasticSearchHit } from '../../../../../doc_views/doc_views_types';
export function getDetails(
field: IndexPatternField,

View file

@ -8,7 +8,7 @@
import { difference } from 'lodash';
import { IndexPattern, IndexPatternField } from 'src/plugins/data/public';
import { isNestedFieldParent } from '../../../helpers/nested_fields';
import { isNestedFieldParent } from '../../../utils/nested_fields';
export function getIndexPatternFieldList(
indexPattern?: IndexPattern,

View file

@ -7,7 +7,7 @@
*/
import { i18n } from '@kbn/i18n';
import { IndexPatternField } from '../../../../../../data/public';
import { IndexPatternField } from '../../../../../../../../data/public';
export function getWarnings(field: IndexPatternField) {
let warnings = [];

View file

@ -8,7 +8,7 @@
import { groupFields } from './group_fields';
import { getDefaultFieldFilter } from './field_filter';
import { IndexPatternField } from '../../../../../../data/common/index_patterns/fields';
import { IndexPatternField } from '../../../../../../../../data/common/index_patterns/fields';
const fields = [
{

View file

@ -11,9 +11,9 @@ import {
VISUALIZE_GEO_FIELD_TRIGGER,
visualizeFieldTrigger,
visualizeGeoFieldTrigger,
} from '../../../../../../ui_actions/public';
import { getUiActions } from '../../../../kibana_services';
import { IndexPatternField, KBN_FIELD_TYPES } from '../../../../../../data/public';
} from '../../../../../../../../ui_actions/public';
import { getUiActions } from '../../../../../../kibana_services';
import { IndexPatternField, KBN_FIELD_TYPES } from '../../../../../../../../data/public';
function getTriggerConstant(type: string) {
return type === KBN_FIELD_TYPES.GEO_POINT || type === KBN_FIELD_TYPES.GEO_SHAPE

View file

@ -12,7 +12,7 @@ import { ReactWrapper } from 'enzyme';
import { TimechartHeader, TimechartHeaderProps } from './timechart_header';
import { EuiIconTip } from '@elastic/eui';
import { findTestSubject } from '@elastic/eui/lib/test';
import { DataPublicPluginStart } from '../../../../../data/public';
import { DataPublicPluginStart } from '../../../../../../../data/public';
describe('timechart header', function () {
let props: TimechartHeaderProps;

View file

@ -18,8 +18,14 @@ import {
import moment from 'moment';
import { i18n } from '@kbn/i18n';
import dateMath from '@elastic/datemath';
import { DataPublicPluginStart } from '../../../../../data/public';
import './timechart_header.scss';
import { DataPublicPluginStart } from '../../../../../../../data/public';
export interface TimechartBucketInterval {
scaled?: boolean;
description?: string;
scale?: number;
}
export interface TimechartHeaderProps {
/**
@ -29,11 +35,7 @@ export interface TimechartHeaderProps {
/**
* Interval for the buckets of the recent request
*/
bucketInterval?: {
scaled?: boolean;
description?: string;
scale?: number;
};
bucketInterval?: TimechartBucketInterval;
data: DataPublicPluginStart;
/**
* Interval Options

View file

@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { shallowWithIntl } from '@kbn/test/jest';
import { indexPatternMock } from '../../../../../__mocks__/index_pattern';
import { savedSearchMock } from '../../../../../__mocks__/saved_search';
import { DiscoverTopNav, DiscoverTopNavProps } from './discover_topnav';
import { TopNavMenuData } from '../../../../../../../navigation/public';
import { ISearchSource, Query } from '../../../../../../../data/common';
import { GetStateReturn } from '../../services/discover_state';
import { setHeaderActionMenuMounter } from '../../../../../kibana_services';
import { discoverServiceMock } from '../../../../../__mocks__/services';
setHeaderActionMenuMounter(jest.fn());
function getProps(savePermissions = true): DiscoverTopNavProps {
discoverServiceMock.capabilities.discover!.save = savePermissions;
return {
stateContainer: {} as GetStateReturn,
indexPattern: indexPatternMock,
savedSearch: savedSearchMock,
navigateTo: jest.fn(),
services: discoverServiceMock,
query: {} as Query,
savedQuery: '',
updateQuery: jest.fn(),
onOpenInspector: jest.fn(),
searchSource: {} as ISearchSource,
};
}
describe('Discover topnav component', () => {
test('generated config of TopNavMenu config is correct when discover save permissions are assigned', () => {
const props = getProps(true);
const component = shallowWithIntl(<DiscoverTopNav {...props} />);
const topMenuConfig = component.props().config.map((obj: TopNavMenuData) => obj.id);
expect(topMenuConfig).toEqual(['options', 'new', 'save', 'open', 'share', 'inspect']);
});
test('generated config of TopNavMenu config is correct when no discover save permissions are assigned', () => {
const props = getProps(false);
const component = shallowWithIntl(<DiscoverTopNav {...props} />);
const topMenuConfig = component.props().config.map((obj: TopNavMenuData) => obj.id);
expect(topMenuConfig).toEqual(['options', 'new', 'open', 'share', 'inspect']);
});
});

View file

@ -6,45 +6,53 @@
* Side Public License, v 1.
*/
import React, { useMemo } from 'react';
import { DiscoverProps } from './types';
import { getTopNavLinks } from './top_nav/get_top_nav_links';
import { Query, TimeRange } from '../../../../data/common/query';
import { DiscoverLayoutProps } from '../layout/types';
import { getTopNavLinks } from './get_top_nav_links';
import { Query, TimeRange } from '../../../../../../../data/common/query';
import { getHeaderActionMenuMounter } from '../../../../../kibana_services';
import { GetStateReturn } from '../../services/discover_state';
export type DiscoverTopNavProps = Pick<DiscoverProps, 'indexPattern' | 'opts' | 'searchSource'> & {
export type DiscoverTopNavProps = Pick<
DiscoverLayoutProps,
'indexPattern' | 'navigateTo' | 'savedSearch' | 'services' | 'searchSource'
> & {
onOpenInspector: () => void;
query?: Query;
savedQuery?: string;
updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void;
stateContainer: GetStateReturn;
};
export const DiscoverTopNav = ({
indexPattern,
opts,
onOpenInspector,
query,
savedQuery,
stateContainer,
updateQuery,
searchSource,
navigateTo,
savedSearch,
services,
}: DiscoverTopNavProps) => {
const showDatePicker = useMemo(() => indexPattern.isTimeBased(), [indexPattern]);
const { TopNavMenu } = opts.services.navigation.ui;
const { TopNavMenu } = services.navigation.ui;
const topNavMenu = useMemo(
() =>
getTopNavLinks({
getFieldCounts: opts.getFieldCounts,
indexPattern,
navigateTo: opts.navigateTo,
savedSearch: opts.savedSearch,
services: opts.services,
state: opts.stateContainer,
navigateTo,
savedSearch,
services,
state: stateContainer,
onOpenInspector,
searchSource,
}),
[indexPattern, opts, onOpenInspector, searchSource]
[indexPattern, navigateTo, onOpenInspector, searchSource, stateContainer, savedSearch, services]
);
const updateSavedQueryId = (newSavedQueryId: string | undefined) => {
const { appStateContainer, setAppState } = opts.stateContainer;
const { appStateContainer, setAppState } = stateContainer;
if (newSavedQueryId) {
setAppState({ savedQuery: newSavedQueryId });
} else {
@ -56,6 +64,10 @@ export const DiscoverTopNav = ({
appStateContainer.set(newState);
}
};
const setMenuMountPoint = useMemo(() => {
return getHeaderActionMenuMounter();
}, []);
return (
<TopNavMenu
appName="discover"
@ -64,11 +76,11 @@ export const DiscoverTopNav = ({
onQuerySubmit={updateQuery}
onSavedQueryIdChange={updateSavedQueryId}
query={query}
setMenuMountPoint={opts.setHeaderActionMenu}
setMenuMountPoint={setMenuMountPoint}
savedQueryId={savedQuery}
screenTitle={opts.savedSearch.title}
screenTitle={savedSearch.title}
showDatePicker={showDatePicker}
showSaveQuery={!!opts.services.capabilities.discover.saveQuery}
showSaveQuery={!!services.capabilities.discover.saveQuery}
showSearchBar={true}
useDefaultBehaviors={true}
/>

View file

@ -8,10 +8,10 @@
import { ISearchSource } from 'src/plugins/data/public';
import { getTopNavLinks } from './get_top_nav_links';
import { indexPatternMock } from '../../../__mocks__/index_pattern';
import { savedSearchMock } from '../../../__mocks__/saved_search';
import { DiscoverServices } from '../../../build_services';
import { GetStateReturn } from '../../angular/discover_state';
import { indexPatternMock } from '../../../../../__mocks__/index_pattern';
import { savedSearchMock } from '../../../../../__mocks__/saved_search';
import { DiscoverServices } from '../../../../../build_services';
import { GetStateReturn } from '../../services/discover_state';
const services = ({
capabilities: {
@ -28,7 +28,6 @@ const state = ({} as unknown) as GetStateReturn;
test('getTopNavLinks result', () => {
const topNavLinks = getTopNavLinks({
getFieldCounts: jest.fn(),
indexPattern: indexPatternMock,
navigateTo: jest.fn(),
onOpenInspector: jest.fn(),

View file

@ -9,20 +9,19 @@
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import { showOpenSearchPanel } from './show_open_search_panel';
import { getSharingData, showPublicUrlSwitch } from '../../helpers/get_sharing_data';
import { unhashUrl } from '../../../../../kibana_utils/public';
import { DiscoverServices } from '../../../build_services';
import { SavedSearch } from '../../../saved_searches';
import { getSharingData, showPublicUrlSwitch } from '../../utils/get_sharing_data';
import { unhashUrl } from '../../../../../../../kibana_utils/public';
import { DiscoverServices } from '../../../../../build_services';
import { SavedSearch } from '../../../../../saved_searches';
import { onSaveSearch } from './on_save_search';
import { GetStateReturn } from '../../angular/discover_state';
import { IndexPattern, ISearchSource } from '../../../kibana_services';
import { GetStateReturn } from '../../services/discover_state';
import { IndexPattern, ISearchSource } from '../../../../../kibana_services';
import { openOptionsPopover } from './open_options_popover';
/**
* Helper function to build the top nav links
*/
export const getTopNavLinks = ({
getFieldCounts,
indexPattern,
navigateTo,
savedSearch,
@ -31,7 +30,6 @@ export const getTopNavLinks = ({
onOpenInspector,
searchSource,
}: {
getFieldCounts: () => Promise<Record<string, number>>;
indexPattern: IndexPattern;
navigateTo: (url: string) => void;
savedSearch: SavedSearch;

View file

@ -6,15 +6,15 @@
* Side Public License, v 1.
*/
import { showSaveModal } from '../../../../../saved_objects/public';
jest.mock('../../../../../saved_objects/public');
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';
import { indexPatternMock } from '../../../../../__mocks__/index_pattern';
import { savedSearchMock } from '../../../../../__mocks__/saved_search';
import { DiscoverServices } from '../../../../../build_services';
import { GetStateReturn } from '../../services/discover_state';
import { i18nServiceMock } from '../../../../../../../../core/public/mocks';
test('onSaveSearch', async () => {
const serviceMock = ({

View file

@ -8,13 +8,13 @@
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';
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 '../../services/discover_state';
import { setBreadcrumbsTitle } from '../../../../helpers/breadcrumbs';
import { persistSavedSearch } from '../../utils/persist_saved_search';
async function saveDataSource({
indexPattern,

Some files were not shown because too many files have changed in this diff Show more