* Refactor minimumVisibleRows * Extract setupVisualization function * Extract getDimensions function * Inline breadcrumb and help menu function exec to discover.tsx * Extract getStateDefault * Remove unnecessary code * Improve performance by React.memo
This commit is contained in:
parent
ec9b609453
commit
cd5b0d6347
|
@ -22,6 +22,8 @@ const fields = [
|
|||
type: 'date',
|
||||
scripted: false,
|
||||
filterable: true,
|
||||
aggregatable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
|
@ -34,12 +36,14 @@ const fields = [
|
|||
type: 'string',
|
||||
scripted: false,
|
||||
filterable: true,
|
||||
aggregatable: true,
|
||||
},
|
||||
{
|
||||
name: 'bytes',
|
||||
type: 'number',
|
||||
scripted: false,
|
||||
filterable: true,
|
||||
aggregatable: true,
|
||||
},
|
||||
{
|
||||
name: 'scripted',
|
||||
|
@ -55,14 +59,14 @@ fields.getByName = (name: string) => {
|
|||
|
||||
const indexPattern = ({
|
||||
id: 'index-pattern-with-timefield-id',
|
||||
title: 'index-pattern-without-timefield',
|
||||
title: 'index-pattern-with-timefield',
|
||||
metaFields: ['_index', '_score'],
|
||||
flattenHit: undefined,
|
||||
formatHit: jest.fn((hit) => hit._source),
|
||||
fields,
|
||||
getComputedFields: () => ({}),
|
||||
getSourceFiltering: () => ({}),
|
||||
getFieldByName: () => ({}),
|
||||
getFieldByName: (name: string) => fields.getByName(name),
|
||||
timeFieldName: 'timestamp',
|
||||
} as unknown) as IndexPattern;
|
||||
|
||||
|
|
|
@ -7,12 +7,14 @@
|
|||
*/
|
||||
|
||||
import { IUiSettingsClient } from 'kibana/public';
|
||||
import { SAMPLE_SIZE_SETTING } from '../../common';
|
||||
import { DEFAULT_COLUMNS_SETTING, SAMPLE_SIZE_SETTING } from '../../common';
|
||||
|
||||
export const uiSettingsMock = ({
|
||||
get: (key: string) => {
|
||||
if (key === SAMPLE_SIZE_SETTING) {
|
||||
return 10;
|
||||
} else if (key === DEFAULT_COLUMNS_SETTING) {
|
||||
return ['default_column'];
|
||||
}
|
||||
},
|
||||
} as unknown) as IUiSettingsClient;
|
||||
|
|
|
@ -9,8 +9,6 @@
|
|||
import _ from 'lodash';
|
||||
import { merge, Subject, Subscription } from 'rxjs';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import moment from 'moment';
|
||||
import dateMath from '@elastic/datemath';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { createSearchSessionRestorationDataProvider, getState, splitState } from './discover_state';
|
||||
import { RequestAdapter } from '../../../../inspector/public';
|
||||
|
@ -23,7 +21,6 @@ import {
|
|||
} from '../../../../data/public';
|
||||
import { getSortArray } from './doc_table';
|
||||
import indexTemplateLegacy from './discover_legacy.html';
|
||||
import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util';
|
||||
import { discoverResponseHandler } from './response_handler';
|
||||
import {
|
||||
getAngularModule,
|
||||
|
@ -36,25 +33,22 @@ import {
|
|||
subscribeWithScope,
|
||||
tabifyAggResponse,
|
||||
} from '../../kibana_services';
|
||||
import {
|
||||
getRootBreadcrumbs,
|
||||
getSavedSearchBreadcrumbs,
|
||||
setBreadcrumbsTitle,
|
||||
} from '../helpers/breadcrumbs';
|
||||
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 {
|
||||
DEFAULT_COLUMNS_SETTING,
|
||||
SAMPLE_SIZE_SETTING,
|
||||
SEARCH_FIELDS_FROM_SOURCE,
|
||||
SEARCH_ON_PAGE_LOAD_SETTING,
|
||||
SORT_DEFAULT_ORDER_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 { getDefaultSort } from './doc_table/lib/get_default_sort';
|
||||
import { DiscoverSearchSessionManager } from './discover_search_session';
|
||||
import { applyAggsToSearchSource, getDimensions } from '../components/histogram';
|
||||
import { fetchStatuses } from '../components/constants';
|
||||
|
||||
const services = getServices();
|
||||
|
||||
|
@ -70,13 +64,6 @@ const {
|
|||
uiSettings: config,
|
||||
} = getServices();
|
||||
|
||||
const fetchStatuses = {
|
||||
UNINITIALIZED: 'uninitialized',
|
||||
LOADING: 'loading',
|
||||
COMPLETE: 'complete',
|
||||
ERROR: 'error',
|
||||
};
|
||||
|
||||
const app = getAngularModule();
|
||||
|
||||
app.config(($routeProvider) => {
|
||||
|
@ -161,7 +148,7 @@ app.directive('discoverApp', function () {
|
|||
};
|
||||
});
|
||||
|
||||
function discoverController($route, $scope, Promise) {
|
||||
function discoverController($route, $scope) {
|
||||
const { isDefault: isDefaultType } = indexPatternsUtils;
|
||||
const subscriptions = new Subscription();
|
||||
const refetch$ = new Subject();
|
||||
|
@ -191,7 +178,14 @@ function discoverController($route, $scope, Promise) {
|
|||
});
|
||||
|
||||
const stateContainer = getState({
|
||||
getStateDefaults,
|
||||
getStateDefaults: () =>
|
||||
getStateDefaults({
|
||||
config,
|
||||
data,
|
||||
indexPattern: $scope.indexPattern,
|
||||
savedSearch,
|
||||
searchSource: persistentSearchSource,
|
||||
}),
|
||||
storeInSessionStorage: config.get('state:storeInSessionStorage'),
|
||||
history,
|
||||
toasts: core.notifications.toasts,
|
||||
|
@ -232,6 +226,21 @@ function discoverController($route, $scope, Promise) {
|
|||
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);
|
||||
|
@ -293,21 +302,6 @@ function discoverController($route, $scope, Promise) {
|
|||
}
|
||||
);
|
||||
|
||||
// update data source when filters update
|
||||
subscriptions.add(
|
||||
subscribeWithScope(
|
||||
$scope,
|
||||
filterManager.getUpdates$(),
|
||||
{
|
||||
next: () => {
|
||||
$scope.state.filters = filterManager.getAppFilters();
|
||||
$scope.updateDataSource();
|
||||
},
|
||||
},
|
||||
(error) => addFatalError(core.fatalErrors, error)
|
||||
)
|
||||
);
|
||||
|
||||
$scope.opts = {
|
||||
// number of records to fetch, then paginate through
|
||||
sampleSize: config.get(SAMPLE_SIZE_SETTING),
|
||||
|
@ -329,8 +323,19 @@ function discoverController($route, $scope, Promise) {
|
|||
requests: new RequestAdapter(),
|
||||
});
|
||||
|
||||
$scope.minimumVisibleRows = 50;
|
||||
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', () => {
|
||||
|
@ -385,157 +390,12 @@ function discoverController($route, $scope, Promise) {
|
|||
|
||||
volatileSearchSource.setParent(persistentSearchSource);
|
||||
$scope.volatileSearchSource = volatileSearchSource;
|
||||
|
||||
const pageTitleSuffix = savedSearch.id && savedSearch.title ? `: ${savedSearch.title}` : '';
|
||||
chrome.docTitle.change(`Discover${pageTitleSuffix}`);
|
||||
|
||||
setBreadcrumbsTitle(savedSearch, chrome);
|
||||
|
||||
function getDefaultColumns() {
|
||||
if (savedSearch.columns.length > 0) {
|
||||
return [...savedSearch.columns];
|
||||
}
|
||||
return [...config.get(DEFAULT_COLUMNS_SETTING)];
|
||||
}
|
||||
|
||||
function getStateDefaults() {
|
||||
const query =
|
||||
persistentSearchSource.getField('query') || data.query.queryString.getDefaultQuery();
|
||||
const sort = getSortArray(savedSearch.sort, $scope.indexPattern);
|
||||
const columns = getDefaultColumns();
|
||||
|
||||
const defaultState = {
|
||||
query,
|
||||
sort: !sort.length
|
||||
? getDefaultSort($scope.indexPattern, config.get(SORT_DEFAULT_ORDER_SETTING, 'desc'))
|
||||
: sort,
|
||||
columns,
|
||||
index: $scope.indexPattern.id,
|
||||
interval: 'auto',
|
||||
filters: _.cloneDeep(persistentSearchSource.getOwnField('filter')),
|
||||
};
|
||||
if (savedSearch.grid) {
|
||||
defaultState.grid = savedSearch.grid;
|
||||
}
|
||||
if (savedSearch.hideChart) {
|
||||
defaultState.hideChart = savedSearch.hideChart;
|
||||
}
|
||||
|
||||
return defaultState;
|
||||
}
|
||||
|
||||
$scope.state.index = $scope.indexPattern.id;
|
||||
$scope.state.sort = getSortArray($scope.state.sort, $scope.indexPattern);
|
||||
|
||||
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()
|
||||
);
|
||||
};
|
||||
|
||||
const init = _.once(() => {
|
||||
$scope.updateDataSource().then(async () => {
|
||||
const fetch$ = merge(
|
||||
refetch$,
|
||||
filterManager.getFetches$(),
|
||||
timefilter.getFetch$(),
|
||||
timefilter.getAutoRefreshFetch$(),
|
||||
data.query.queryString.getUpdates$(),
|
||||
searchSessionManager.newSearchSessionIdFromURL$
|
||||
).pipe(debounceTime(100));
|
||||
|
||||
subscriptions.add(
|
||||
subscribeWithScope(
|
||||
$scope,
|
||||
fetch$,
|
||||
{
|
||||
next: $scope.fetch,
|
||||
},
|
||||
(error) => addFatalError(core.fatalErrors, error)
|
||||
)
|
||||
);
|
||||
subscriptions.add(
|
||||
subscribeWithScope(
|
||||
$scope,
|
||||
timefilter.getTimeUpdate$(),
|
||||
{
|
||||
next: () => {
|
||||
$scope.updateTime();
|
||||
},
|
||||
},
|
||||
(error) => addFatalError(core.fatalErrors, error)
|
||||
)
|
||||
);
|
||||
|
||||
$scope.$watchMulti(
|
||||
['rows', 'fetchStatus'],
|
||||
(function updateResultState() {
|
||||
let prev = {};
|
||||
const status = {
|
||||
UNINITIALIZED: 'uninitialized',
|
||||
LOADING: 'loading', // initial data load
|
||||
READY: 'ready', // results came back
|
||||
NO_RESULTS: 'none', // no results came back
|
||||
};
|
||||
|
||||
function pick(rows, oldRows, fetchStatus) {
|
||||
// initial state, pretend we're already loading if we're about to execute a search so
|
||||
// that the uninitilized message doesn't flash on screen
|
||||
if (!$scope.fetchError && rows == null && oldRows == null && shouldSearchOnPageLoad()) {
|
||||
return status.LOADING;
|
||||
}
|
||||
|
||||
if (fetchStatus === fetchStatuses.UNINITIALIZED) {
|
||||
return status.UNINITIALIZED;
|
||||
}
|
||||
|
||||
const rowsEmpty = _.isEmpty(rows);
|
||||
if (rowsEmpty && fetchStatus === fetchStatuses.LOADING) return status.LOADING;
|
||||
else if (!rowsEmpty) return status.READY;
|
||||
else return status.NO_RESULTS;
|
||||
}
|
||||
|
||||
return function () {
|
||||
const current = {
|
||||
rows: $scope.rows,
|
||||
fetchStatus: $scope.fetchStatus,
|
||||
};
|
||||
|
||||
$scope.resultState = pick(
|
||||
current.rows,
|
||||
prev.rows,
|
||||
current.fetchStatus,
|
||||
prev.fetchStatus
|
||||
);
|
||||
|
||||
prev = current;
|
||||
};
|
||||
})()
|
||||
);
|
||||
|
||||
if (getTimeField()) {
|
||||
setupVisualization();
|
||||
$scope.updateTime();
|
||||
}
|
||||
|
||||
init.complete = true;
|
||||
if (shouldSearchOnPageLoad()) {
|
||||
refetch$.next();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$scope.opts.fetch = $scope.fetch = function () {
|
||||
// ignore requests to fetch before the app inits
|
||||
if (!init.complete) return;
|
||||
$scope.fetchCounter++;
|
||||
$scope.fetchError = undefined;
|
||||
$scope.minimumVisibleRows = 50;
|
||||
if (!validateTimeRange(timefilter.getTime(), toastNotifications)) {
|
||||
$scope.resultState = 'none';
|
||||
return;
|
||||
|
@ -546,17 +406,23 @@ function discoverController($route, $scope, Promise) {
|
|||
abortController = new AbortController();
|
||||
|
||||
const searchSessionId = searchSessionManager.getNextSearchSessionId();
|
||||
updateSearchSourceHelper();
|
||||
|
||||
$scope
|
||||
.updateDataSource()
|
||||
.then(setupVisualization)
|
||||
.then(function () {
|
||||
$scope.fetchStatus = fetchStatuses.LOADING;
|
||||
logInspectorRequest({ searchSessionId });
|
||||
return $scope.volatileSearchSource.fetch({
|
||||
abortSignal: abortController.signal,
|
||||
sessionId: searchSessionId,
|
||||
});
|
||||
$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);
|
||||
logInspectorRequest({ searchSessionId });
|
||||
return $scope.volatileSearchSource
|
||||
.fetch({
|
||||
abortSignal: abortController.signal,
|
||||
sessionId: searchSessionId,
|
||||
})
|
||||
.then(onResults)
|
||||
.catch((error) => {
|
||||
|
@ -565,40 +431,14 @@ function discoverController($route, $scope, Promise) {
|
|||
|
||||
$scope.fetchStatus = fetchStatuses.NO_RESULTS;
|
||||
$scope.fetchError = error;
|
||||
|
||||
data.search.showError(error);
|
||||
})
|
||||
.finally(() => {
|
||||
$scope.resultState = getResultState($scope.fetchStatus, $scope.rows);
|
||||
$scope.$apply();
|
||||
});
|
||||
};
|
||||
|
||||
function getDimensions(aggs, timeRange) {
|
||||
const [metric, agg] = aggs;
|
||||
agg.params.timeRange = timeRange;
|
||||
const bounds = agg.params.timeRange ? timefilter.calculateBounds(agg.params.timeRange) : null;
|
||||
agg.buckets.setBounds(bounds);
|
||||
|
||||
const { esUnit, esValue } = agg.buckets.getInterval();
|
||||
return {
|
||||
x: {
|
||||
accessor: 0,
|
||||
label: agg.makeLabel(),
|
||||
format: agg.toSerializedFieldFormat(),
|
||||
params: {
|
||||
date: true,
|
||||
interval: moment.duration(esValue, esUnit),
|
||||
intervalESValue: esValue,
|
||||
intervalESUnit: esUnit,
|
||||
format: agg.buckets.getScaledDateFormat(),
|
||||
bounds: agg.buckets.getBounds(),
|
||||
},
|
||||
},
|
||||
y: {
|
||||
accessor: 1,
|
||||
format: metric.toSerializedFieldFormat(),
|
||||
label: metric.makeLabel(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function onResults(resp) {
|
||||
inspectorRequest
|
||||
.stats(getResponseInspectorStats(resp, $scope.volatileSearchSource))
|
||||
|
@ -607,11 +447,10 @@ function discoverController($route, $scope, Promise) {
|
|||
if (getTimeField() && !$scope.state.hideChart) {
|
||||
const tabifiedData = tabifyAggResponse($scope.opts.chartAggConfigs, resp);
|
||||
$scope.volatileSearchSource.rawResponse = resp;
|
||||
$scope.histogramData = discoverResponseHandler(
|
||||
tabifiedData,
|
||||
getDimensions($scope.opts.chartAggConfigs.aggs, $scope.timeRange)
|
||||
);
|
||||
$scope.updateTime();
|
||||
const dimensions = getDimensions($scope.opts.chartAggConfigs, data);
|
||||
if (dimensions) {
|
||||
$scope.histogramData = discoverResponseHandler(tabifiedData, dimensions);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.hits = resp.hits.total;
|
||||
|
@ -640,15 +479,6 @@ function discoverController($route, $scope, Promise) {
|
|||
});
|
||||
}
|
||||
|
||||
$scope.updateTime = function () {
|
||||
const { from, to } = timefilter.getTime();
|
||||
// this is the timerange for the histogram, should be refactored
|
||||
$scope.timeRange = {
|
||||
from: dateMath.parse(from),
|
||||
to: dateMath.parse(to, { roundUp: true }),
|
||||
};
|
||||
};
|
||||
|
||||
$scope.resetQuery = function () {
|
||||
history.push(
|
||||
$route.current.params.id ? `/view/${encodeURIComponent($route.current.params.id)}` : '/'
|
||||
|
@ -656,88 +486,39 @@ function discoverController($route, $scope, Promise) {
|
|||
$route.reload();
|
||||
};
|
||||
|
||||
$scope.onSkipBottomButtonClick = async () => {
|
||||
// show all the Rows
|
||||
$scope.minimumVisibleRows = $scope.hits;
|
||||
|
||||
// delay scrolling to after the rows have been rendered
|
||||
const bottomMarker = document.getElementById('discoverBottomMarker');
|
||||
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
while ($scope.rows.length !== document.getElementsByClassName('kbnDocTable__row').length) {
|
||||
await wait(50);
|
||||
}
|
||||
bottomMarker.focus();
|
||||
await wait(50);
|
||||
bottomMarker.blur();
|
||||
};
|
||||
|
||||
$scope.newQuery = function () {
|
||||
history.push('/');
|
||||
};
|
||||
|
||||
const showUnmappedFields = $scope.useNewFieldsApi;
|
||||
|
||||
$scope.unmappedFieldsConfig = {
|
||||
showUnmappedFields,
|
||||
};
|
||||
|
||||
$scope.updateDataSource = () => {
|
||||
const { indexPattern, useNewFieldsApi } = $scope;
|
||||
const { columns, sort } = $scope.state;
|
||||
updateSearchSource({
|
||||
persistentSearchSource,
|
||||
volatileSearchSource: $scope.volatileSearchSource,
|
||||
indexPattern,
|
||||
services,
|
||||
sort,
|
||||
columns,
|
||||
useNewFieldsApi,
|
||||
showUnmappedFields,
|
||||
});
|
||||
return Promise.resolve();
|
||||
};
|
||||
const fetch$ = merge(
|
||||
refetch$,
|
||||
filterManager.getFetches$(),
|
||||
timefilter.getFetch$(),
|
||||
timefilter.getAutoRefreshFetch$(),
|
||||
data.query.queryString.getUpdates$(),
|
||||
searchSessionManager.newSearchSessionIdFromURL$
|
||||
).pipe(debounceTime(100));
|
||||
|
||||
async function setupVisualization() {
|
||||
// If no timefield has been specified we don't create a histogram of messages
|
||||
if (!getTimeField() || $scope.state.hideChart) {
|
||||
if ($scope.volatileSearchSource.getField('aggs')) {
|
||||
// cleanup aggs field in case it was set before
|
||||
$scope.volatileSearchSource.removeField('aggs');
|
||||
}
|
||||
return;
|
||||
subscriptions.add(
|
||||
subscribeWithScope(
|
||||
$scope,
|
||||
fetch$,
|
||||
{
|
||||
next: $scope.fetch,
|
||||
},
|
||||
(error) => addFatalError(core.fatalErrors, error)
|
||||
)
|
||||
);
|
||||
|
||||
// Propagate current app state to url, then start syncing and fetching
|
||||
replaceUrlAppState().then(() => {
|
||||
startStateSync();
|
||||
if (shouldSearchOnPageLoad()) {
|
||||
refetch$.next();
|
||||
}
|
||||
const { interval: histogramInterval } = $scope.state;
|
||||
|
||||
const visStateAggs = [
|
||||
{
|
||||
type: 'count',
|
||||
schema: 'metric',
|
||||
},
|
||||
{
|
||||
type: 'date_histogram',
|
||||
schema: 'segment',
|
||||
params: {
|
||||
field: getTimeField(),
|
||||
interval: histogramInterval,
|
||||
timeRange: timefilter.getTime(),
|
||||
},
|
||||
},
|
||||
];
|
||||
$scope.opts.chartAggConfigs = data.search.aggs.createAggConfigs(
|
||||
$scope.indexPattern,
|
||||
visStateAggs
|
||||
);
|
||||
|
||||
$scope.volatileSearchSource.setField('aggs', function () {
|
||||
if (!$scope.opts.chartAggConfigs) return;
|
||||
return $scope.opts.chartAggConfigs.toDsl();
|
||||
});
|
||||
}
|
||||
|
||||
addHelpMenuToAppChrome(chrome);
|
||||
|
||||
init();
|
||||
// Propagate current app state to url, then start syncing
|
||||
replaceUrlAppState().then(() => startStateSync());
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,15 +7,12 @@
|
|||
histogram-data="histogramData"
|
||||
hits="hits"
|
||||
index-pattern="indexPattern"
|
||||
minimum-visible-rows="minimumVisibleRows"
|
||||
on-skip-bottom-button-click="onSkipBottomButtonClick"
|
||||
opts="opts"
|
||||
reset-query="resetQuery"
|
||||
result-state="resultState"
|
||||
rows="rows"
|
||||
search-source="volatileSearchSource"
|
||||
state="state"
|
||||
time-range="timeRange"
|
||||
top-nav-menu="topNavMenu"
|
||||
use-new-fields-api="useNewFieldsApi"
|
||||
unmapped-fields-config="unmappedFieldsConfig"
|
||||
|
|
|
@ -8,11 +8,12 @@
|
|||
|
||||
import angular, { auto, ICompileService, IScope } from 'angular';
|
||||
import { render } from 'react-dom';
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
||||
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';
|
||||
|
||||
export interface DocTableLegacyProps {
|
||||
columns: string[];
|
||||
|
@ -97,18 +98,42 @@ function getRenderFn(domNode: Element, props: any) {
|
|||
export function DocTableLegacy(renderProps: DocTableLegacyProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const scope = useRef<AngularScope | undefined>();
|
||||
const [rows, setRows] = useState(renderProps.rows);
|
||||
const [minimumVisibleRows, setMinimumVisibleRows] = useState(50);
|
||||
const onSkipBottomButtonClick = useCallback(async () => {
|
||||
// delay scrolling to after the rows have been rendered
|
||||
const bottomMarker = document.getElementById('discoverBottomMarker');
|
||||
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
// show all the rows
|
||||
setMinimumVisibleRows(renderProps.rows.length);
|
||||
|
||||
while (renderProps.rows.length !== document.getElementsByClassName('kbnDocTable__row').length) {
|
||||
await wait(50);
|
||||
}
|
||||
bottomMarker!.focus();
|
||||
await wait(50);
|
||||
bottomMarker!.blur();
|
||||
}, [setMinimumVisibleRows, renderProps.rows]);
|
||||
|
||||
useEffect(() => {
|
||||
if (minimumVisibleRows > 50) {
|
||||
setMinimumVisibleRows(50);
|
||||
}
|
||||
setRows(renderProps.rows);
|
||||
}, [renderProps.rows, minimumVisibleRows, setMinimumVisibleRows]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref && ref.current && !scope.current) {
|
||||
const fn = getRenderFn(ref.current, renderProps);
|
||||
const fn = getRenderFn(ref.current, { ...renderProps, rows, minimumVisibleRows });
|
||||
fn().then((newScope) => {
|
||||
scope.current = newScope;
|
||||
});
|
||||
} else if (scope && scope.current) {
|
||||
scope.current.renderProps = renderProps;
|
||||
scope.current.renderProps = { ...renderProps, rows, minimumVisibleRows };
|
||||
scope.current.$apply();
|
||||
}
|
||||
}, [renderProps]);
|
||||
}, [renderProps, minimumVisibleRows, rows]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (scope.current) {
|
||||
|
@ -118,6 +143,7 @@ export function DocTableLegacy(renderProps: DocTableLegacyProps) {
|
|||
}, []);
|
||||
return (
|
||||
<div>
|
||||
<SkipBottomButton onClick={onSkipBottomButtonClick} />
|
||||
<div ref={ref} />
|
||||
{renderProps.rows.length === renderProps.sampleSize ? (
|
||||
<div
|
||||
|
@ -132,7 +158,7 @@ export function DocTableLegacy(renderProps: DocTableLegacyProps) {
|
|||
your search, refine your search to see others."
|
||||
values={{ sampleSize: renderProps.sampleSize }}
|
||||
/>
|
||||
<EuiButtonEmpty onClick={renderProps.onBackToTop}>
|
||||
<EuiButtonEmpty onClick={renderProps.onBackToTop} data-test-subj="discoverBackToTop">
|
||||
<FormattedMessage id="discover.backToTopLinkText" defaultMessage="Back to top." />
|
||||
</EuiButtonEmpty>
|
||||
</div>
|
||||
|
|
|
@ -14,9 +14,9 @@ export type SortPairArr = [string, string];
|
|||
export type SortPair = SortPairArr | SortPairObj;
|
||||
export type SortInput = SortPair | SortPair[];
|
||||
|
||||
export function isSortable(fieldName: string, indexPattern: IndexPattern) {
|
||||
export function isSortable(fieldName: string, indexPattern: IndexPattern): boolean {
|
||||
const field = indexPattern.getFieldByName(fieldName);
|
||||
return field && field.sortable;
|
||||
return !!(field && field.sortable);
|
||||
}
|
||||
|
||||
function createSortObject(
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export const fetchStatuses = {
|
||||
UNINITIALIZED: 'uninitialized',
|
||||
LOADING: 'loading',
|
||||
COMPLETE: 'complete',
|
||||
ERROR: 'error',
|
||||
};
|
|
@ -16,8 +16,6 @@ export function createDiscoverDirective(reactDirective: any) {
|
|||
['histogramData', { watchDepth: 'reference' }],
|
||||
['hits', { watchDepth: 'reference' }],
|
||||
['indexPattern', { watchDepth: 'reference' }],
|
||||
['minimumVisibleRows', { watchDepth: 'reference' }],
|
||||
['onSkipBottomButtonClick', { watchDepth: 'reference' }],
|
||||
['opts', { watchDepth: 'reference' }],
|
||||
['resetQuery', { watchDepth: 'reference' }],
|
||||
['resultState', { watchDepth: 'reference' }],
|
||||
|
@ -26,7 +24,6 @@ export function createDiscoverDirective(reactDirective: any) {
|
|||
['searchSource', { watchDepth: 'reference' }],
|
||||
['showSaveQuery', { watchDepth: 'reference' }],
|
||||
['state', { watchDepth: 'reference' }],
|
||||
['timeRange', { watchDepth: 'reference' }],
|
||||
['topNavMenu', { watchDepth: 'reference' }],
|
||||
['updateQuery', { watchDepth: 'reference' }],
|
||||
['updateSavedQueryId', { watchDepth: 'reference' }],
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
import './discover.scss';
|
||||
import React, { useState, useRef, useMemo, useCallback } from 'react';
|
||||
import React, { useState, useRef, useMemo, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiButtonIcon,
|
||||
|
@ -30,7 +30,6 @@ 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 { SkipBottomButton } from './skip_bottom_button';
|
||||
import { esFilters, IndexPatternField, search } from '../../../../data/public';
|
||||
import { DiscoverSidebarResponsive } from './sidebar';
|
||||
import { DiscoverProps } from './types';
|
||||
|
@ -42,11 +41,15 @@ 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';
|
||||
|
||||
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);
|
||||
|
||||
export function Discover({
|
||||
fetch,
|
||||
|
@ -58,14 +61,12 @@ export function Discover({
|
|||
hits,
|
||||
indexPattern,
|
||||
minimumVisibleRows,
|
||||
onSkipBottomButtonClick,
|
||||
opts,
|
||||
resetQuery,
|
||||
resultState,
|
||||
rows,
|
||||
searchSource,
|
||||
state,
|
||||
timeRange,
|
||||
unmappedFieldsConfig,
|
||||
}: DiscoverProps) {
|
||||
const [expandedDoc, setExpandedDoc] = useState<ElasticSearchHit | undefined>(undefined);
|
||||
|
@ -81,13 +82,16 @@ export function Discover({
|
|||
}, [state, opts]);
|
||||
const hideChart = useMemo(() => state.hideChart, [state]);
|
||||
const { savedSearch, indexPatternList, config, services, data, setAppState } = opts;
|
||||
const { trackUiMetric, capabilities, indexPatterns } = services;
|
||||
const { trackUiMetric, capabilities, indexPatterns, chrome, docLinks } = services;
|
||||
|
||||
const [isSidebarClosed, setIsSidebarClosed] = useState(false);
|
||||
const bucketAggConfig = opts.chartAggConfigs?.aggs[1];
|
||||
const bucketInterval =
|
||||
bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig)
|
||||
const bucketInterval = useMemo(() => {
|
||||
const bucketAggConfig = opts.chartAggConfigs?.aggs[1];
|
||||
return bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig)
|
||||
? bucketAggConfig.buckets?.getInterval()
|
||||
: undefined;
|
||||
}, [opts.chartAggConfigs]);
|
||||
|
||||
const contentCentered = resultState === 'uninitialized';
|
||||
const isLegacy = services.uiSettings.get('doc_table:legacy');
|
||||
const useNewFieldsApi = !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE);
|
||||
|
@ -101,6 +105,14 @@ export function Discover({
|
|||
[opts]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const pageTitleSuffix = savedSearch.id && savedSearch.title ? `: ${savedSearch.title}` : '';
|
||||
chrome.docTitle.change(`Discover${pageTitleSuffix}`);
|
||||
|
||||
setBreadcrumbsTitle(savedSearch, chrome);
|
||||
addHelpMenuToAppChrome(chrome, docLinks);
|
||||
}, [savedSearch, chrome, docLinks]);
|
||||
|
||||
const { onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useMemo(
|
||||
() =>
|
||||
getStateColumnActions({
|
||||
|
@ -293,9 +305,9 @@ export function Discover({
|
|||
</EuiFlexItem>
|
||||
{!hideChart && (
|
||||
<EuiFlexItem className="dscResultCount__actions">
|
||||
<TimechartHeader
|
||||
<TimechartHeaderMemoized
|
||||
data={opts.data}
|
||||
dateFormat={opts.config.get('dateFormat')}
|
||||
timeRange={timeRange}
|
||||
options={search.aggs.intervalOptions}
|
||||
onChangeInterval={onChangeInterval}
|
||||
stateInterval={state.interval || ''}
|
||||
|
@ -324,7 +336,6 @@ export function Discover({
|
|||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
{isLegacy && <SkipBottomButton onClick={onSkipBottomButtonClick} />}
|
||||
</EuiFlexItem>
|
||||
{!hideChart && opts.timefield && (
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -342,7 +353,7 @@ export function Discover({
|
|||
className={isLegacy ? 'dscHistogram' : 'dscHistogramGrid'}
|
||||
data-test-subj="discoverChart"
|
||||
>
|
||||
<DiscoverHistogram
|
||||
<DiscoverHistogramMemoized
|
||||
chartData={histogramData}
|
||||
timefilterUpdateHandler={timefilterUpdateHandler}
|
||||
/>
|
||||
|
|
|
@ -64,11 +64,14 @@ describe('Discover grid columns ', function () {
|
|||
"showMoveLeft": false,
|
||||
"showMoveRight": false,
|
||||
},
|
||||
"cellActions": undefined,
|
||||
"cellActions": Array [
|
||||
[Function],
|
||||
[Function],
|
||||
],
|
||||
"display": undefined,
|
||||
"id": "extension",
|
||||
"isSortable": false,
|
||||
"schema": "kibana-json",
|
||||
"schema": "string",
|
||||
},
|
||||
Object {
|
||||
"actions": Object {
|
||||
|
@ -80,7 +83,7 @@ describe('Discover grid columns ', function () {
|
|||
"display": undefined,
|
||||
"id": "message",
|
||||
"isSortable": false,
|
||||
"schema": "kibana-json",
|
||||
"schema": "string",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
@ -101,12 +104,15 @@ describe('Discover grid columns ', function () {
|
|||
"showMoveLeft": true,
|
||||
"showMoveRight": true,
|
||||
},
|
||||
"cellActions": undefined,
|
||||
"cellActions": Array [
|
||||
[Function],
|
||||
[Function],
|
||||
],
|
||||
"display": "Time (timestamp)",
|
||||
"id": "timestamp",
|
||||
"initialWidth": 180,
|
||||
"isSortable": false,
|
||||
"schema": "kibana-json",
|
||||
"isSortable": true,
|
||||
"schema": "datetime",
|
||||
},
|
||||
Object {
|
||||
"actions": Object {
|
||||
|
@ -117,11 +123,14 @@ describe('Discover grid columns ', function () {
|
|||
"showMoveLeft": true,
|
||||
"showMoveRight": true,
|
||||
},
|
||||
"cellActions": undefined,
|
||||
"cellActions": Array [
|
||||
[Function],
|
||||
[Function],
|
||||
],
|
||||
"display": undefined,
|
||||
"id": "extension",
|
||||
"isSortable": false,
|
||||
"schema": "kibana-json",
|
||||
"schema": "string",
|
||||
},
|
||||
Object {
|
||||
"actions": Object {
|
||||
|
@ -136,7 +145,7 @@ describe('Discover grid columns ', function () {
|
|||
"display": undefined,
|
||||
"id": "message",
|
||||
"isSortable": false,
|
||||
"schema": "kibana-json",
|
||||
"schema": "string",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
|
|
@ -7,10 +7,9 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getServices } from '../../../kibana_services';
|
||||
const { docLinks } = getServices();
|
||||
import { ChromeStart, DocLinksStart } from 'kibana/public';
|
||||
|
||||
export function addHelpMenuToAppChrome(chrome) {
|
||||
export function addHelpMenuToAppChrome(chrome: ChromeStart, docLinks: DocLinksStart) {
|
||||
chrome.setHelpExtension({
|
||||
appName: i18n.translate('discover.helpMenu.appName', {
|
||||
defaultMessage: 'Discover',
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_with_timefield';
|
||||
import { SearchSource } from '../../../../../data/public';
|
||||
import { dataPluginMock } from '../../../../../data/public/mocks';
|
||||
import { applyAggsToSearchSource } from './apply_aggs_to_search_source';
|
||||
|
||||
describe('applyAggsToSearchSource', () => {
|
||||
test('enabled = true', () => {
|
||||
const indexPattern = indexPatternWithTimefieldMock;
|
||||
const setField = jest.fn();
|
||||
const searchSource = ({
|
||||
setField,
|
||||
removeField: jest.fn(),
|
||||
} as unknown) as SearchSource;
|
||||
|
||||
const dataMock = dataPluginMock.createStartContract();
|
||||
|
||||
const aggsConfig = applyAggsToSearchSource(true, searchSource, 'auto', indexPattern, dataMock);
|
||||
|
||||
expect(aggsConfig!.aggs).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"enabled": true,
|
||||
"id": "1",
|
||||
"params": Object {},
|
||||
"schema": "metric",
|
||||
"type": "count",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"id": "2",
|
||||
"params": Object {
|
||||
"drop_partials": false,
|
||||
"extended_bounds": Object {},
|
||||
"field": "timestamp",
|
||||
"interval": "auto",
|
||||
"min_doc_count": 1,
|
||||
"scaleMetricValues": false,
|
||||
"useNormalizedEsInterval": true,
|
||||
},
|
||||
"schema": "segment",
|
||||
"type": "date_histogram",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
expect(setField).toHaveBeenCalledWith('aggs', expect.any(Function));
|
||||
const dslFn = setField.mock.calls[0][1];
|
||||
expect(dslFn()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"2": Object {
|
||||
"date_histogram": Object {
|
||||
"field": "timestamp",
|
||||
"min_doc_count": 1,
|
||||
"time_zone": "America/New_York",
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('enabled = false', () => {
|
||||
const indexPattern = indexPatternWithTimefieldMock;
|
||||
const setField = jest.fn();
|
||||
const getField = jest.fn(() => {
|
||||
return true;
|
||||
});
|
||||
const removeField = jest.fn();
|
||||
const searchSource = ({
|
||||
getField,
|
||||
setField,
|
||||
removeField,
|
||||
} as unknown) as SearchSource;
|
||||
|
||||
const dataMock = dataPluginMock.createStartContract();
|
||||
|
||||
const aggsConfig = applyAggsToSearchSource(false, searchSource, 'auto', indexPattern, dataMock);
|
||||
expect(aggsConfig).toBeFalsy();
|
||||
expect(getField).toHaveBeenCalledWith('aggs');
|
||||
expect(removeField).toHaveBeenCalledWith('aggs');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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, SearchSource } from '../../../../../data/common';
|
||||
import { DataPublicPluginStart } from '../../../../../data/public';
|
||||
|
||||
/**
|
||||
* Helper function to apply or remove aggregations to a given search source used for gaining data
|
||||
* for Discover's histogram vis
|
||||
*/
|
||||
export function applyAggsToSearchSource(
|
||||
enabled: boolean,
|
||||
searchSource: SearchSource,
|
||||
histogramInterval: string,
|
||||
indexPattern: IndexPattern,
|
||||
data: DataPublicPluginStart
|
||||
) {
|
||||
if (!enabled) {
|
||||
if (searchSource.getField('aggs')) {
|
||||
// clean up fields in case it was set before
|
||||
searchSource.removeField('aggs');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const visStateAggs = [
|
||||
{
|
||||
type: 'count',
|
||||
schema: 'metric',
|
||||
},
|
||||
{
|
||||
type: 'date_histogram',
|
||||
schema: 'segment',
|
||||
params: {
|
||||
field: indexPattern.timeFieldName!,
|
||||
interval: histogramInterval,
|
||||
timeRange: data.query.timefilter.timefilter.getTime(),
|
||||
},
|
||||
},
|
||||
];
|
||||
const chartAggConfigs = data.search.aggs.createAggConfigs(indexPattern, visStateAggs);
|
||||
|
||||
searchSource.setField('aggs', function () {
|
||||
return chartAggConfigs.toDsl();
|
||||
});
|
||||
return chartAggConfigs;
|
||||
}
|
|
@ -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 { dataPluginMock } from '../../../../../data/public/mocks';
|
||||
|
||||
import { getDimensions } from './get_dimensions';
|
||||
import { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_with_timefield';
|
||||
import { SearchSource } from '../../../../../data/common/search/search_source';
|
||||
import { applyAggsToSearchSource } from './apply_aggs_to_search_source';
|
||||
import { calculateBounds } from '../../../../../data/common/query/timefilter';
|
||||
|
||||
test('getDimensions', () => {
|
||||
const indexPattern = indexPatternWithTimefieldMock;
|
||||
const setField = jest.fn();
|
||||
const searchSource = ({
|
||||
setField,
|
||||
removeField: jest.fn(),
|
||||
} as unknown) as SearchSource;
|
||||
|
||||
const dataMock = dataPluginMock.createStartContract();
|
||||
dataMock.query.timefilter.timefilter.getTime = () => {
|
||||
return { from: 'now-30y', to: 'now' };
|
||||
};
|
||||
dataMock.query.timefilter.timefilter.calculateBounds = (timeRange) => {
|
||||
return calculateBounds(timeRange);
|
||||
};
|
||||
|
||||
const aggsConfig = applyAggsToSearchSource(true, searchSource, 'auto', indexPattern, dataMock);
|
||||
const actual = getDimensions(aggsConfig!, dataMock);
|
||||
expect(actual).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"x": Object {
|
||||
"accessor": 0,
|
||||
"format": Object {
|
||||
"id": "date",
|
||||
"params": Object {
|
||||
"pattern": "HH:mm:ss.SSS",
|
||||
},
|
||||
},
|
||||
"label": "timestamp per 0 milliseconds",
|
||||
"params": Object {
|
||||
"bounds": undefined,
|
||||
"date": true,
|
||||
"format": "HH:mm:ss.SSS",
|
||||
"interval": "P365D",
|
||||
"intervalESUnit": "d",
|
||||
"intervalESValue": 365,
|
||||
},
|
||||
},
|
||||
"y": Object {
|
||||
"accessor": 1,
|
||||
"format": Object {
|
||||
"id": "number",
|
||||
},
|
||||
"label": "Count",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import moment from 'moment';
|
||||
import dateMath from '@elastic/datemath';
|
||||
import { IAggConfigs, TimeRangeBounds } from '../../../../../data/common';
|
||||
import { DataPublicPluginStart, search } from '../../../../../data/public';
|
||||
|
||||
export function getDimensions(aggs: IAggConfigs, data: DataPublicPluginStart) {
|
||||
const [metric, agg] = aggs.aggs;
|
||||
const { from, to } = data.query.timefilter.timefilter.getTime();
|
||||
agg.params.timeRange = {
|
||||
from: dateMath.parse(from),
|
||||
to: dateMath.parse(to, { roundUp: true }),
|
||||
};
|
||||
const bounds = agg.params.timeRange
|
||||
? data.query.timefilter.timefilter.calculateBounds(agg.params.timeRange)
|
||||
: null;
|
||||
const buckets = search.aggs.isDateHistogramBucketAggConfig(agg) ? agg.buckets : undefined;
|
||||
|
||||
if (!buckets) {
|
||||
return;
|
||||
}
|
||||
|
||||
buckets.setBounds(bounds as TimeRangeBounds);
|
||||
|
||||
const { esUnit, esValue } = buckets.getInterval();
|
||||
return {
|
||||
x: {
|
||||
accessor: 0,
|
||||
label: agg.makeLabel(),
|
||||
format: agg.toSerializedFieldFormat(),
|
||||
params: {
|
||||
date: true,
|
||||
interval: moment.duration(esValue, esUnit),
|
||||
intervalESValue: esValue,
|
||||
intervalESUnit: esUnit,
|
||||
format: buckets.getScaledDateFormat(),
|
||||
bounds: buckets.getBounds(),
|
||||
},
|
||||
},
|
||||
y: {
|
||||
accessor: 1,
|
||||
format: metric.toSerializedFieldFormat(),
|
||||
label: metric.makeLabel(),
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { applyAggsToSearchSource } from './apply_aggs_to_search_source';
|
||||
export { getDimensions } from './get_dimensions';
|
|
@ -12,6 +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';
|
||||
|
||||
describe('timechart header', function () {
|
||||
let props: TimechartHeaderProps;
|
||||
|
@ -19,10 +20,18 @@ describe('timechart header', function () {
|
|||
|
||||
beforeAll(() => {
|
||||
props = {
|
||||
timeRange: {
|
||||
from: 'May 14, 2020 @ 11:05:13.590',
|
||||
to: 'May 14, 2020 @ 11:20:13.590',
|
||||
},
|
||||
data: {
|
||||
query: {
|
||||
timefilter: {
|
||||
timefilter: {
|
||||
getTime: () => {
|
||||
return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' };
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as DataPublicPluginStart,
|
||||
dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS',
|
||||
stateInterval: 's',
|
||||
options: [
|
||||
{
|
||||
|
|
|
@ -15,9 +15,11 @@ import {
|
|||
EuiSelect,
|
||||
EuiIconTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import './timechart_header.scss';
|
||||
import moment from 'moment';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import dateMath from '@elastic/datemath';
|
||||
import { DataPublicPluginStart } from '../../../../../data/public';
|
||||
import './timechart_header.scss';
|
||||
|
||||
export interface TimechartHeaderProps {
|
||||
/**
|
||||
|
@ -32,13 +34,7 @@ export interface TimechartHeaderProps {
|
|||
description?: string;
|
||||
scale?: number;
|
||||
};
|
||||
/**
|
||||
* Range of dates to be displayed
|
||||
*/
|
||||
timeRange?: {
|
||||
from: string;
|
||||
to: string;
|
||||
};
|
||||
data: DataPublicPluginStart;
|
||||
/**
|
||||
* Interval Options
|
||||
*/
|
||||
|
@ -56,21 +52,27 @@ export interface TimechartHeaderProps {
|
|||
export function TimechartHeader({
|
||||
bucketInterval,
|
||||
dateFormat,
|
||||
timeRange,
|
||||
data,
|
||||
options,
|
||||
onChangeInterval,
|
||||
stateInterval,
|
||||
}: TimechartHeaderProps) {
|
||||
const { timefilter } = data.query.timefilter;
|
||||
const { from, to } = timefilter.getTime();
|
||||
const timeRange = {
|
||||
from: dateMath.parse(from),
|
||||
to: dateMath.parse(to, { roundUp: true }),
|
||||
};
|
||||
const [interval, setInterval] = useState(stateInterval);
|
||||
const toMoment = useCallback(
|
||||
(datetime: string) => {
|
||||
(datetime: moment.Moment | undefined) => {
|
||||
if (!datetime) {
|
||||
return '';
|
||||
}
|
||||
if (!dateFormat) {
|
||||
return datetime;
|
||||
return String(datetime);
|
||||
}
|
||||
return moment(datetime).format(dateFormat);
|
||||
return datetime.format(dateFormat);
|
||||
},
|
||||
[dateFormat]
|
||||
);
|
||||
|
|
|
@ -158,10 +158,6 @@ export interface DiscoverProps {
|
|||
* Current app state of URL
|
||||
*/
|
||||
state: AppState;
|
||||
/**
|
||||
* Currently selected time range
|
||||
*/
|
||||
timeRange?: { from: string; to: string };
|
||||
/**
|
||||
* An object containing properties for unmapped fields behavior
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 { getResultState, resultStatuses } from './get_result_state';
|
||||
import { fetchStatuses } from '../components/constants';
|
||||
import { ElasticSearchHit } from '../doc_views/doc_views_types';
|
||||
|
||||
describe('getResultState', () => {
|
||||
test('fetching uninitialized', () => {
|
||||
const actual = getResultState(fetchStatuses.UNINITIALIZED, []);
|
||||
expect(actual).toBe(resultStatuses.UNINITIALIZED);
|
||||
});
|
||||
|
||||
test('fetching complete with no records', () => {
|
||||
const actual = getResultState(fetchStatuses.COMPLETE, []);
|
||||
expect(actual).toBe(resultStatuses.NO_RESULTS);
|
||||
});
|
||||
|
||||
test('fetching ongoing aka loading', () => {
|
||||
const actual = getResultState(fetchStatuses.LOADING, []);
|
||||
expect(actual).toBe(resultStatuses.LOADING);
|
||||
});
|
||||
|
||||
test('fetching ready', () => {
|
||||
const record = ({ _id: 123 } as unknown) as ElasticSearchHit;
|
||||
const actual = getResultState(fetchStatuses.COMPLETE, [record]);
|
||||
expect(actual).toBe(resultStatuses.READY);
|
||||
});
|
||||
|
||||
test('re-fetching after already data is available', () => {
|
||||
const record = ({ _id: 123 } as unknown) as ElasticSearchHit;
|
||||
const actual = getResultState(fetchStatuses.LOADING, [record]);
|
||||
expect(actual).toBe(resultStatuses.READY);
|
||||
});
|
||||
|
||||
test('after a fetch error when data was successfully fetched before ', () => {
|
||||
const record = ({ _id: 123 } as unknown) as ElasticSearchHit;
|
||||
const actual = getResultState(fetchStatuses.ERROR, [record]);
|
||||
expect(actual).toBe(resultStatuses.READY);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { ElasticSearchHit } from '../doc_views/doc_views_types';
|
||||
import { fetchStatuses } from '../components/constants';
|
||||
|
||||
export const resultStatuses = {
|
||||
UNINITIALIZED: 'uninitialized',
|
||||
LOADING: 'loading', // initial data load
|
||||
READY: 'ready', // results came back
|
||||
NO_RESULTS: 'none', // no results came back
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the current state of the result, depends on fetchStatus and the given fetched rows
|
||||
* Determines what is displayed in Discover main view (loading view, data view, empty data view, ...)
|
||||
*/
|
||||
export function getResultState(fetchStatus: string, rows: ElasticSearchHit[]) {
|
||||
if (fetchStatus === fetchStatuses.UNINITIALIZED) {
|
||||
return resultStatuses.UNINITIALIZED;
|
||||
}
|
||||
|
||||
const rowsEmpty = !Array.isArray(rows) || rows.length === 0;
|
||||
if (rowsEmpty && fetchStatus === fetchStatuses.LOADING) return resultStatuses.LOADING;
|
||||
else if (!rowsEmpty) return resultStatuses.READY;
|
||||
else return resultStatuses.NO_RESULTS;
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 { getStateDefaults } from './get_state_defaults';
|
||||
import { createSearchSourceMock, dataPluginMock } from '../../../../data/public/mocks';
|
||||
import { uiSettingsMock } from '../../__mocks__/ui_settings';
|
||||
import { indexPatternWithTimefieldMock } from '../../__mocks__/index_pattern_with_timefield';
|
||||
import { savedSearchMock } from '../../__mocks__/saved_search';
|
||||
import { indexPatternMock } from '../../__mocks__/index_pattern';
|
||||
|
||||
describe('getStateDefaults', () => {
|
||||
test('index pattern with timefield', () => {
|
||||
const actual = getStateDefaults({
|
||||
config: uiSettingsMock,
|
||||
data: dataPluginMock.createStartContract(),
|
||||
indexPattern: indexPatternWithTimefieldMock,
|
||||
savedSearch: savedSearchMock,
|
||||
searchSource: createSearchSourceMock({ index: indexPatternWithTimefieldMock }),
|
||||
});
|
||||
expect(actual).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"columns": Array [
|
||||
"default_column",
|
||||
],
|
||||
"filters": undefined,
|
||||
"index": "index-pattern-with-timefield-id",
|
||||
"interval": "auto",
|
||||
"query": undefined,
|
||||
"sort": Array [
|
||||
Array [
|
||||
"timestamp",
|
||||
"desc",
|
||||
],
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('index pattern without timefield', () => {
|
||||
const actual = getStateDefaults({
|
||||
config: uiSettingsMock,
|
||||
data: dataPluginMock.createStartContract(),
|
||||
indexPattern: indexPatternMock,
|
||||
savedSearch: savedSearchMock,
|
||||
searchSource: createSearchSourceMock({ index: indexPatternMock }),
|
||||
});
|
||||
expect(actual).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"columns": Array [
|
||||
"default_column",
|
||||
],
|
||||
"filters": undefined,
|
||||
"index": "the-index-pattern-id",
|
||||
"interval": "auto",
|
||||
"query": undefined,
|
||||
"sort": Array [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { cloneDeep } from 'lodash';
|
||||
import { IUiSettingsClient } from 'kibana/public';
|
||||
import { DEFAULT_COLUMNS_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common';
|
||||
import { getSortArray } from '../angular/doc_table';
|
||||
import { getDefaultSort } from '../angular/doc_table/lib/get_default_sort';
|
||||
import { SavedSearch } from '../../saved_searches';
|
||||
import { SearchSource } from '../../../../data/common/search/search_source';
|
||||
import { DataPublicPluginStart, IndexPattern } from '../../../../data/public';
|
||||
|
||||
import { AppState } from '../angular/discover_state';
|
||||
|
||||
function getDefaultColumns(savedSearch: SavedSearch, config: IUiSettingsClient) {
|
||||
if (savedSearch.columns && savedSearch.columns.length > 0) {
|
||||
return [...savedSearch.columns];
|
||||
}
|
||||
return [...config.get(DEFAULT_COLUMNS_SETTING)];
|
||||
}
|
||||
|
||||
export function getStateDefaults({
|
||||
config,
|
||||
data,
|
||||
indexPattern,
|
||||
savedSearch,
|
||||
searchSource,
|
||||
}: {
|
||||
config: IUiSettingsClient;
|
||||
data: DataPublicPluginStart;
|
||||
indexPattern: IndexPattern;
|
||||
savedSearch: SavedSearch;
|
||||
searchSource: SearchSource;
|
||||
}) {
|
||||
const query = searchSource.getField('query') || data.query.queryString.getDefaultQuery();
|
||||
const sort = getSortArray(savedSearch.sort, indexPattern);
|
||||
const columns = getDefaultColumns(savedSearch, config);
|
||||
|
||||
const defaultState = {
|
||||
query,
|
||||
sort: !sort.length
|
||||
? getDefaultSort(indexPattern, config.get(SORT_DEFAULT_ORDER_SETTING, 'desc'))
|
||||
: sort,
|
||||
columns,
|
||||
index: indexPattern.id,
|
||||
interval: 'auto',
|
||||
filters: cloneDeep(searchSource.getOwnField('filter')),
|
||||
} as AppState;
|
||||
if (savedSearch.grid) {
|
||||
defaultState.grid = savedSearch.grid;
|
||||
}
|
||||
if (savedSearch.hideChart) {
|
||||
defaultState.hideChart = savedSearch.hideChart;
|
||||
}
|
||||
|
||||
return defaultState;
|
||||
}
|
|
@ -102,21 +102,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
it('should modify the time range when the histogram is brushed', async function () {
|
||||
// this is the number of renderings of the histogram needed when new data is fetched
|
||||
// this needs to be improved
|
||||
const renderingCountInc = 3;
|
||||
const renderingCountInc = 1;
|
||||
const prevRenderingCount = await elasticChart.getVisualizationRenderingCount();
|
||||
await PageObjects.timePicker.setDefaultAbsoluteRange();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await retry.waitFor('chart rendering complete', async () => {
|
||||
const actualRenderingCount = await elasticChart.getVisualizationRenderingCount();
|
||||
log.debug(`Number of renderings before brushing: ${actualRenderingCount}`);
|
||||
return actualRenderingCount === prevRenderingCount + renderingCountInc;
|
||||
const actualCount = await elasticChart.getVisualizationRenderingCount();
|
||||
const expectedCount = prevRenderingCount + renderingCountInc;
|
||||
log.debug(
|
||||
`renderings before brushing - actual: ${actualCount} expected: ${expectedCount}`
|
||||
);
|
||||
return actualCount === expectedCount;
|
||||
});
|
||||
await PageObjects.discover.brushHistogram();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await retry.waitFor('chart rendering complete after being brushed', async () => {
|
||||
const actualRenderingCount = await elasticChart.getVisualizationRenderingCount();
|
||||
log.debug(`Number of renderings after brushing: ${actualRenderingCount}`);
|
||||
return actualRenderingCount === prevRenderingCount + 6;
|
||||
const actualCount = await elasticChart.getVisualizationRenderingCount();
|
||||
const expectedCount = prevRenderingCount + renderingCountInc * 2;
|
||||
log.debug(
|
||||
`renderings after brushing - actual: ${actualCount} expected: ${expectedCount}`
|
||||
);
|
||||
return actualCount === expectedCount;
|
||||
});
|
||||
const newDurationHours = await PageObjects.timePicker.getTimeDurationInHours();
|
||||
expect(Math.round(newDurationHours)).to.be(26);
|
||||
|
|
|
@ -65,6 +65,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const finalRows = await PageObjects.discover.getDocTableRows();
|
||||
expect(finalRows.length).to.be.above(initialRows.length);
|
||||
expect(finalRows.length).to.be(rowsHardLimit);
|
||||
await PageObjects.discover.backToTop();
|
||||
});
|
||||
|
||||
it('should go the end of the table when using the accessible Skip button', async function () {
|
||||
|
@ -74,6 +75,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const footer = await PageObjects.discover.getDocTableFooter();
|
||||
log.debug(await footer.getVisibleText());
|
||||
expect(await footer.getVisibleText()).to.have.string(rowsHardLimit);
|
||||
await PageObjects.discover.backToTop();
|
||||
});
|
||||
|
||||
describe('expand a document row', function () {
|
||||
|
|
|
@ -217,6 +217,15 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider
|
|||
return skipButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* When scrolling down the legacy table there's a link to scroll up
|
||||
* So this is done by this function
|
||||
*/
|
||||
public async backToTop() {
|
||||
const skipButton = await testSubjects.find('discoverBackToTop');
|
||||
return skipButton.click();
|
||||
}
|
||||
|
||||
public async getDocTableFooter() {
|
||||
return await testSubjects.find('discoverDocTableFooter');
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue