[Discover] Add "Hide chart" / "Show chart" persistence (#88603) (#90961)

This commit is contained in:
Matthias Wilhelm 2021-02-10 18:30:11 +01:00 committed by GitHub
parent 747a5fda60
commit bd8642ba76
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 128 additions and 44 deletions

View file

@ -252,6 +252,12 @@ function discoverController($route, $scope, Promise) {
(prop) => !_.isEqual(newStatePartial[prop], oldStatePartial[prop])
);
if (oldStatePartial.hideChart && !newStatePartial.hideChart) {
// in case the histogram is hidden, no data is requested
// so when changing this state data needs to be fetched
changes.push(true);
}
if (changes.length) {
refetch$.next();
}
@ -313,6 +319,8 @@ function discoverController($route, $scope, Promise) {
setAppState,
data,
stateContainer,
searchSessionManager,
refetch$,
};
const inspectorAdapters = ($scope.opts.inspectorAdapters = {
@ -412,6 +420,9 @@ function discoverController($route, $scope, Promise) {
if (savedSearch.grid) {
defaultState.grid = savedSearch.grid;
}
if (savedSearch.hideChart) {
defaultState.hideChart = savedSearch.hideChart;
}
return defaultState;
}
@ -562,13 +573,6 @@ function discoverController($route, $scope, Promise) {
});
};
$scope.handleRefresh = function (_payload, isUpdate) {
if (isUpdate === false) {
searchSessionManager.removeSearchSessionIdFromURL({ replace: false });
refetch$.next();
}
};
function getDimensions(aggs, timeRange) {
const [metric, agg] = aggs;
agg.params.timeRange = timeRange;
@ -601,7 +605,7 @@ function discoverController($route, $scope, Promise) {
function onResults(resp) {
inspectorRequest.stats(getResponseInspectorStats(resp, $scope.searchSource)).ok({ json: resp });
if (getTimeField()) {
if (getTimeField() && !$scope.state.hideChart) {
const tabifiedData = tabifyAggResponse($scope.opts.chartAggConfigs, resp);
$scope.searchSource.rawResponse = resp;
$scope.histogramData = discoverResponseHandler(
@ -704,7 +708,7 @@ function discoverController($route, $scope, Promise) {
async function setupVisualization() {
// If no timefield has been specified we don't create a histogram of messages
if (!getTimeField()) return;
if (!getTimeField() || $scope.state.hideChart) return;
const { interval: histogramInterval } = $scope.state;
const visStateAggs = [

View file

@ -17,7 +17,6 @@
state="state"
time-range="timeRange"
top-nav-menu="topNavMenu"
update-query="handleRefresh"
use-new-fields-api="useNewFieldsApi"
unmapped-fields-config="unmappedFieldsConfig"
>

View file

@ -44,6 +44,10 @@ export interface AppState {
* Data Grid related state
*/
grid?: DiscoverGridSettings;
/**
* Hide chart
*/
hideChart?: boolean;
/**
* id of the used index pattern
*/

View file

@ -25,6 +25,8 @@ import { indexPatternWithTimefieldMock } from '../../__mocks__/index_pattern_wit
import { calcFieldCounts } from '../helpers/calc_field_counts';
import { DiscoverProps } from './types';
import { RequestAdapter } from '../../../../inspector/common';
import { Subject } from 'rxjs';
import { DiscoverSearchSessionManager } from '../angular/discover_search_session';
const mockNavigation = navigationPluginMock.createStartContract();
@ -73,8 +75,10 @@ function getProps(indexPattern: IndexPattern): DiscoverProps {
indexPatternList: (indexPattern as unknown) as Array<SavedObject<IndexPatternAttributes>>,
inspectorAdapters: { requests: {} as RequestAdapter },
navigateTo: jest.fn(),
refetch$: {} as Subject<undefined>,
sampleSize: 10,
savedSearch: savedSearchMock,
searchSessionManager: {} as DiscoverSearchSessionManager,
setHeaderActionMenu: jest.fn(),
timefield: indexPattern.timeFieldName || '',
setAppState: jest.fn(),
@ -86,7 +90,6 @@ function getProps(indexPattern: IndexPattern): DiscoverProps {
rows: esHits,
searchSource: searchSourceMock,
state: { columns: [] },
updateQuery: jest.fn(),
};
}

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import './discover.scss';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import React, { useState, useRef, useMemo, useCallback } from 'react';
import {
EuiButtonEmpty,
EuiButtonIcon,
@ -66,7 +66,6 @@ export function Discover({
searchSource,
state,
timeRange,
updateQuery,
unmappedFieldsConfig,
}: DiscoverProps) {
const [expandedDoc, setExpandedDoc] = useState<ElasticSearchHit | undefined>(undefined);
@ -76,8 +75,11 @@ export function Discover({
// collapse icon isn't displayed in mobile view, use it to detect which view is displayed
return collapseIcon && !collapseIcon.current;
};
const [toggleOn, toggleChart] = useState(true);
const toggleHideChart = useCallback(() => {
const newState = { ...state, hideChart: !state.hideChart };
opts.stateContainer.setAppState(newState);
}, [state, opts]);
const hideChart = useMemo(() => state.hideChart, [state]);
const { savedSearch, indexPatternList, config, services, data, setAppState } = opts;
const { trackUiMetric, capabilities, indexPatterns } = services;
const [isSidebarClosed, setIsSidebarClosed] = useState(false);
@ -89,6 +91,15 @@ export function Discover({
const contentCentered = resultState === 'uninitialized';
const isLegacy = services.uiSettings.get('doc_table:legacy');
const useNewFieldsApi = !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE);
const updateQuery = useCallback(
(_payload, isUpdate?: boolean) => {
if (isUpdate === false) {
opts.searchSessionManager.removeSearchSessionIdFromURL({ replace: false });
opts.refetch$.next();
}
},
[opts]
);
const { onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useMemo(
() =>
@ -192,7 +203,8 @@ export function Discover({
indexPattern={indexPattern}
opts={opts}
onOpenInspector={onOpenInspector}
state={state}
query={state.query}
savedQuery={state.savedQuery}
updateQuery={updateQuery}
/>
<EuiPageBody className="dscPageBody" aria-describedby="savedSearchTitle">
@ -277,7 +289,7 @@ export function Discover({
onResetQuery={resetQuery}
/>
</EuiFlexItem>
{toggleOn && (
{!hideChart && (
<EuiFlexItem className="dscResultCount__actions">
<TimechartHeader
dateFormat={opts.config.get('dateFormat')}
@ -293,13 +305,13 @@ export function Discover({
<EuiFlexItem className="dscResultCount__toggle" grow={false}>
<EuiButtonEmpty
size="xs"
iconType={toggleOn ? 'eyeClosed' : 'eye'}
iconType={!hideChart ? 'eyeClosed' : 'eye'}
onClick={() => {
toggleChart(!toggleOn);
toggleHideChart();
}}
data-test-subj="discoverChartToggle"
>
{toggleOn
{!hideChart
? i18n.translate('discover.hideChart', {
defaultMessage: 'Hide chart',
})
@ -312,7 +324,7 @@ export function Discover({
</EuiFlexGroup>
{isLegacy && <SkipBottomButton onClick={onSkipBottomButtonClick} />}
</EuiFlexItem>
{toggleOn && opts.timefield && (
{!hideChart && opts.timefield && (
<EuiFlexItem grow={false}>
<section
aria-label={i18n.translate(

View file

@ -10,7 +10,7 @@ import React from 'react';
import { mountWithIntl } from '@kbn/test/jest';
import { indexPatternMock } from '../../__mocks__/index_pattern';
import { DiscoverServices } from '../../build_services';
import { AppState, GetStateReturn } from '../angular/discover_state';
import { GetStateReturn } from '../angular/discover_state';
import { savedSearchMock } from '../../__mocks__/saved_search';
import { dataPluginMock } from '../../../../data/public/mocks';
import { createFilterManagerMock } from '../../../../data/public/query/filter_manager/filter_manager.mock';
@ -20,9 +20,11 @@ import { SavedObject } from '../../../../../core/types';
import { DiscoverTopNav, DiscoverTopNavProps } from './discover_topnav';
import { RequestAdapter } from '../../../../inspector/common/adapters/request';
import { TopNavMenu } from '../../../../navigation/public';
import { Query } from '../../../../data/common';
import { DiscoverSearchSessionManager } from '../angular/discover_search_session';
import { Subject } from 'rxjs';
function getProps(): DiscoverTopNavProps {
const state = ({} as unknown) as AppState;
const services = ({
navigation: {
ui: { TopNavMenu },
@ -45,15 +47,18 @@ function getProps(): DiscoverTopNavProps {
indexPatternList: (indexPattern as unknown) as Array<SavedObject<IndexPatternAttributes>>,
inspectorAdapters: { requests: {} as RequestAdapter },
navigateTo: jest.fn(),
refetch$: {} as Subject<undefined>,
sampleSize: 10,
savedSearch: savedSearchMock,
searchSessionManager: {} as DiscoverSearchSessionManager,
services,
setAppState: jest.fn(),
setHeaderActionMenu: jest.fn(),
stateContainer: {} as GetStateReturn,
timefield: indexPattern.timeFieldName || '',
},
state,
query: {} as Query,
savedQuery: '',
updateQuery: jest.fn(),
onOpenInspector: jest.fn(),
};

View file

@ -8,17 +8,21 @@
import React, { useMemo } from 'react';
import { DiscoverProps } from './types';
import { getTopNavLinks } from './top_nav/get_top_nav_links';
import { Query, TimeRange } from '../../../../data/common/query';
export type DiscoverTopNavProps = Pick<
DiscoverProps,
'indexPattern' | 'updateQuery' | 'state' | 'opts'
> & { onOpenInspector: () => void };
export type DiscoverTopNavProps = Pick<DiscoverProps, 'indexPattern' | 'opts'> & {
onOpenInspector: () => void;
query?: Query;
savedQuery?: string;
updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void;
};
export const DiscoverTopNav = ({
indexPattern,
opts,
onOpenInspector,
state,
query,
savedQuery,
updateQuery,
}: DiscoverTopNavProps) => {
const showDatePicker = useMemo(() => indexPattern.isTimeBased(), [indexPattern]);
@ -58,9 +62,9 @@ export const DiscoverTopNav = ({
indexPatterns={[indexPattern]}
onQuerySubmit={updateQuery}
onSavedQueryIdChange={updateSavedQueryId}
query={state.query}
query={query}
setMenuMountPoint={opts.setHeaderActionMenu}
savedQueryId={state.savedQuery}
savedQueryId={savedQuery}
screenTitle={opts.savedSearch.title}
showDatePicker={showDatePicker}
showSaveQuery={!!opts.services.capabilities.discover.saveQuery}

View file

@ -7,6 +7,7 @@
*/
import { IUiSettingsClient, MountPoint, SavedObject } from 'kibana/public';
import { Subject } from 'rxjs';
import { Chart } from '../angular/helpers/point_series';
import { IndexPattern } from '../../../../data/common/index_patterns/index_patterns';
import { ElasticSearchHit } from '../doc_views/doc_views_types';
@ -17,13 +18,12 @@ import {
FilterManager,
IndexPatternAttributes,
ISearchSource,
Query,
TimeRange,
} from '../../../../data/public';
import { SavedSearch } from '../../saved_searches';
import { AppState, GetStateReturn } from '../angular/discover_state';
import { RequestAdapter } from '../../../../inspector/common';
import { DiscoverServices } from '../../build_services';
import { DiscoverSearchSessionManager } from '../angular/discover_search_session';
export interface DiscoverProps {
/**
@ -97,10 +97,18 @@ export interface DiscoverProps {
* List of available index patterns
*/
indexPatternList: Array<SavedObject<IndexPatternAttributes>>;
/**
* Refetch observable
*/
refetch$: Subject<undefined>;
/**
* Kibana core services used by discover
*/
services: DiscoverServices;
/**
* Helps with state management of search session
*/
searchSessionManager: DiscoverSearchSessionManager;
/**
* The number of documents that can be displayed in the table/grid
*/
@ -113,10 +121,6 @@ export interface DiscoverProps {
* Function to set the header menu
*/
setHeaderActionMenu: (menuMount: MountPoint | undefined) => void;
/**
* Functions for retrieving/mutating state
*/
stateContainer: GetStateReturn;
/**
* Timefield of the currently used index pattern
*/
@ -125,6 +129,10 @@ export interface DiscoverProps {
* Function to set the current state
*/
setAppState: (state: Partial<AppState>) => void;
/**
* State container providing globalState, appState and functions
*/
stateContainer: GetStateReturn;
};
/**
* Function to reset the current query
@ -150,10 +158,6 @@ export interface DiscoverProps {
* Currently selected time range
*/
timeRange?: { from: string; to: string };
/**
* Function to update the actual query
*/
updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void;
/**
* An object containing properties for proper handling of unmapped fields in the UI
*/

View file

@ -48,6 +48,9 @@ export async function persistSavedSearch(
if (state.grid) {
savedSearch.grid = state.grid;
}
if (state.hideChart) {
savedSearch.hideChart = state.hideChart;
}
try {
const id = await savedSearch.save(saveOptions);

View file

@ -14,6 +14,7 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) {
public static mapping = {
title: 'text',
description: 'text',
hideChart: 'boolean',
hits: 'integer',
columns: 'keyword',
grid: 'object',
@ -35,6 +36,7 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) {
mapping: {
title: 'text',
description: 'text',
hideChart: 'boolean',
hits: 'integer',
columns: 'keyword',
grid: 'object',

View file

@ -24,6 +24,7 @@ export interface SavedSearch {
lastSavedTitle?: string;
copyOnSave?: boolean;
pre712?: boolean;
hideChart?: boolean;
}
export interface SavedSearchLoader {
get: (id: string) => Promise<SavedSearch>;

View file

@ -34,6 +34,7 @@ export const searchSavedObjectType: SavedObjectsType = {
properties: {
columns: { type: 'keyword', index: false, doc_values: false },
description: { type: 'text' },
hideChart: { type: 'boolean', index: false, doc_values: false },
hits: { type: 'integer', index: false, doc_values: false },
kibanaSavedObjectMeta: {
properties: {

View file

@ -19,6 +19,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
defaultIndex: 'long-window-logstash-*',
'dateFormat:tz': 'Europe/Berlin',
};
const testSubjects = getService('testSubjects');
const browser = getService('browser');
describe('discover histogram', function describeIndexTests() {
before(async () => {
@ -35,11 +37,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await security.testUser.restoreDefaults();
});
async function prepareTest(fromTime: string, toTime: string, interval: string) {
async function prepareTest(fromTime: string, toTime: string, interval?: string) {
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.discover.setChartInterval(interval);
await PageObjects.header.waitUntilLoadingHasFinished();
if (interval) {
await PageObjects.discover.setChartInterval(interval);
await PageObjects.header.waitUntilLoadingHasFinished();
}
}
it('should visualize monthly data with different day intervals', async () => {
@ -65,5 +69,43 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const chartIntervalIconTip = await PageObjects.discover.getChartIntervalWarningIcon();
expect(chartIntervalIconTip).to.be(true);
});
it('should allow hide/show histogram, persisted in url state', async () => {
const fromTime = 'Jan 01, 2010 @ 00:00:00.000';
const toTime = 'Mar 21, 2019 @ 00:00:00.000';
await prepareTest(fromTime, toTime);
let canvasExists = await elasticChart.canvasExists();
expect(canvasExists).to.be(true);
await testSubjects.click('discoverChartToggle');
canvasExists = await elasticChart.canvasExists();
expect(canvasExists).to.be(false);
// histogram is hidden, when reloading the page it should remain hidden
await browser.refresh();
canvasExists = await elasticChart.canvasExists();
expect(canvasExists).to.be(false);
await testSubjects.click('discoverChartToggle');
await PageObjects.header.waitUntilLoadingHasFinished();
canvasExists = await elasticChart.canvasExists();
expect(canvasExists).to.be(true);
});
it('should allow hiding the histogram, persisted in saved search', async () => {
const fromTime = 'Jan 01, 2010 @ 00:00:00.000';
const toTime = 'Mar 21, 2019 @ 00:00:00.000';
const savedSearch = 'persisted hidden histogram';
await prepareTest(fromTime, toTime);
await testSubjects.click('discoverChartToggle');
let canvasExists = await elasticChart.canvasExists();
expect(canvasExists).to.be(false);
await PageObjects.discover.saveSearch(savedSearch);
await PageObjects.header.waitUntilLoadingHasFinished();
canvasExists = await elasticChart.canvasExists();
expect(canvasExists).to.be(false);
await testSubjects.click('discoverChartToggle');
canvasExists = await elasticChart.canvasExists();
expect(canvasExists).to.be(true);
await PageObjects.discover.clickResetSavedSearchButton();
await PageObjects.header.waitUntilLoadingHasFinished();
canvasExists = await elasticChart.canvasExists();
expect(canvasExists).to.be(false);
});
});
}