diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts index 6262855409b29..98ce5fc3b0b2b 100644 --- a/src/plugins/discover/common/index.ts +++ b/src/plugins/discover/common/index.ts @@ -22,4 +22,5 @@ export const MAX_DOC_FIELDS_DISPLAYED = 'discover:maxDocFieldsDisplayed'; export const SHOW_FIELD_STATISTICS = 'discover:showFieldStatistics'; export const SHOW_MULTIFIELDS = 'discover:showMultiFields'; export const TRUNCATE_MAX_HEIGHT = 'truncate:maxHeight'; +export const ROW_HEIGHT_OPTION = 'discover:rowHeightOption'; export const SEARCH_EMBEDDABLE_TYPE = 'search'; diff --git a/src/plugins/discover/public/__mocks__/local_storage_mock.ts b/src/plugins/discover/public/__mocks__/local_storage_mock.ts new file mode 100644 index 0000000000000..42cd33d2eb699 --- /dev/null +++ b/src/plugins/discover/public/__mocks__/local_storage_mock.ts @@ -0,0 +1,26 @@ +/* + * 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 class LocalStorageMock { + private store: Record; + constructor(defaultStore: Record) { + this.store = defaultStore; + } + clear() { + this.store = {}; + } + get(key: string) { + return this.store[key] || null; + } + set(key: string, value: unknown) { + this.store[key] = String(value); + } + remove(key: string) { + delete this.store[key]; + } +} diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index 337d44227139e..e0a76a8617135 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -20,6 +20,7 @@ import { import { UI_SETTINGS } from '../../../data/common'; import { TopNavMenu } from '../../../navigation/public'; import { FORMATS_UI_SETTINGS } from 'src/plugins/field_formats/common'; +import { LocalStorageMock } from './local_storage_mock'; const dataPlugin = dataPluginMock.createStartContract(); export const discoverServiceMock = { @@ -94,8 +95,6 @@ export const discoverServiceMock = { useChartsTheme: jest.fn(() => EUI_CHARTS_THEME_LIGHT.theme), useChartsBaseTheme: jest.fn(() => EUI_CHARTS_THEME_LIGHT.theme), }, - storage: { - get: jest.fn(), - }, + storage: new LocalStorageMock({}) as unknown as Storage, addBasePath: jest.fn(), } as unknown as DiscoverServices; diff --git a/src/plugins/discover/public/__mocks__/ui_settings.ts b/src/plugins/discover/public/__mocks__/ui_settings.ts index be50f73041f8d..7c6a4768b577f 100644 --- a/src/plugins/discover/public/__mocks__/ui_settings.ts +++ b/src/plugins/discover/public/__mocks__/ui_settings.ts @@ -14,6 +14,7 @@ import { SAMPLE_SIZE_SETTING, SHOW_MULTIFIELDS, SEARCH_FIELDS_FROM_SOURCE, + ROW_HEIGHT_OPTION, } from '../../common'; export const uiSettingsMock = { @@ -30,6 +31,8 @@ export const uiSettingsMock = { return false; } else if (key === SHOW_MULTIFIELDS) { return false; + } else if (key === ROW_HEIGHT_OPTION) { + return 3; } }, } as unknown as IUiSettingsClient; diff --git a/src/plugins/discover/public/application/context/context_app_content.test.tsx b/src/plugins/discover/public/application/context/context_app_content.test.tsx index a70770f5d230f..a066dbe0deddb 100644 --- a/src/plugins/discover/public/application/context/context_app_content.test.tsx +++ b/src/plugins/discover/public/application/context/context_app_content.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { findTestSubject } from '@elastic/eui/lib/test'; import { ActionBar } from './components/action_bar/action_bar'; -import { AppState, GetStateReturn } from './services/context_state'; +import { GetStateReturn } from './services/context_state'; import { SortDirection } from 'src/plugins/data/common'; import { ContextAppContent, ContextAppContentProps } from './context_app_content'; import { LoadingStatus } from './services/context_query_state'; @@ -52,7 +52,6 @@ describe('ContextAppContent test', () => { const props = { columns: ['order_date', '_source'], indexPattern: indexPatternMock, - appState: {} as unknown as AppState, stateContainer: {} as unknown as GetStateReturn, anchorStatus: anchorStatus || LoadingStatus.LOADED, predecessorsStatus: LoadingStatus.LOADED, diff --git a/src/plugins/discover/public/application/main/components/document_tour_callout/document_tour_callout.scss b/src/plugins/discover/public/application/main/components/document_tour_callout/document_tour_callout.scss new file mode 100644 index 0000000000000..eb4407bd76e19 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/document_tour_callout/document_tour_callout.scss @@ -0,0 +1,8 @@ +.dscDocumentTourCallout { + .euiCallOutHeader__title { + width: 100%; + } + p { + margin-bottom: 0.5rem; + } +} diff --git a/src/plugins/discover/public/application/main/components/document_tour_callout/document_tour_callout.tsx b/src/plugins/discover/public/application/main/components/document_tour_callout/document_tour_callout.tsx new file mode 100644 index 0000000000000..289672254690f --- /dev/null +++ b/src/plugins/discover/public/application/main/components/document_tour_callout/document_tour_callout.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import './document_tour_callout.scss'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiButton, EuiButtonIcon, EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { Storage } from '../../../../../../kibana_utils/public'; +import { useDiscoverServices } from '../../../../utils/use_discover_services'; + +const CALLOUT_STATE_KEY = 'discover:docTourCalloutClosed'; + +const getStoredCalloutState = (storage: Storage): boolean => { + const calloutClosed = storage.get(CALLOUT_STATE_KEY); + return Boolean(calloutClosed); +}; +const updateStoredCalloutState = (newState: boolean, storage: Storage) => { + storage.set(CALLOUT_STATE_KEY, newState); +}; + +export const DocumentTourCallout = ({ onStartTour }: { onStartTour: () => void }) => { + const { storage, capabilities } = useDiscoverServices(); + const [calloutClosed, setCalloutClosed] = useState(getStoredCalloutState(storage)); + + const onCloseCallout = () => { + updateStoredCalloutState(true, storage); + setCalloutClosed(true); + }; + + if (calloutClosed && capabilities.advancedSettings.save) { + return null; + } + + return ( + } + iconType="tableDensityNormal" + > +

+ +

+

+ { + onStartTour(); + onCloseCallout(); + }} + > + + +

+
+ ); +}; + +function CalloutTitle({ onCloseCallout }: { onCloseCallout: () => void }) { + return ( + + + + + + + + + ); +} diff --git a/src/plugins/discover/public/application/main/components/document_tour_callout/index.ts b/src/plugins/discover/public/application/main/components/document_tour_callout/index.ts new file mode 100644 index 0000000000000..ae72ea3cc583f --- /dev/null +++ b/src/plugins/discover/public/application/main/components/document_tour_callout/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { DocumentTourCallout } from './document_tour_callout'; diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx index 5f9d2d41f862d..b336480cdeb65 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { BehaviorSubject } from 'rxjs'; import { mountWithIntl } from '@kbn/test/jest'; -import { setHeaderActionMenuMounter } from '../../../../kibana_services'; +import { setHeaderActionMenuMounter, setServices } from '../../../../kibana_services'; import { esHits } from '../../../../__mocks__/es_hits'; import { savedSearchMock } from '../../../../__mocks__/saved_search'; import { GetStateReturn } from '../../services/discover_state'; @@ -28,6 +28,7 @@ function mountComponent(fetchStatus: FetchStatus, hits: ElasticSearchHit[]) { services.data.query.timefilter.timefilter.getTime = () => { return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; }; + setServices(services); const documents$ = new BehaviorSubject({ fetchStatus, @@ -43,7 +44,7 @@ function mountComponent(fetchStatus: FetchStatus, hits: ElasticSearchHit[]) { searchSource: documents$, setExpandedDoc: jest.fn(), state: { columns: [] }, - stateContainer: {} as GetStateReturn, + stateContainer: { setAppState: () => {} } as unknown as GetStateReturn, navigateTo: jest.fn(), }; diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index c955157a9f703..ccbb5afacb40f 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -5,13 +5,15 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { useMemo, useCallback, memo } from 'react'; +import React, { useMemo, useCallback, useEffect, memo } from 'react'; import { EuiFlexItem, EuiSpacer, EuiText, EuiLoadingSpinner, EuiScreenReaderOnly, + EuiTourState, + useEuiTour, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { useDiscoverServices } from '../../../../utils/use_discover_services'; @@ -33,6 +35,13 @@ import { useDataState } from '../../utils/use_data_state'; import { DocTableInfinite } from '../../../../components/doc_table/doc_table_infinite'; import { SortPairArr } from '../../../../components/doc_table/lib/get_sort'; import { ElasticSearchHit } from '../../../../types'; +import { DocumentTourCallout } from '../document_tour_callout'; +import { + DiscoverTourDetails, + tourConfig, + discoverTourSteps, + STORAGE_KEY, +} from '../../../../components/discover_grid/discover_grid_tour'; const DocTableInfiniteMemoized = React.memo(DocTableInfinite); const DataGridMemoized = React.memo(DiscoverGrid); @@ -78,6 +87,25 @@ function DiscoverDocumentsComponent({ useNewFieldsApi, }); + const initialState = localStorage.getItem(STORAGE_KEY); + let tourState: EuiTourState; + if (initialState) { + tourState = JSON.parse(initialState); + tourState = { ...tourState, isTourActive: false }; + } else { + tourState = tourConfig; + } + const [steps, actions, reducerState] = useEuiTour(discoverTourSteps, tourState); + const discoverTour = { steps, actions, state: reducerState } as DiscoverTourDetails; + + useEffect(() => { + localStorage.setItem(STORAGE_KEY, JSON.stringify(discoverTour.state)); + }, [discoverTour.state]); + + const onStartTour = () => { + discoverTour.actions.resetTour(); + }; + const onResize = useCallback( (colSettings: { columnId: string; width: number }) => { const grid = { ...state.grid } || {}; @@ -98,6 +126,13 @@ function DiscoverDocumentsComponent({ [stateContainer] ); + const onUpdateRowHeight = useCallback( + (newRowHeight?: number) => { + stateContainer.setAppState({ rowHeight: newRowHeight }); + }, + [stateContainer] + ); + const showTimeCol = useMemo( () => !uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName, [uiSettings, indexPattern.timeFieldName] @@ -144,30 +179,36 @@ function DiscoverDocumentsComponent({ /> )} {!isLegacy && ( -
- -
+ <> + +
+ +
+ )} ); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx index b258987e3ea30..3549c493228df 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { Subject, BehaviorSubject } from 'rxjs'; import { mountWithIntl } from '@kbn/test/jest'; -import { setHeaderActionMenuMounter } from '../../../../kibana_services'; +import { setHeaderActionMenuMounter, setServices } from '../../../../kibana_services'; import { DiscoverLayout, SIDEBAR_CLOSED_KEY } from './discover_layout'; import { esHits } from '../../../../__mocks__/es_hits'; import { indexPatternMock } from '../../../../__mocks__/index_pattern'; @@ -35,12 +35,21 @@ import { ElasticSearchHit } from '../../../../types'; import { KibanaContextProvider } from '../../../../../../kibana_react/public'; import { FieldFormatsStart } from '../../../../../../field_formats/public'; import { IUiSettingsClient } from 'kibana/public'; +import { DiscoverServices } from 'src/plugins/discover/public/build_services'; +import { LocalStorageMock } from 'src/plugins/discover/public/__mocks__/local_storage_mock'; setHeaderActionMenuMounter(jest.fn()); function mountComponent(indexPattern: DataView, prevSidebarClosed?: boolean) { const searchSourceMock = createSearchSourceMock({}); - const services = discoverServiceMock; + const services = { + ...discoverServiceMock, + fieldFormats: { + getDefaultInstance: jest.fn(() => ({ convert: (value: unknown) => value })), + getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })), + }, + storage: new LocalStorageMock({ [SIDEBAR_CLOSED_KEY]: wasSidebarClosed }) as unknown as Storage, + } as unknown as DiscoverServices; services.data.query.timefilter.timefilter.getAbsoluteTime = () => { return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; }; @@ -146,7 +155,7 @@ function mountComponent(indexPattern: DataView, prevSidebarClosed?: boolean) { savedSearchRefetch$: new Subject(), searchSource: searchSourceMock, state: { columns: [] }, - stateContainer: {} as GetStateReturn, + stateContainer: { setAppState: () => {} } as unknown as GetStateReturn, setExpandedDoc: jest.fn(), }; diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index 5601596a4d73b..dc8f19900b242 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -198,6 +198,29 @@ export function DiscoverLayout({ filterManager.setFilters(disabledFilters); }, [filterManager]); + const clickEvent = useMemo(() => { + return new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true, + buttons: 1, + }); + }, []); + const onOpenDatePicker = useCallback(() => { + const element = document.querySelector( + 'button[data-test-subj="superDatePickerToggleQuickMenuButton"]' + ); + if (element) { + element.dispatchEvent(clickEvent); + } + }, [clickEvent]); + const onEditSearch = useCallback(() => { + const element = document.querySelector('textarea[data-test-subj="queryInput"]'); + if (element) { + element.dispatchEvent(clickEvent); + } + }, [clickEvent]); + const toggleSidebarCollapse = useCallback(() => { storage.set(SIDEBAR_CLOSED_KEY, !isSidebarClosed); setIsSidebarClosed(!isSidebarClosed); @@ -287,6 +310,8 @@ export function DiscoverLayout({ state.filters && state.filters.filter((f) => !f.meta.disabled).length > 0 } onDisableFilters={onDisableFilters} + onEditSearch={onEditSearch} + onOpenDatePicker={onOpenDatePicker} /> )} {resultState === 'uninitialized' && ( diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results.tsx index aaaad49b1f611..d6ce8683b3ba4 100644 --- a/src/plugins/discover/public/application/main/components/no_results/no_results.tsx +++ b/src/plugins/discover/public/application/main/components/no_results/no_results.tsx @@ -15,9 +15,12 @@ import { EuiFlexItem, EuiSpacer, EuiTitle, + EuiTourStep, + EuiLink, + EuiText, } from '@elastic/eui'; import { DataPublicPluginStart } from '../../../../../../data/public'; -import { AdjustSearch, getTimeFieldMessage } from './no_results_helper'; +import { AdjustSearch } from './no_results_helper'; import './_no_results.scss'; import { NoResultsIllustration } from './assets/no_results_illustration'; @@ -28,6 +31,8 @@ export interface DiscoverNoResultsProps { hasQuery?: boolean; hasFilters?: boolean; onDisableFilters: () => void; + onOpenDatePicker: () => void; + onEditSearch: () => void; } export function DiscoverNoResults({ @@ -37,6 +42,8 @@ export function DiscoverNoResults({ hasFilters, hasQuery, onDisableFilters, + onOpenDatePicker, + onEditSearch, }: DiscoverNoResultsProps) { const callOut = !error ? ( @@ -54,12 +61,14 @@ export function DiscoverNoResults({ - {isTimeBased && getTimeFieldMessage()} - {(hasFilters || hasQuery) && ( + {(hasFilters || hasQuery || isTimeBased) && ( )} diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results_helper.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results_helper.tsx index b5a52d40e1939..e05c71b1b6340 100644 --- a/src/plugins/discover/public/application/main/components/no_results/no_results_helper.tsx +++ b/src/plugins/discover/public/application/main/components/no_results/no_results_helper.tsx @@ -16,36 +16,51 @@ import { EuiSpacer, } from '@elastic/eui'; -export function getTimeFieldMessage() { - return ( - - - - - - - - - - - ); -} - interface AdjustSearchProps { onDisableFilters: () => void; + onOpenDatePicker: () => void; + onEditSearch: () => void; + isTimeBased?: boolean; hasFilters?: boolean; hasQuery?: boolean; } -export function AdjustSearch({ hasFilters, hasQuery, onDisableFilters }: AdjustSearchProps) { +export function AdjustSearch({ + isTimeBased, + hasFilters, + hasQuery, + onDisableFilters, + onOpenDatePicker, + onEditSearch, +}: AdjustSearchProps) { return ( + {isTimeBased && ( + + + + + + + + + ), + }} + /> + + + )} {hasQuery && ( <> @@ -59,7 +74,20 @@ export function AdjustSearch({ hasFilters, hasQuery, onDisableFilters }: AdjustS + + + ), + }} /> @@ -78,7 +106,7 @@ export function AdjustSearch({ hasFilters, hasQuery, onDisableFilters }: AdjustS (null); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [isCalloutOpen, setIsCalloutOpen] = useState(true); + + const onButtonClick = () => setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen); + const closePopover = () => setIsPopoverOpen(false); + const closeCallout = () => { + setIsCalloutOpen(false); + setIsPopoverOpen(false); + }; useEffect(() => { if (documents) { @@ -306,6 +323,74 @@ export function DiscoverSidebarComponent({ ); } + const calloutClasses = classNames('dscLearnCallout', { hide: !isCalloutOpen }); + const callout = ( + + + + + Learn about field types + + + + + + + + ); + + const columnsSidebar = [ + { + field: 'type', + name: 'Icon', + width: '40px', + render: (name) => , + }, + { + field: 'dataType', + name: 'Data type', + width: '70px', + }, + { + field: 'description', + name: 'Description', + }, + ]; + + const items = [ + { + id: 0, + dataType: 'text', + type: 'tokenString', + description: 'Full text such as the body of an email or a product description.', + }, + { + id: 1, + dataType: 'number', + type: 'tokenNumber', + description: 'Long, integer, short, byte, double, and float values.', + }, + { + id: 2, + dataType: 'keyword', + type: 'tokenKeyword', + description: + 'Structured content such as an ID, email address, hostname, status code, or tag.', + }, + { + id: 3, + dataType: 'date', + type: 'tokenDate', + description: 'A date string or the number of seconds or milliseconds since 1/1/1970.', + }, + { + id: 4, + dataType: 'geo_point', + type: 'tokenGeo', + description: 'Latitude and longitude points.', + }, + ]; + return ( + + + Field types + + + +
{ diff --git a/src/plugins/discover/public/application/main/services/discover_state.ts b/src/plugins/discover/public/application/main/services/discover_state.ts index c1587b0d9ebff..ad448702aba40 100644 --- a/src/plugins/discover/public/application/main/services/discover_state.ts +++ b/src/plugins/discover/public/application/main/services/discover_state.ts @@ -82,6 +82,10 @@ export interface AppState { * Hide mini distribution/preview charts when in Field Statistics mode */ hideAggregatedPreview?: boolean; + /** + * Document explorer row height option + */ + rowHeight?: number; } interface GetStateParams { diff --git a/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts b/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts index 0d38b1997716b..df1a8c92f7355 100644 --- a/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts +++ b/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts @@ -36,6 +36,7 @@ describe('getStateDefaults', () => { "index": "index-pattern-with-timefield-id", "interval": "auto", "query": undefined, + "rowHeight": 3, "savedQuery": undefined, "sort": Array [ Array [ @@ -68,6 +69,7 @@ describe('getStateDefaults', () => { "index": "the-index-pattern-id", "interval": "auto", "query": undefined, + "rowHeight": 3, "savedQuery": undefined, "sort": Array [], "viewMode": undefined, diff --git a/src/plugins/discover/public/application/main/utils/get_state_defaults.ts b/src/plugins/discover/public/application/main/utils/get_state_defaults.ts index 10ef04ca8643e..807988ddf36d2 100644 --- a/src/plugins/discover/public/application/main/utils/get_state_defaults.ts +++ b/src/plugins/discover/public/application/main/utils/get_state_defaults.ts @@ -63,6 +63,7 @@ export function getStateDefaults({ viewMode: undefined, hideAggregatedPreview: undefined, savedQuery: undefined, + rowHeight: undefined, } as AppState; if (savedSearch.grid) { defaultState.grid = savedSearch.grid; @@ -70,6 +71,9 @@ export function getStateDefaults({ if (savedSearch.hideChart !== undefined) { defaultState.hideChart = savedSearch.hideChart; } + if (savedSearch.rowHeight !== undefined) { + defaultState.rowHeight = savedSearch.rowHeight; + } if (savedSearch.viewMode) { defaultState.viewMode = savedSearch.viewMode; } diff --git a/src/plugins/discover/public/application/main/utils/persist_saved_search.ts b/src/plugins/discover/public/application/main/utils/persist_saved_search.ts index 95d7421b83f42..60a9b0b7d5fcd 100644 --- a/src/plugins/discover/public/application/main/utils/persist_saved_search.ts +++ b/src/plugins/discover/public/application/main/utils/persist_saved_search.ts @@ -51,6 +51,9 @@ export async function persistSavedSearch( if (typeof state.hideChart !== 'undefined') { savedSearch.hideChart = state.hideChart; } + if (typeof state.rowHeight !== 'undefined') { + savedSearch.rowHeight = state.rowHeight; + } if (state.viewMode) { savedSearch.viewMode = state.viewMode; diff --git a/src/plugins/discover/public/application/main/utils/use_discover_state.test.ts b/src/plugins/discover/public/application/main/utils/use_discover_state.test.ts index e77ea5787705f..bac6d085acf05 100644 --- a/src/plugins/discover/public/application/main/utils/use_discover_state.test.ts +++ b/src/plugins/discover/public/application/main/utils/use_discover_state.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react-hooks'; import { createSearchSessionMock } from '../../../__mocks__/search_session'; import { discoverServiceMock } from '../../../__mocks__/services'; import { savedSearchMock } from '../../../__mocks__/saved_search'; @@ -42,6 +42,50 @@ describe('test useDiscoverState', () => { }); expect(result.current.state.index).toBe(indexPatternMock.id); expect(result.current.stateContainer).toBeInstanceOf(Object); + expect(result.current.setState).toBeInstanceOf(Function); expect(result.current.searchSource).toBeInstanceOf(SearchSource); }); + + test('setState', async () => { + const { history } = createSearchSessionMock(); + + const { result } = renderHook(() => { + return useDiscoverState({ + services: discoverServiceMock, + history, + savedSearch: savedSearchMock, + setExpandedDoc: jest.fn(), + }); + }); + await act(async () => { + result.current.setState({ columns: ['123'] }); + }); + expect(result.current.state.columns).toEqual(['123']); + }); + + test('resetSavedSearch', async () => { + const { history } = createSearchSessionMock(); + + const { result, waitForValueToChange } = renderHook(() => { + return useDiscoverState({ + services: discoverServiceMock, + history, + savedSearch: savedSearchMock, + setExpandedDoc: jest.fn(), + }); + }); + + const initialColumns = result.current.state.columns; + await act(async () => { + result.current.setState({ columns: ['123'] }); + }); + expect(result.current.state.columns).toEqual(['123']); + + result.current.resetSavedSearch('the-saved-search-id'); + await waitForValueToChange(() => { + return result.current.state; + }); + + expect(result.current.state.columns).toEqual(initialColumns); + }); }); diff --git a/src/plugins/discover/public/assets/expand_document.gif b/src/plugins/discover/public/assets/expand_document.gif new file mode 100644 index 0000000000000..dde047d6fd1ec Binary files /dev/null and b/src/plugins/discover/public/assets/expand_document.gif differ diff --git a/src/plugins/discover/public/assets/reorder_columns.gif b/src/plugins/discover/public/assets/reorder_columns.gif new file mode 100644 index 0000000000000..d3aeedb513c1e Binary files /dev/null and b/src/plugins/discover/public/assets/reorder_columns.gif differ diff --git a/src/plugins/discover/public/assets/rows_per_line.gif b/src/plugins/discover/public/assets/rows_per_line.gif new file mode 100644 index 0000000000000..66033d03d8fd2 Binary files /dev/null and b/src/plugins/discover/public/assets/rows_per_line.gif differ diff --git a/src/plugins/discover/public/components/discover_grid/constants.ts b/src/plugins/discover/public/components/discover_grid/constants.ts index 80900c06d4932..d026607aef373 100644 --- a/src/plugins/discover/public/components/discover_grid/constants.ts +++ b/src/plugins/discover/public/components/discover_grid/constants.ts @@ -6,14 +6,16 @@ * Side Public License, v 1. */ +import { EuiDataGridStyle } from '@elastic/eui'; + // data types export const kibanaJSON = 'kibana-json'; -export const gridStyle = { +export const GRID_STYLE = { border: 'all', fontSize: 's', cellPadding: 's', rowHover: 'none', -}; +} as EuiDataGridStyle; export const pageSizeArr = [25, 50, 100, 250]; export const defaultPageSize = 100; @@ -23,7 +25,6 @@ export const toolbarVisibility = { allowHide: false, allowReorder: true, }, - showDisplaySelector: false, }; export const defaultMonacoEditorWidth = 370; diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid.tsx index 4307afbeb9e38..82a2b305e513b 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid.tsx @@ -11,7 +11,6 @@ import { FormattedMessage } from '@kbn/i18n-react'; import './discover_grid.scss'; import { EuiDataGridSorting, - EuiDataGridStyle, EuiDataGridProps, EuiDataGrid, EuiScreenReaderOnly, @@ -20,6 +19,7 @@ import { htmlIdGenerator, EuiLoadingSpinner, EuiIcon, + EuiTourStep, } from '@elastic/eui'; import { flattenHit, DataView } from '../../../../data/common'; import { DocViewFilterFn } from '../../services/doc_views/doc_views_types'; @@ -35,7 +35,7 @@ import { } from './discover_grid_columns'; import { defaultPageSize, - gridStyle, + GRID_STYLE, pageSizeArr, toolbarVisibility as toolbarVisibilityDefaults, } from './constants'; @@ -50,6 +50,8 @@ import { SortPairArr } from '../doc_table/lib/get_sort'; import { getFieldsToShow } from '../../utils/get_fields_to_show'; import { ElasticSearchHit } from '../../types'; import { useDiscoverServices } from '../../utils/use_discover_services'; +import { useRowHeightsOptions } from '../../utils/use_row_heights_options'; +import { DiscoverTourDetails, buttomButtons } from './discover_grid_tour'; interface SortObj { id: string; @@ -154,6 +156,18 @@ export interface DiscoverGridProps { * List of used control columns (available: 'openDetails', 'select') */ controlColumnIds?: string[]; + /** + * Row height from state + */ + rowHeightState?: number; + /** + * The state of the Discover tour + */ + tour: DiscoverTourDetails; + /** + * Update row height state + */ + onUpdateRowHeight?: (rowHeight: number) => void; } export const EuiDataGridMemoized = React.memo((props: EuiDataGridProps) => { @@ -187,10 +201,14 @@ export const DiscoverGrid = ({ isPaginationEnabled = true, controlColumnIds = CONTROL_COLUMN_IDS_DEFAULT, className, + rowHeightState, + onUpdateRowHeight, + tour, }: DiscoverGridProps) => { const services = useDiscoverServices(); const [selectedDocs, setSelectedDocs] = useState([]); const [isFilterActive, setIsFilterActive] = useState(false); + const displayedColumns = getDisplayedColumns(columns, indexPattern); const defaultColumns = displayedColumns.includes('_source'); const usedSelectedDocs = useMemo(() => { @@ -221,6 +239,17 @@ export const DiscoverGrid = ({ return rowsFiltered; }, [rows, usedSelectedDocs, isFilterActive]); + const showDisplaySelector = useMemo( + () => + !!onUpdateRowHeight + ? { + allowDensity: false, + allowRowHeight: true, + } + : undefined, + [onUpdateRowHeight] + ); + /** * Pagination */ @@ -331,8 +360,8 @@ export const DiscoverGrid = ({ return { columns: sortingColumns, onSort: () => {} }; }, [sortingColumns, onTableSort, isSortEnabled]); const lead = useMemo( - () => getLeadControlColumns().filter(({ id }) => controlColumnIds.includes(id)), - [controlColumnIds] + () => getLeadControlColumns(tour).filter(({ id }) => controlColumnIds.includes(id)), + [controlColumnIds, tour] ); const additionalControls = useMemo( @@ -357,18 +386,56 @@ export const DiscoverGrid = ({ showColumnSelector: false, showSortSelector: isSortEnabled, additionalControls, + showDisplaySelector, } : { ...toolbarVisibilityDefaults, showSortSelector: isSortEnabled, - additionalControls, + additionalControls: { + left: { + append: additionalControls, + prepend: ( + +
+ {' '} +
+
+ ), + }, + right: ( + +
+ {' '} +
+
+ ), + }, + showDisplaySelector, }, - [defaultColumns, additionalControls, isSortEnabled] + [defaultColumns, additionalControls, isSortEnabled, tour, showDisplaySelector] ); + const rowHeightsOptions = useRowHeightsOptions({ + rowHeightState, + onUpdateRowHeight, + storage: services.storage, + uiSettings: services.uiSettings, + }); + if (!rowCount && isLoading) { return (
+ + @@ -423,7 +490,6 @@ export const DiscoverGrid = ({ columns={euiGridColumns} columnVisibility={columnsVisibility} data-test-subj="docTable" - gridStyle={gridStyle as EuiDataGridStyle} leadingControlColumns={lead} onColumnResize={onResize} pagination={paginationObj} @@ -432,10 +498,12 @@ export const DiscoverGrid = ({ schemaDetectors={schemaDetectors} sorting={sorting as EuiDataGridSorting} toolbarVisibility={toolbarVisibility} + rowHeightsOptions={rowHeightsOptions} + gridStyle={GRID_STYLE} /> - {showDisclaimer && (

+ Footer )} {expandedDoc && ( - setExpandedDoc(undefined)} - setExpandedDoc={setExpandedDoc} - /> + + setExpandedDoc(undefined)} + setExpandedDoc={setExpandedDoc} + services={services} + /> + )} diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx index 5e4ec7a4f9629..9db4042d36628 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx @@ -8,7 +8,12 @@ import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiDataGridColumn, EuiIconTip, EuiScreenReaderOnly } from '@elastic/eui'; +import { + EuiDataGridColumn, + EuiIconTip, + EuiScreenReaderOnly, + EuiDataGridCellValueElementProps, +} from '@elastic/eui'; import { ExpandButton } from './discover_grid_expand_button'; import { DiscoverGridSettings } from './types'; import type { DataView } from '../../../../data/common'; @@ -16,8 +21,9 @@ import { buildCellActions } from './discover_grid_cell_actions'; import { getSchemaByKbnType } from './discover_grid_schema'; import { SelectButton } from './discover_grid_document_selection'; import { defaultTimeColumnWidth } from './constants'; +import { DiscoverTourDetails } from './discover_grid_tour'; -export function getLeadControlColumns() { +export function getLeadControlColumns(tour: DiscoverTourDetails) { return [ { id: 'openDetails', @@ -31,7 +37,12 @@ export function getLeadControlColumns() { ), - rowCellRender: ExpandButton, + rowCellRender: ({ rowIndex, setCellProps }: EuiDataGridCellValueElementProps) => { + return ExpandButton(tour, { + rowIndex, + setCellProps, + } as EuiDataGridCellValueElementProps); + }, }, { id: 'select', diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx index 6765a8d24f91a..9d7071b400b20 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx @@ -7,15 +7,25 @@ */ import React, { useContext, useEffect } from 'react'; -import { EuiButtonIcon, EuiDataGridCellValueElementProps, EuiToolTip } from '@elastic/eui'; +import { + EuiButtonIcon, + EuiDataGridCellValueElementProps, + EuiToolTip, + EuiTourStep, +} from '@elastic/eui'; import { euiLightVars as themeLight, euiDarkVars as themeDark } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; import { DiscoverGridContext } from './discover_grid_context'; import { EsHitRecord } from '../../application/types'; +import { buttomButtons, DiscoverTourDetails } from './discover_grid_tour'; + /** * Button to expand a given row */ -export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueElementProps) => { +export const ExpandButton = ( + tour: DiscoverTourDetails, + { rowIndex, setCellProps }: EuiDataGridCellValueElementProps +) => { const { expanded, setExpanded, rows, isDarkMode } = useContext(DiscoverGridContext); const current = rows[rowIndex]; useEffect(() => { @@ -39,7 +49,7 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle defaultMessage: 'Toggle dialog with details', }); - return ( + const toolTipIcon = ( ); + if (rowIndex === 0) { + return ( + + {toolTipIcon} + + ); + } + return toolTipIcon; }; diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx index 371eb014eab8f..a1e5937150155 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx @@ -15,6 +15,7 @@ import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, + EuiIconTip, EuiTitle, EuiButtonEmpty, EuiText, @@ -159,19 +160,42 @@ export function DiscoverGridFlyout({ {indexPattern.isTimeBased() && indexPattern.id && ( - - - {i18n.translate('discover.grid.tableRow.viewSurroundingDocumentsLinkTextSimple', { - defaultMessage: 'Surrounding documents', - })} - - + + + + {i18n.translate( + 'discover.grid.tableRow.viewSurroundingDocumentsLinkTextSimple', + { + defaultMessage: 'Surrounding documents', + } + )} + + + + + + )} {activePage !== -1 && ( diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_tour.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_tour.tsx new file mode 100644 index 0000000000000..48aff97fb2ffd --- /dev/null +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_tour.tsx @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import { + EuiButton, + EuiButtonEmpty, + EuiIcon, + EuiImage, + EuiText, + EuiTourState, + EuiStatelessTourStep, + EuiTourStepProps, + EuiFlexGroup, + EuiFlexItem, + EuiTourActions, +} from '@elastic/eui'; + +import expandDocumentGif from '../../assets/expand_document.gif'; +import reorderColumnsGif from '../../assets/reorder_columns.gif'; +import rowsPerLineGif from '../../assets/rows_per_line.gif'; + +export interface DiscoverTourDetails { + steps: EuiTourStepProps[]; + actions: EuiTourActions; + state: EuiTourState; +} + +export const discoverTourSteps = [ + { + step: 1, + title: 'Expand a document', + content: ( + +

+ Click the button to see a document in detail. +

+ +
+ ), + maxWidth: 350, + } as EuiStatelessTourStep, + { + step: 2, + title: 'Reorder your columns', + content: ( + +

+ Click Columns and then drag to the desired order. +

+ +
+ ), + maxWidth: 350, + anchorPosition: 'downLeft', + } as EuiStatelessTourStep, + { + step: 3, + title: 'Adjust the row height', + content: ( + +

+ Click to set the row height to 1 or more lines, or + automatically adjust the height to fit the contents. +

+ +
+ ), + maxWidth: 350, + anchorPosition: 'leftCenter', + } as EuiStatelessTourStep, +] as EuiStatelessTourStep[]; + +export const tourConfig = { + currentTourStep: 1, + isTourActive: false, + tourPopoverWidth: 350, + tourSubtitle: '', +}; + +export const STORAGE_KEY = 'discover.demoTour'; + +export const buttomButtons = (tour: DiscoverTourDetails) => { + let emptyButton; + let mainButton; + if (tour.state.currentTourStep === discoverTourSteps.length) { + emptyButton = { + properties: { + target: '_blank', + href: 'https://github.com/elastic/kibana/issues/new/choose', + }, + text: ( + <> + {'Give feedback'} + + ), + }; + mainButton = { + properties: { + onClick: () => { + tour.actions.finishTour(); + }, + }, + text: 'Finish tour', + }; + } else { + emptyButton = { + properties: { + onClick: () => { + tour.actions.finishTour(); + }, + }, + text: 'Skip tour', + }; + mainButton = { + properties: { + onClick: () => { + tour.actions.incrementStep(); + }, + }, + text: 'Next', + }; + } + + return ( + + + + {emptyButton.text} + + + + + {mainButton.text} + + + + ); +}; diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx index 5e0f06f143a0c..b950e42fb5f22 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx @@ -62,6 +62,7 @@ export type SearchProps = Partial & hits?: ElasticSearchHit[]; totalHitCount?: number; onMoveColumn?: (column: string, index: number) => void; + onUpdateRowHeight?: (rowHeight?: number) => void; }; interface SearchEmbeddableConfig { @@ -293,6 +294,10 @@ export class SavedSearchEmbeddable useNewFieldsApi: !this.services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE, false), showTimeCol: !this.services.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false), ariaLabelledBy: 'documentsAriaLabel', + rowHeightState: this.input.rowHeight || this.savedSearch.rowHeight, + onUpdateRowHeight: (rowHeight) => { + this.updateInput({ rowHeight }); + }, }; const timeRangeSearchSource = searchSource.create(); @@ -342,6 +347,7 @@ export class SavedSearchEmbeddable ); searchProps.sort = this.input.sort || savedSearchSort; searchProps.sharedItemTitle = this.panelTitle; + searchProps.rowHeightState = this.input.rowHeight || this.savedSearch.rowHeight; if (forceFetch || isFetchRequired) { this.filtersSearchSource.setField('filter', this.input.filters); this.filtersSearchSource.setField('query', this.input.query); @@ -414,9 +420,9 @@ export class SavedSearchEmbeddable } const useLegacyTable = this.services.uiSettings.get(DOC_TABLE_LEGACY); const props = { + savedSearch: this.savedSearch, searchProps, useLegacyTable, - refs: domNode, }; if (searchProps.services) { ReactDOM.render( diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable_component.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable_component.tsx index 5d7907d3cf791..46681a5d9ec40 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable_component.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable_component.tsx @@ -16,7 +16,6 @@ import { SearchProps } from './saved_search_embeddable'; interface SavedSearchEmbeddableComponentProps { searchProps: SearchProps; useLegacyTable: boolean; - refs: HTMLElement; } const DiscoverDocTableEmbeddableMemoized = React.memo(DiscoverDocTableEmbeddable); @@ -25,15 +24,14 @@ const DiscoverGridEmbeddableMemoized = React.memo(DiscoverGridEmbeddable); export function SavedSearchEmbeddableComponent({ searchProps, useLegacyTable, - refs, }: SavedSearchEmbeddableComponentProps) { if (useLegacyTable) { - const docTableProps = { - ...searchProps, - refs, - }; - return ; + return ; } - const discoverGridProps = searchProps as DiscoverGridEmbeddableProps; - return ; + return ( + + ); } diff --git a/src/plugins/discover/public/embeddable/types.ts b/src/plugins/discover/public/embeddable/types.ts index 509d9644f8735..32c029cccf7b1 100644 --- a/src/plugins/discover/public/embeddable/types.ts +++ b/src/plugins/discover/public/embeddable/types.ts @@ -23,6 +23,7 @@ export interface SearchInput extends EmbeddableInput { hidePanelTitles?: boolean; columns?: string[]; sort?: SortOrder[]; + rowHeight?: number; } export interface SearchOutput extends EmbeddableOutput { diff --git a/src/plugins/discover/public/services/saved_searches/get_saved_searches.test.ts b/src/plugins/discover/public/services/saved_searches/get_saved_searches.test.ts index 1159320c9a09f..13254aa7b077e 100644 --- a/src/plugins/discover/public/services/saved_searches/get_saved_searches.test.ts +++ b/src/plugins/discover/public/services/saved_searches/get_saved_searches.test.ts @@ -104,6 +104,7 @@ describe('getSavedSearch', () => { "hideAggregatedPreview": undefined, "hideChart": false, "id": "ccf1af80-2297-11ec-86e0-1155ffb9c7a7", + "rowHeight": undefined, "searchSource": Object { "create": [MockFunction], "createChild": [MockFunction], diff --git a/src/plugins/discover/public/services/saved_searches/saved_searches_utils.test.ts b/src/plugins/discover/public/services/saved_searches/saved_searches_utils.test.ts index f2ad8b92adbc8..13739f0b1f4d1 100644 --- a/src/plugins/discover/public/services/saved_searches/saved_searches_utils.test.ts +++ b/src/plugins/discover/public/services/saved_searches/saved_searches_utils.test.ts @@ -57,6 +57,7 @@ describe('saved_searches_utils', () => { "hideAggregatedPreview": undefined, "hideChart": true, "id": "id", + "rowHeight": undefined, "searchSource": SearchSource { "dependencies": Object { "getConfig": [MockFunction], @@ -130,6 +131,7 @@ describe('saved_searches_utils', () => { "kibanaSavedObjectMeta": Object { "searchSourceJSON": "{}", }, + "rowHeight": undefined, "sort": Array [ Array [ "a", diff --git a/src/plugins/discover/public/services/saved_searches/saved_searches_utils.ts b/src/plugins/discover/public/services/saved_searches/saved_searches_utils.ts index 064ee6afe0e99..4dbb84613ead8 100644 --- a/src/plugins/discover/public/services/saved_searches/saved_searches_utils.ts +++ b/src/plugins/discover/public/services/saved_searches/saved_searches_utils.ts @@ -43,6 +43,7 @@ export const fromSavedSearchAttributes = ( hideChart: attributes.hideChart, viewMode: attributes.viewMode, hideAggregatedPreview: attributes.hideAggregatedPreview, + rowHeight: attributes.rowHeight, }); export const toSavedSearchAttributes = ( @@ -58,4 +59,5 @@ export const toSavedSearchAttributes = ( hideChart: savedSearch.hideChart ?? false, viewMode: savedSearch.viewMode, hideAggregatedPreview: savedSearch.hideAggregatedPreview, + rowHeight: savedSearch.rowHeight, }); diff --git a/src/plugins/discover/public/services/saved_searches/types.ts b/src/plugins/discover/public/services/saved_searches/types.ts index 4247f68ba8194..9fa579048d4af 100644 --- a/src/plugins/discover/public/services/saved_searches/types.ts +++ b/src/plugins/discover/public/services/saved_searches/types.ts @@ -25,6 +25,7 @@ export interface SavedSearchAttributes { }; viewMode?: VIEW_MODE; hideAggregatedPreview?: boolean; + rowHeight?: number; } /** @internal **/ @@ -49,4 +50,5 @@ export interface SavedSearch { }; viewMode?: VIEW_MODE; hideAggregatedPreview?: boolean; + rowHeight?: number; } diff --git a/src/plugins/discover/public/utils/use_row_heights_options.test.ts b/src/plugins/discover/public/utils/use_row_heights_options.test.ts new file mode 100644 index 0000000000000..8481c354f5f21 --- /dev/null +++ b/src/plugins/discover/public/utils/use_row_heights_options.test.ts @@ -0,0 +1,78 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { Storage } from '../../../kibana_utils/public'; +import { DiscoverServices } from '../build_services'; +import { LocalStorageMock } from '../__mocks__/local_storage_mock'; +import { uiSettingsMock } from '../__mocks__/ui_settings'; +import { useRowHeightsOptions } from './use_row_heights_options'; + +const CONFIG_ROW_HEIGHT = 3; + +describe('useRowHeightsOptions', () => { + test('should apply rowHeight from savedSearch', () => { + const { result } = renderHook(() => { + return useRowHeightsOptions({ + rowHeightState: 2, + uiSettings: uiSettingsMock, + storage: new LocalStorageMock({}) as unknown as Storage, + }); + }); + + expect(result.current.defaultHeight).toEqual({ lineCount: 2 }); + }); + + test('should apply rowHeight from local storage', () => { + const { result } = renderHook(() => { + return useRowHeightsOptions({ + uiSettings: uiSettingsMock, + storage: new LocalStorageMock({ + ['discover:dataGridRowHeight']: { + previousRowHeight: 5, + previousConfigRowHeight: 3, + }, + }) as unknown as Storage, + }); + }); + + expect(result.current.defaultHeight).toEqual({ lineCount: 5 }); + }); + + test('should apply rowHeight from uiSettings', () => { + const { result } = renderHook(() => { + return useRowHeightsOptions({ + uiSettings: uiSettingsMock, + storage: new LocalStorageMock({}) as unknown as Storage, + } as unknown as DiscoverServices); + }); + + expect(result.current.defaultHeight).toEqual({ + lineCount: CONFIG_ROW_HEIGHT, + }); + }); + + test('should apply rowHeight from uiSettings instead of local storage value, since uiSettings has been changed', () => { + const { result } = renderHook(() => { + return useRowHeightsOptions({ + uiSettings: uiSettingsMock, + storage: new LocalStorageMock({ + ['discover:dataGridRowHeight']: { + previousRowHeight: 4, + // different from uiSettings (config), now user changed it to 3, but prev was 4 + previousConfigRowHeight: 4, + }, + }) as unknown as Storage, + } as unknown as DiscoverServices); + }); + + expect(result.current.defaultHeight).toEqual({ + lineCount: CONFIG_ROW_HEIGHT, + }); + }); +}); diff --git a/src/plugins/discover/public/utils/use_row_heights_options.ts b/src/plugins/discover/public/utils/use_row_heights_options.ts new file mode 100644 index 0000000000000..bd067304251bf --- /dev/null +++ b/src/plugins/discover/public/utils/use_row_heights_options.ts @@ -0,0 +1,135 @@ +/* + * 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 type { EuiDataGridRowHeightOption, EuiDataGridRowHeightsOptions } from '@elastic/eui'; +import { useEffect, useMemo } from 'react'; +import { IUiSettingsClient } from 'kibana/public'; +import { Storage } from '../../../kibana_utils/public'; +import { ROW_HEIGHT_OPTION } from '../../common'; +import { isValidRowHeight } from './validate_row_height'; + +interface UseRowHeightProps { + rowHeightState?: number; + onUpdateRowHeight?: (rowHeight: number) => void; + storage: Storage; + uiSettings: IUiSettingsClient; +} + +interface DataGridOptionsRecord { + previousRowHeight: number; + previousConfigRowHeight: number; +} + +/** + * Row height might be a value from -1 to 20 + * A value of -1 automatically adjusts the row height to fit the contents. + * A value of 0 displays the content in a single line. + * A value from 1 to 20 represents number of lines of Document explorer row to display. + */ +const SINGLE_ROW_HEIGHT_OPTION = 0; +const AUTO_ROW_HEIGHT_OPTION = -1; +const ROW_HEIGHT_KEY = 'discover:dataGridRowHeight'; + +/** + * Converts rowHeight of EuiDataGrid to rowHeight number (-1 to 20) + */ +const serializeRowHeight = (rowHeight?: EuiDataGridRowHeightOption): number => { + if (rowHeight === 'auto') { + return AUTO_ROW_HEIGHT_OPTION; + } else if (typeof rowHeight === 'object' && rowHeight.lineCount) { + return rowHeight.lineCount; // custom + } + + return SINGLE_ROW_HEIGHT_OPTION; +}; + +/** + * Converts rowHeight number (-1 to 20) of EuiDataGrid rowHeight + */ +const deserializeRowHeight = (number: number): EuiDataGridRowHeightOption | undefined => { + if (number === AUTO_ROW_HEIGHT_OPTION) { + return 'auto'; + } else if (number === SINGLE_ROW_HEIGHT_OPTION) { + return undefined; + } + + return { lineCount: number }; // custom +}; + +const getStoredRowHeight = (storage: Storage): DataGridOptionsRecord | null => { + const entry = storage.get(ROW_HEIGHT_KEY); + if ( + typeof entry === 'object' && + entry !== null && + isValidRowHeight(entry.previousRowHeight) && + isValidRowHeight(entry.previousConfigRowHeight) + ) { + return entry; + } + return null; +}; + +const updateStoredRowHeight = (newRowHeight: number, configRowHeight: number, storage: Storage) => { + storage.set(ROW_HEIGHT_KEY, { + previousRowHeight: newRowHeight, + previousConfigRowHeight: configRowHeight, + }); +}; + +export const useRowHeightsOptions = ({ + rowHeightState, + onUpdateRowHeight, + storage, + uiSettings, +}: UseRowHeightProps) => { + /** + * The following should be removed after EUI update + * with https://github.com/elastic/eui/issues/5524 + */ + useEffect(() => { + if (isValidRowHeight(rowHeightState)) { + onUpdateRowHeight?.(rowHeightState); + updateStoredRowHeight(rowHeightState, uiSettings.get(ROW_HEIGHT_OPTION), storage); + } + }, [rowHeightState, onUpdateRowHeight, storage, uiSettings]); + + const defaultRowHeights = useMemo((): EuiDataGridRowHeightsOptions => { + const rowHeightFromLS = getStoredRowHeight(storage); + const configRowHeight = uiSettings.get(ROW_HEIGHT_OPTION); + + const configHasNotChanged = ( + localStorageRecord: DataGridOptionsRecord | null + ): localStorageRecord is DataGridOptionsRecord => + localStorageRecord !== null && configRowHeight === localStorageRecord.previousConfigRowHeight; + + let rowHeight; + if (isValidRowHeight(rowHeightState)) { + rowHeight = rowHeightState; + } else if (configHasNotChanged(rowHeightFromLS)) { + rowHeight = rowHeightFromLS.previousRowHeight; + } else { + rowHeight = configRowHeight; + } + + // update local storage value when config has changed + if (!configHasNotChanged(rowHeightFromLS)) { + updateStoredRowHeight(configRowHeight, configRowHeight, storage); + } + + return { + defaultHeight: deserializeRowHeight(rowHeight), + onChange: ({ defaultHeight: newRowHeight }: EuiDataGridRowHeightsOptions) => { + const newSerializedRowHeight = serializeRowHeight(newRowHeight); + updateStoredRowHeight(newSerializedRowHeight, configRowHeight, storage); + onUpdateRowHeight?.(newSerializedRowHeight); + }, + }; + }, [rowHeightState, uiSettings, storage, onUpdateRowHeight]); + + return defaultRowHeights; +}; diff --git a/src/plugins/discover/public/utils/validate_row_height.ts b/src/plugins/discover/public/utils/validate_row_height.ts new file mode 100644 index 0000000000000..c66ae75dcccd9 --- /dev/null +++ b/src/plugins/discover/public/utils/validate_row_height.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +const MIN_ROW_HEIGHT = -1; +const MAX_ROW_HEIGHT = 20; + +export const isValidRowHeight = (rowHeight?: number): rowHeight is number => { + return ( + // is number + typeof rowHeight === 'number' && + !Number.isNaN(rowHeight) && + // is integer + Math.floor(rowHeight) === rowHeight && + // does it fit the range + rowHeight >= MIN_ROW_HEIGHT && + rowHeight <= MAX_ROW_HEIGHT + ); +}; diff --git a/src/plugins/discover/server/saved_objects/search.ts b/src/plugins/discover/server/saved_objects/search.ts index 23d9312e82897..d5b0a3e09bc61 100644 --- a/src/plugins/discover/server/saved_objects/search.ts +++ b/src/plugins/discover/server/saved_objects/search.ts @@ -45,6 +45,7 @@ export const searchSavedObjectType: SavedObjectsType = { title: { type: 'text' }, grid: { type: 'object', enabled: false }, version: { type: 'integer' }, + rowHeight: { type: 'text' }, }, }, migrations: searchMigrations, diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index 45df73aca9022..cb0714051a55e 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -28,6 +28,7 @@ import { SHOW_MULTIFIELDS, TRUNCATE_MAX_HEIGHT, SHOW_FIELD_STATISTICS, + ROW_HEIGHT_OPTION, } from '../common'; export const getUiSettings: () => Record = () => ({ @@ -251,15 +252,27 @@ export const getUiSettings: () => Record = () => ({ category: ['discover'], schema: schema.boolean(), }, + [ROW_HEIGHT_OPTION]: { + name: i18n.translate('discover.advancedSettings.params.rowHeightTitle', { + defaultMessage: 'Row height in the Document Explorer', + }), + value: 3, + category: ['discover'], + description: i18n.translate('discover.advancedSettings.params.rowHeightText', { + defaultMessage: + 'The number of lines to allow in a row. A value of -1 automatically adjusts the row height to fit the contents. A value of 0 displays the content in a single line.', + }), + schema: schema.number({ min: -1 }), + }, [TRUNCATE_MAX_HEIGHT]: { - name: i18n.translate('discover.advancedSettings.params.maxCellHeightTitle', { - defaultMessage: 'Maximum table cell height', + name: i18n.translate('discover.advancedSettings.params.truncateHeightTitle', { + defaultMessage: 'Maximum height of classic view cell', }), value: 115, category: ['discover'], - description: i18n.translate('discover.advancedSettings.params.maxCellHeightText', { + description: i18n.translate('discover.advancedSettings.params.truncateHeightText', { defaultMessage: - 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation', + 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation.', }), schema: schema.number({ min: 0 }), }, diff --git a/test/functional/apps/dashboard/embeddable_data_grid.ts b/test/functional/apps/dashboard/embeddable_data_grid.ts index 66aca561383c3..5dea22f5006c5 100644 --- a/test/functional/apps/dashboard/embeddable_data_grid.ts +++ b/test/functional/apps/dashboard/embeddable_data_grid.ts @@ -36,6 +36,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.clickNewDashboard(); await PageObjects.timePicker.setDefaultDataRange(); await dashboardAddPanel.addSavedSearch('Rendering-Test:-saved-search'); + await PageObjects.header.waitUntilLoadingHasFinished(); }); it('should expand the detail row when the toggle arrow is clicked', async function () {