[Discover] Deangularize controller part 1 (#93896) (#95379)

* 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:
Matthias Wilhelm 2021-03-25 09:52:47 +01:00 committed by GitHub
parent ec9b609453
commit cd5b0d6347
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 707 additions and 377 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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());
});
}

View file

@ -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"

View file

@ -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>

View file

@ -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(

View file

@ -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',
};

View file

@ -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' }],

View file

@ -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}
/>

View file

@ -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",
},
]
`);

View file

@ -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',

View file

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

View file

@ -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;
}

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 { 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",
},
}
`);
});

View file

@ -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(),
},
};
}

View file

@ -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';

View file

@ -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: [
{

View file

@ -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]
);

View file

@ -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
*/

View file

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

View file

@ -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;
}

View file

@ -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 [],
}
`);
});
});

View file

@ -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;
}

View file

@ -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);

View file

@ -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 () {

View file

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