diff --git a/src/plugins/discover/public/application/main/components/chart/discover_chart.tsx b/src/plugins/discover/public/application/main/components/chart/discover_chart.tsx index da2f0810706b8..4bc60e904bb84 100644 --- a/src/plugins/discover/public/application/main/components/chart/discover_chart.tsx +++ b/src/plugins/discover/public/application/main/components/chart/discover_chart.tsx @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { memo, useCallback, useEffect, useRef, useState } from 'react'; +import React, { memo, ReactElement, useCallback, useEffect, useRef, useState } from 'react'; import moment from 'moment'; import { EuiButtonIcon, @@ -14,7 +14,6 @@ import { EuiFlexItem, EuiPopover, EuiToolTip, - EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { DataView } from '@kbn/data-views-plugin/public'; @@ -24,8 +23,6 @@ import { GetStateReturn } from '../../services/discover_state'; import { DiscoverHistogram } from './histogram'; import { DataCharts$, DataTotalHits$ } from '../../hooks/use_saved_search'; import { useChartPanels } from './use_chart_panels'; -import { VIEW_MODE, DocumentViewModeToggle } from '../../../../components/view_mode_toggle'; -import { SHOW_FIELD_STATISTICS } from '../../../../../common'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { getVisualizeInformation, @@ -36,33 +33,32 @@ const DiscoverHistogramMemoized = memo(DiscoverHistogram); export const CHART_HIDDEN_KEY = 'discover:chartHidden'; export function DiscoverChart({ + className, resetSavedSearch, savedSearch, savedSearchDataChart$, savedSearchDataTotalHits$, stateContainer, dataView, - viewMode, - setDiscoverViewMode, hideChart, interval, isTimeBased, + appendHistogram, }: { + className?: string; resetSavedSearch: () => void; savedSearch: SavedSearch; savedSearchDataChart$: DataCharts$; savedSearchDataTotalHits$: DataTotalHits$; stateContainer: GetStateReturn; dataView: DataView; - viewMode: VIEW_MODE; - setDiscoverViewMode: (viewMode: VIEW_MODE) => void; isTimeBased: boolean; hideChart?: boolean; interval?: string; + appendHistogram?: ReactElement; }) { - const { uiSettings, data, storage } = useDiscoverServices(); + const { data, storage } = useDiscoverServices(); const [showChartOptionsPopover, setShowChartOptionsPopover] = useState(false); - const showViewModeToggle = uiSettings.get(SHOW_FIELD_STATISTICS) ?? false; const chartRef = useRef<{ element: HTMLElement | null; moveFocus: boolean }>({ element: null, @@ -126,9 +122,15 @@ export function DiscoverChart({ ); return ( - + - + - {showViewModeToggle && ( - - - - )} {isTimeBased && ( @@ -203,7 +197,7 @@ export function DiscoverChart({ {isTimeBased && !hideChart && ( - +
(chartRef.current.element = element)} tabIndex={-1} @@ -218,7 +212,7 @@ export function DiscoverChart({ stateContainer={stateContainer} />
- + {appendHistogram}
)}
diff --git a/src/plugins/discover/public/application/main/components/field_stats_table/field_stats_table.tsx b/src/plugins/discover/public/application/main/components/field_stats_table/field_stats_table.tsx index 59d8fae4afbeb..cd3849d3cf4d3 100644 --- a/src/plugins/discover/public/application/main/components/field_stats_table/field_stats_table.tsx +++ b/src/plugins/discover/public/application/main/components/field_stats_table/field_stats_table.tsx @@ -18,6 +18,8 @@ import { isErrorEmbeddable, } from '@kbn/embeddable-plugin/public'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; +import { EuiFlexItem } from '@elastic/eui'; +import { css } from '@emotion/react'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { FIELD_STATISTICS_LOADED } from './constants'; import type { GetStateReturn } from '../../services/discover_state'; @@ -226,13 +228,22 @@ export const FieldStatisticsTable = (props: FieldStatisticsTableProps) => { }; }, [embeddable, embeddableRoot, trackUiMetric]); + const statsTableCss = css` + overflow-y: auto; + + .kbnDocTableWrapper { + overflow-x: hidden; + } + `; + return ( -
+ +
+ ); }; diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.scss b/src/plugins/discover/public/application/main/components/layout/discover_layout.scss index 9ea41f343b885..ee727779fc2a1 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.scss +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.scss @@ -70,7 +70,9 @@ discover-app { } .dscTimechart { - display: block; + flex-grow: 1; + display: flex; + flex-direction: column; position: relative; // SASSTODO: the visualizing component should have an option or a modifier @@ -81,13 +83,12 @@ discover-app { } .dscHistogram { - height: $euiSize * 7; - padding: 0 $euiSizeS $euiSizeS * 2 $euiSizeS; + flex-grow: 1; + padding: 0 $euiSizeS $euiSizeS $euiSizeS; } .dscHistogramTimeRange { padding: 0 $euiSizeS 0 $euiSizeS; - margin-top: - $euiSizeS; } .dscTable { 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 678e44b3ca444..39db1481500fa 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 @@ -175,24 +175,27 @@ function mountComponent( describe('Discover component', () => { test('selected data view without time field displays no chart toggle', () => { - const component = mountComponent(dataViewMock); - expect(component.find('[data-test-subj="discoverChartOptionsToggle"]').exists()).toBeFalsy(); + const container = document.createElement('div'); + mountComponent(dataViewMock, undefined, { attachTo: container }); + expect(container.querySelector('[data-test-subj="discoverChartOptionsToggle"]')).toBeNull(); }); test('selected data view with time field displays chart toggle', () => { - const component = mountComponent(dataViewWithTimefieldMock); - expect(component.find('[data-test-subj="discoverChartOptionsToggle"]').exists()).toBeTruthy(); + const container = document.createElement('div'); + mountComponent(dataViewWithTimefieldMock, undefined, { attachTo: container }); + expect(container.querySelector('[data-test-subj="discoverChartOptionsToggle"]')).not.toBeNull(); }); test('sql query displays no chart toggle', () => { - const component = mountComponent( + const container = document.createElement('div'); + mountComponent( dataViewWithTimefieldMock, false, - {}, + { attachTo: container }, { sql: 'SELECT * FROM test' }, true ); - expect(component.find('[data-test-subj="discoverChartOptionsToggle"]').exists()).toBeFalsy(); + expect(container.querySelector('[data-test-subj="discoverChartOptionsToggle"]')).toBeNull(); }); test('the saved search title h1 gains focus on navigate', () => { 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 76eccfe2813ec..fbd58853d797c 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 @@ -12,7 +12,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiHideFor, - EuiHorizontalRule, EuiPage, EuiPageBody, EuiPageContent_Deprecated as EuiPageContent, @@ -34,20 +33,17 @@ import { SEARCH_FIELDS_FROM_SOURCE, SHOW_FIELD_STATISTICS } from '../../../../.. import { popularizeField } from '../../../../utils/popularize_field'; import { DiscoverTopNav } from '../top_nav/discover_topnav'; import { DocViewFilterFn } from '../../../../services/doc_views/doc_views_types'; -import { DiscoverChart } from '../chart'; import { getResultState } from '../../utils/get_result_state'; import { DiscoverUninitialized } from '../uninitialized/uninitialized'; import { DataMainMsg, RecordRawType } from '../../hooks/use_saved_search'; import { useColumns } from '../../../../hooks/use_data_grid_columns'; -import { DiscoverDocuments } from './discover_documents'; import { FetchStatus } from '../../../types'; import { useDataState } from '../../hooks/use_data_state'; -import { FieldStatisticsTable } from '../field_stats_table'; import { VIEW_MODE } from '../../../../components/view_mode_toggle'; -import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stats_table/constants'; import { hasActiveFilter } from './utils'; import { getRawRecordType } from '../../utils/get_raw_record_type'; import { SavedSearchURLConflictCallout } from '../../../../components/saved_search_url_conflict_callout/saved_search_url_conflict_callout'; +import { DiscoverMainContent } from './discover_main_content'; /** * Local storage key for sidebar persistence state @@ -56,8 +52,6 @@ export const SIDEBAR_CLOSED_KEY = 'discover:sidebarClosed'; const SidebarMemoized = React.memo(DiscoverSidebarResponsive); const TopNavMemoized = React.memo(DiscoverTopNav); -const DiscoverChartMemoized = React.memo(DiscoverChart); -const FieldStatisticsTableMemoized = React.memo(FieldStatisticsTable); export function DiscoverLayout({ dataView, @@ -91,7 +85,7 @@ export function DiscoverLayout({ spaces, inspector, } = useDiscoverServices(); - const { main$, charts$, totalHits$ } = savedSearchData$; + const { main$ } = savedSearchData$; const dataState: DataMainMsg = useDataState(main$); const viewMode = useMemo(() => { @@ -99,21 +93,6 @@ export function DiscoverLayout({ return state.viewMode ?? VIEW_MODE.DOCUMENT_LEVEL; }, [uiSettings, state.viewMode]); - const setDiscoverViewMode = useCallback( - (mode: VIEW_MODE) => { - stateContainer.setAppState({ viewMode: mode }); - - if (trackUiMetric) { - if (mode === VIEW_MODE.AGGREGATED_LEVEL) { - trackUiMetric(METRIC_TYPE.CLICK, FIELD_STATISTICS_VIEW_CLICK); - } else { - trackUiMetric(METRIC_TYPE.CLICK, DOCUMENTS_VIEW_CLICK); - } - } - }, - [trackUiMetric, stateContainer] - ); - const fetchCounter = useRef(0); useEffect(() => { @@ -210,6 +189,8 @@ export function DiscoverLayout({ } }, [dataState.error, isPlainRecord]); + const resizeRef = useRef(null); + return (

} {resultState === 'ready' && ( - - {!isPlainRecord && ( - <> - - - - - - )} - {viewMode === VIEW_MODE.DOCUMENT_LEVEL ? ( - - ) : ( - - )} - + )} diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx new file mode 100644 index 0000000000000..6b499aa62c430 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx @@ -0,0 +1,254 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { Subject, BehaviorSubject } from 'rxjs'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { esHits } from '../../../../__mocks__/es_hits'; +import { dataViewMock } from '../../../../__mocks__/data_view'; +import { savedSearchMock } from '../../../../__mocks__/saved_search'; +import { GetStateReturn } from '../../services/discover_state'; +import { + AvailableFields$, + DataCharts$, + DataDocuments$, + DataMain$, + DataTotalHits$, + RecordRawType, +} from '../../hooks/use_saved_search'; +import { discoverServiceMock } from '../../../../__mocks__/services'; +import { FetchStatus } from '../../../types'; +import { Chart } from '../chart/point_series'; +import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { buildDataTableRecord } from '../../../../utils/build_data_record'; +import { DiscoverMainContent, DiscoverMainContentProps } from './discover_main_content'; +import { VIEW_MODE } from '@kbn/saved-search-plugin/public'; +import { DiscoverPanels, DISCOVER_PANELS_MODE } from './discover_panels'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { CoreTheme } from '@kbn/core/public'; +import { act } from 'react-dom/test-utils'; +import { setTimeout } from 'timers/promises'; +import { DiscoverChart } from '../chart'; +import { ReactWrapper } from 'enzyme'; +import { DocumentViewModeToggle } from '../../../../components/view_mode_toggle'; + +const mountComponent = async ({ + isPlainRecord = false, + hideChart = false, + isTimeBased = true, +}: { + isPlainRecord?: boolean; + hideChart?: boolean; + isTimeBased?: boolean; +} = {}) => { + const services = discoverServiceMock; + services.data.query.timefilter.timefilter.getAbsoluteTime = () => { + return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; + }; + + const main$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + recordRawType: isPlainRecord ? RecordRawType.PLAIN : RecordRawType.DOCUMENT, + foundDocuments: true, + }) as DataMain$; + + const documents$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + result: esHits.map((esHit) => buildDataTableRecord(esHit, dataViewMock)), + }) as DataDocuments$; + + const availableFields$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + fields: [] as string[], + }) as AvailableFields$; + + const totalHits$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + result: Number(esHits.length), + }) as DataTotalHits$; + + const chartData = { + xAxisOrderedValues: [ + 1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000, + 1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000, + 1624917600000, 1625004000000, 1625090400000, + ], + xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, + xAxisLabel: 'order_date per day', + yAxisFormat: { id: 'number' }, + ordered: { + date: true, + interval: { + asMilliseconds: jest.fn(), + }, + intervalESUnit: 'd', + intervalESValue: 1, + min: '2021-03-18T08:28:56.411Z', + max: '2021-07-01T07:28:56.411Z', + }, + yAxisLabel: 'Count', + values: [ + { x: 1623880800000, y: 134 }, + { x: 1623967200000, y: 152 }, + { x: 1624053600000, y: 141 }, + { x: 1624140000000, y: 138 }, + { x: 1624226400000, y: 142 }, + { x: 1624312800000, y: 157 }, + { x: 1624399200000, y: 149 }, + { x: 1624485600000, y: 146 }, + { x: 1624572000000, y: 170 }, + { x: 1624658400000, y: 137 }, + { x: 1624744800000, y: 150 }, + { x: 1624831200000, y: 144 }, + { x: 1624917600000, y: 147 }, + { x: 1625004000000, y: 137 }, + { x: 1625090400000, y: 66 }, + ], + } as unknown as Chart; + + const charts$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + chartData, + bucketInterval: { + scaled: true, + description: 'test', + scale: 2, + }, + }) as DataCharts$; + + const savedSearchData$ = { + main$, + documents$, + totalHits$, + charts$, + availableFields$, + }; + + const props: DiscoverMainContentProps = { + isPlainRecord, + dataView: dataViewMock, + navigateTo: jest.fn(), + resetSavedSearch: jest.fn(), + setExpandedDoc: jest.fn(), + savedSearch: savedSearchMock, + savedSearchData$, + savedSearchRefetch$: new Subject(), + state: { columns: [], hideChart }, + stateContainer: { + setAppState: () => {}, + appStateContainer: { + getState: () => ({ + interval: 'auto', + }), + }, + } as unknown as GetStateReturn, + isTimeBased, + viewMode: VIEW_MODE.DOCUMENT_LEVEL, + onAddFilter: jest.fn(), + onFieldEdited: jest.fn(), + columns: [], + resizeRef: { current: null }, + }; + + const coreTheme$ = new BehaviorSubject({ darkMode: false }); + + const component = mountWithIntl( + + + + + + ); + + // useIsWithinBreakpoints triggers state updates which cause act + // issues and prevent our resize events from being fired correctly + // https://github.com/enzymejs/enzyme/issues/2073 + await act(() => setTimeout(0)); + + return component; +}; + +const setWindowWidth = (component: ReactWrapper, width: string) => { + window.innerWidth = parseInt(width, 10); + act(() => { + window.dispatchEvent(new Event('resize')); + }); + component.update(); +}; + +describe('Discover main content component', () => { + const windowWidth = window.innerWidth; + + beforeEach(() => { + window.innerWidth = windowWidth; + }); + + it('should set the panels mode to DISCOVER_PANELS_MODE.RESIZABLE when viewing on medium screens and above', async () => { + const component = await mountComponent(); + setWindowWidth(component, euiThemeVars.euiBreakpoints.m); + expect(component.find(DiscoverPanels).prop('mode')).toBe(DISCOVER_PANELS_MODE.RESIZABLE); + }); + + it('should set the panels mode to DISCOVER_PANELS_MODE.FIXED when viewing on small screens and below', async () => { + const component = await mountComponent(); + setWindowWidth(component, euiThemeVars.euiBreakpoints.s); + expect(component.find(DiscoverPanels).prop('mode')).toBe(DISCOVER_PANELS_MODE.FIXED); + }); + + it('should set the panels mode to DISCOVER_PANELS_MODE.FIXED if hideChart is true', async () => { + const component = await mountComponent({ hideChart: true }); + expect(component.find(DiscoverPanels).prop('mode')).toBe(DISCOVER_PANELS_MODE.FIXED); + }); + + it('should set the panels mode to DISCOVER_PANELS_MODE.FIXED if isTimeBased is false', async () => { + const component = await mountComponent({ isTimeBased: false }); + expect(component.find(DiscoverPanels).prop('mode')).toBe(DISCOVER_PANELS_MODE.FIXED); + }); + + it('should set the panels mode to DISCOVER_PANELS_MODE.SINGLE if isPlainRecord is true', async () => { + const component = await mountComponent({ isPlainRecord: true }); + expect(component.find(DiscoverPanels).prop('mode')).toBe(DISCOVER_PANELS_MODE.SINGLE); + }); + + it('should set a fixed height for DiscoverChart when panels mode is DISCOVER_PANELS_MODE.FIXED and hideChart is false', async () => { + const component = await mountComponent(); + setWindowWidth(component, euiThemeVars.euiBreakpoints.s); + const expectedHeight = component.find(DiscoverPanels).prop('initialTopPanelHeight'); + expect(component.find(DiscoverChart).childAt(0).getDOMNode()).toHaveStyle({ + height: `${expectedHeight}px`, + }); + }); + + it('should not set a fixed height for DiscoverChart when panels mode is DISCOVER_PANELS_MODE.FIXED and hideChart is true', async () => { + const component = await mountComponent({ hideChart: true }); + setWindowWidth(component, euiThemeVars.euiBreakpoints.s); + const expectedHeight = component.find(DiscoverPanels).prop('initialTopPanelHeight'); + expect(component.find(DiscoverChart).childAt(0).getDOMNode()).not.toHaveStyle({ + height: `${expectedHeight}px`, + }); + }); + + it('should not set a fixed height for DiscoverChart when panels mode is DISCOVER_PANELS_MODE.FIXED and isTimeBased is false', async () => { + const component = await mountComponent({ isTimeBased: false }); + setWindowWidth(component, euiThemeVars.euiBreakpoints.s); + const expectedHeight = component.find(DiscoverPanels).prop('initialTopPanelHeight'); + expect(component.find(DiscoverChart).childAt(0).getDOMNode()).not.toHaveStyle({ + height: `${expectedHeight}px`, + }); + }); + + it('should show DocumentViewModeToggle when isPlainRecord is false', async () => { + const component = await mountComponent(); + expect(component.find(DocumentViewModeToggle).exists()).toBe(true); + }); + + it('should not show DocumentViewModeToggle when isPlainRecord is true', async () => { + const component = await mountComponent({ isPlainRecord: true }); + expect(component.find(DocumentViewModeToggle).exists()).toBe(false); + }); +}); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx new file mode 100644 index 0000000000000..234b4392453e6 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx @@ -0,0 +1,199 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiSpacer, + useEuiTheme, + useIsWithinBreakpoints, +} from '@elastic/eui'; +import { SavedSearch } from '@kbn/saved-search-plugin/public'; +import React, { RefObject, useCallback, useMemo } from 'react'; +import { DataView } from '@kbn/data-views-plugin/common'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal'; +import { css } from '@emotion/css'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; +import { DataTableRecord } from '../../../../types'; +import { DocumentViewModeToggle, VIEW_MODE } from '../../../../components/view_mode_toggle'; +import { DocViewFilterFn } from '../../../../services/doc_views/doc_views_types'; +import { DataRefetch$, SavedSearchData } from '../../hooks/use_saved_search'; +import { AppState, GetStateReturn } from '../../services/discover_state'; +import { DiscoverChart } from '../chart'; +import { FieldStatisticsTable } from '../field_stats_table'; +import { DiscoverDocuments } from './discover_documents'; +import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stats_table/constants'; +import { DiscoverPanels, DISCOVER_PANELS_MODE } from './discover_panels'; + +const DiscoverChartMemoized = React.memo(DiscoverChart); +const FieldStatisticsTableMemoized = React.memo(FieldStatisticsTable); + +export interface DiscoverMainContentProps { + isPlainRecord: boolean; + dataView: DataView; + navigateTo: (url: string) => void; + resetSavedSearch: () => void; + expandedDoc?: DataTableRecord; + setExpandedDoc: (doc?: DataTableRecord) => void; + savedSearch: SavedSearch; + savedSearchData$: SavedSearchData; + savedSearchRefetch$: DataRefetch$; + state: AppState; + stateContainer: GetStateReturn; + isTimeBased: boolean; + viewMode: VIEW_MODE; + onAddFilter: DocViewFilterFn | undefined; + onFieldEdited: () => void; + columns: string[]; + resizeRef: RefObject; +} + +export const DiscoverMainContent = ({ + isPlainRecord, + dataView, + navigateTo, + resetSavedSearch, + expandedDoc, + setExpandedDoc, + savedSearch, + savedSearchData$, + savedSearchRefetch$, + state, + stateContainer, + isTimeBased, + viewMode, + onAddFilter, + onFieldEdited, + columns, + resizeRef, +}: DiscoverMainContentProps) => { + const { trackUiMetric } = useDiscoverServices(); + + const setDiscoverViewMode = useCallback( + (mode: VIEW_MODE) => { + stateContainer.setAppState({ viewMode: mode }); + + if (trackUiMetric) { + if (mode === VIEW_MODE.AGGREGATED_LEVEL) { + trackUiMetric(METRIC_TYPE.CLICK, FIELD_STATISTICS_VIEW_CLICK); + } else { + trackUiMetric(METRIC_TYPE.CLICK, DOCUMENTS_VIEW_CLICK); + } + } + }, + [trackUiMetric, stateContainer] + ); + + const topPanelNode = useMemo( + () => createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } }), + [] + ); + + const mainPanelNode = useMemo( + () => createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } }), + [] + ); + + const hideChart = state.hideChart || !isTimeBased; + const showFixedPanels = useIsWithinBreakpoints(['xs', 's']) || isPlainRecord || hideChart; + const { euiTheme } = useEuiTheme(); + const topPanelHeight = euiTheme.base * 12; + const minTopPanelHeight = euiTheme.base * 8; + const minMainPanelHeight = euiTheme.base * 10; + + const chartClassName = + showFixedPanels && !hideChart + ? css` + height: ${topPanelHeight}px; + ` + : 'eui-fullHeight'; + + const panelsMode = isPlainRecord + ? DISCOVER_PANELS_MODE.SINGLE + : showFixedPanels + ? DISCOVER_PANELS_MODE.FIXED + : DISCOVER_PANELS_MODE.RESIZABLE; + + return ( + <> + + : } + /> + + + + {!isPlainRecord && ( + + {!showFixedPanels && } + + + + )} + {viewMode === VIEW_MODE.DOCUMENT_LEVEL ? ( + + ) : ( + + )} + + + } + mainPanel={} + /> + + ); +}; diff --git a/src/plugins/discover/public/application/main/components/layout/discover_panels.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_panels.test.tsx new file mode 100644 index 0000000000000..60ba0038f1194 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/layout/discover_panels.test.tsx @@ -0,0 +1,93 @@ +/* + * 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 { mount } from 'enzyme'; +import React, { ReactElement, RefObject } from 'react'; +import { DiscoverPanels, DISCOVER_PANELS_MODE } from './discover_panels'; +import { DiscoverPanelsResizable } from './discover_panels_resizable'; +import { DiscoverPanelsFixed } from './discover_panels_fixed'; + +describe('Discover panels component', () => { + const mountComponent = ({ + mode = DISCOVER_PANELS_MODE.RESIZABLE, + resizeRef = { current: null }, + initialTopPanelHeight = 200, + minTopPanelHeight = 100, + minMainPanelHeight = 100, + topPanel = <>, + mainPanel = <>, + }: { + mode?: DISCOVER_PANELS_MODE; + resizeRef?: RefObject; + initialTopPanelHeight?: number; + minTopPanelHeight?: number; + minMainPanelHeight?: number; + mainPanel?: ReactElement; + topPanel?: ReactElement; + }) => { + return mount( + + ); + }; + + it('should show DiscoverPanelsFixed when mode is DISCOVER_PANELS_MODE.SINGLE', () => { + const topPanel =
; + const mainPanel =
; + const component = mountComponent({ mode: DISCOVER_PANELS_MODE.SINGLE, topPanel, mainPanel }); + expect(component.find(DiscoverPanelsFixed).exists()).toBe(true); + expect(component.find(DiscoverPanelsResizable).exists()).toBe(false); + expect(component.contains(topPanel)).toBe(false); + expect(component.contains(mainPanel)).toBe(true); + }); + + it('should show DiscoverPanelsFixed when mode is DISCOVER_PANELS_MODE.FIXED', () => { + const topPanel =
; + const mainPanel =
; + const component = mountComponent({ mode: DISCOVER_PANELS_MODE.FIXED, topPanel, mainPanel }); + expect(component.find(DiscoverPanelsFixed).exists()).toBe(true); + expect(component.find(DiscoverPanelsResizable).exists()).toBe(false); + expect(component.contains(topPanel)).toBe(true); + expect(component.contains(mainPanel)).toBe(true); + }); + + it('should show DiscoverPanelsResizable when mode is DISCOVER_PANELS_MODE.RESIZABLE', () => { + const topPanel =
; + const mainPanel =
; + const component = mountComponent({ mode: DISCOVER_PANELS_MODE.RESIZABLE, topPanel, mainPanel }); + expect(component.find(DiscoverPanelsFixed).exists()).toBe(false); + expect(component.find(DiscoverPanelsResizable).exists()).toBe(true); + expect(component.contains(topPanel)).toBe(true); + expect(component.contains(mainPanel)).toBe(true); + }); + + it('should pass true for hideTopPanel when mode is DISCOVER_PANELS_MODE.SINGLE', () => { + const topPanel =
; + const mainPanel =
; + const component = mountComponent({ mode: DISCOVER_PANELS_MODE.SINGLE, topPanel, mainPanel }); + expect(component.find(DiscoverPanelsFixed).prop('hideTopPanel')).toBe(true); + expect(component.contains(topPanel)).toBe(false); + expect(component.contains(mainPanel)).toBe(true); + }); + + it('should pass false for hideTopPanel when mode is DISCOVER_PANELS_MODE.FIXED', () => { + const topPanel =
; + const mainPanel =
; + const component = mountComponent({ mode: DISCOVER_PANELS_MODE.FIXED, topPanel, mainPanel }); + expect(component.find(DiscoverPanelsFixed).prop('hideTopPanel')).toBe(false); + expect(component.contains(topPanel)).toBe(true); + expect(component.contains(mainPanel)).toBe(true); + }); +}); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_panels.tsx b/src/plugins/discover/public/application/main/components/layout/discover_panels.tsx new file mode 100644 index 0000000000000..57ea08ee92c8b --- /dev/null +++ b/src/plugins/discover/public/application/main/components/layout/discover_panels.tsx @@ -0,0 +1,55 @@ +/* + * 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, { ReactElement, RefObject } from 'react'; +import { DiscoverPanelsResizable } from './discover_panels_resizable'; +import { DiscoverPanelsFixed } from './discover_panels_fixed'; + +export enum DISCOVER_PANELS_MODE { + SINGLE = 'single', + FIXED = 'fixed', + RESIZABLE = 'resizable', +} + +export interface DiscoverPanelsProps { + className?: string; + mode: DISCOVER_PANELS_MODE; + resizeRef: RefObject; + initialTopPanelHeight: number; + minTopPanelHeight: number; + minMainPanelHeight: number; + topPanel: ReactElement; + mainPanel: ReactElement; +} + +const fixedModes = [DISCOVER_PANELS_MODE.SINGLE, DISCOVER_PANELS_MODE.FIXED]; + +export const DiscoverPanels = ({ + className, + mode, + resizeRef, + initialTopPanelHeight, + minTopPanelHeight, + minMainPanelHeight, + topPanel, + mainPanel, +}: DiscoverPanelsProps) => { + const panelsProps = { className, topPanel, mainPanel }; + + return fixedModes.includes(mode) ? ( + + ) : ( + + ); +}; diff --git a/src/plugins/discover/public/application/main/components/layout/discover_panels_fixed.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_panels_fixed.test.tsx new file mode 100644 index 0000000000000..a4632f83c918c --- /dev/null +++ b/src/plugins/discover/public/application/main/components/layout/discover_panels_fixed.test.tsx @@ -0,0 +1,43 @@ +/* + * 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 { mount } from 'enzyme'; +import React, { ReactElement } from 'react'; +import { DiscoverPanelsFixed } from './discover_panels_fixed'; + +describe('Discover panels fixed', () => { + const mountComponent = ({ + hideTopPanel = false, + topPanel = <>, + mainPanel = <>, + }: { + hideTopPanel?: boolean; + topPanel: ReactElement; + mainPanel: ReactElement; + }) => { + return mount( + + ); + }; + + it('should render both panels when hideTopPanel is false', () => { + const topPanel =
; + const mainPanel =
; + const component = mountComponent({ topPanel, mainPanel }); + expect(component.contains(topPanel)).toBe(true); + expect(component.contains(mainPanel)).toBe(true); + }); + + it('should render only main panel when hideTopPanel is true', () => { + const topPanel =
; + const mainPanel =
; + const component = mountComponent({ hideTopPanel: true, topPanel, mainPanel }); + expect(component.contains(topPanel)).toBe(false); + expect(component.contains(mainPanel)).toBe(true); + }); +}); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_panels_fixed.tsx b/src/plugins/discover/public/application/main/components/layout/discover_panels_fixed.tsx new file mode 100644 index 0000000000000..1db99e61fb8c5 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/layout/discover_panels_fixed.tsx @@ -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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { css } from '@emotion/react'; +import React, { ReactElement } from 'react'; + +export const DiscoverPanelsFixed = ({ + className, + hideTopPanel, + topPanel, + mainPanel, +}: { + className?: string; + hideTopPanel?: boolean; + topPanel: ReactElement; + mainPanel: ReactElement; +}) => { + // By default a flex item has overflow: visible, min-height: auto, and min-width: auto. + // This can cause the item to overflow the flexbox parent when its content is too large. + // Setting the overflow to something other than visible (e.g. auto) resets the min-height + // and min-width to 0 and makes the item respect the flexbox parent's size. + // https://stackoverflow.com/questions/36247140/why-dont-flex-items-shrink-past-content-size + const mainPanelCss = css` + overflow: auto; + `; + + return ( + + {!hideTopPanel && {topPanel}} + {mainPanel} + + ); +}; diff --git a/src/plugins/discover/public/application/main/components/layout/discover_panels_resizable.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_panels_resizable.test.tsx new file mode 100644 index 0000000000000..16c50a94b4656 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/layout/discover_panels_resizable.test.tsx @@ -0,0 +1,188 @@ +/* + * 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 { mount, ReactWrapper } from 'enzyme'; +import React, { ReactElement, RefObject } from 'react'; +import { DiscoverPanelsResizable } from './discover_panels_resizable'; +import { act } from 'react-dom/test-utils'; + +const containerHeight = 1000; +const topPanelId = 'topPanel'; + +jest.mock('@elastic/eui', () => ({ + ...jest.requireActual('@elastic/eui'), + useResizeObserver: jest.fn(), + useGeneratedHtmlId: jest.fn(() => topPanelId), +})); + +import * as eui from '@elastic/eui'; +import { waitFor } from '@testing-library/dom'; + +describe('Discover panels resizable', () => { + const mountComponent = ({ + className = '', + resizeRef = { current: null }, + initialTopPanelHeight = 0, + minTopPanelHeight = 0, + minMainPanelHeight = 0, + topPanel = <>, + mainPanel = <>, + attachTo, + }: { + className?: string; + resizeRef?: RefObject; + initialTopPanelHeight?: number; + minTopPanelHeight?: number; + minMainPanelHeight?: number; + topPanel?: ReactElement; + mainPanel?: ReactElement; + attachTo?: HTMLElement; + }) => { + return mount( + , + attachTo ? { attachTo } : undefined + ); + }; + + const expectCorrectPanelSizes = ( + component: ReactWrapper, + currentContainerHeight: number, + topPanelHeight: number + ) => { + const topPanelSize = (topPanelHeight / currentContainerHeight) * 100; + expect(component.find('[data-test-subj="dscResizablePanelTop"]').at(0).prop('size')).toBe( + topPanelSize + ); + expect(component.find('[data-test-subj="dscResizablePanelMain"]').at(0).prop('size')).toBe( + 100 - topPanelSize + ); + }; + + const forceRender = (component: ReactWrapper) => { + component.setProps({}).update(); + }; + + beforeEach(() => { + jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: containerHeight, width: 0 }); + }); + + it('should render both panels', () => { + const topPanel =
; + const mainPanel =
; + const component = mountComponent({ topPanel, mainPanel }); + expect(component.contains(topPanel)).toBe(true); + expect(component.contains(mainPanel)).toBe(true); + }); + + it('should set the initial heights of both panels', () => { + const initialTopPanelHeight = 200; + const component = mountComponent({ initialTopPanelHeight }); + expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight); + }); + + it('should set the correct heights of both panels when the panels are resized', () => { + const initialTopPanelHeight = 200; + const component = mountComponent({ initialTopPanelHeight }); + expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight); + const newTopPanelSize = 30; + const onPanelSizeChange = component + .find('[data-test-subj="dscResizableContainer"]') + .at(0) + .prop('onPanelWidthChange') as Function; + act(() => { + onPanelSizeChange({ [topPanelId]: newTopPanelSize }); + }); + forceRender(component); + expectCorrectPanelSizes(component, containerHeight, containerHeight * (newTopPanelSize / 100)); + }); + + it('should maintain the height of the top panel and resize the main panel when the container height changes', () => { + const initialTopPanelHeight = 200; + const component = mountComponent({ initialTopPanelHeight }); + expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight); + const newContainerHeight = 2000; + jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: newContainerHeight, width: 0 }); + forceRender(component); + expectCorrectPanelSizes(component, newContainerHeight, initialTopPanelHeight); + }); + + it('should resize the top panel once the main panel is at its minimum height', () => { + const initialTopPanelHeight = 500; + const minTopPanelHeight = 100; + const minMainPanelHeight = 100; + const component = mountComponent({ + initialTopPanelHeight, + minTopPanelHeight, + minMainPanelHeight, + }); + expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight); + const newContainerHeight = 400; + jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: newContainerHeight, width: 0 }); + forceRender(component); + expectCorrectPanelSizes(component, newContainerHeight, newContainerHeight - minMainPanelHeight); + jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: containerHeight, width: 0 }); + forceRender(component); + expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight); + }); + + it('should maintain the minimum heights of both panels when the container is too small to fit them', () => { + const initialTopPanelHeight = 500; + const minTopPanelHeight = 100; + const minMainPanelHeight = 150; + const component = mountComponent({ + initialTopPanelHeight, + minTopPanelHeight, + minMainPanelHeight, + }); + expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight); + const newContainerHeight = 200; + jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: newContainerHeight, width: 0 }); + forceRender(component); + expect(component.find('[data-test-subj="dscResizablePanelTop"]').at(0).prop('size')).toBe( + (minTopPanelHeight / newContainerHeight) * 100 + ); + expect(component.find('[data-test-subj="dscResizablePanelMain"]').at(0).prop('size')).toBe( + (minMainPanelHeight / newContainerHeight) * 100 + ); + jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: containerHeight, width: 0 }); + forceRender(component); + expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight); + }); + + it('should blur the resize button after a resize', async () => { + const attachTo = document.createElement('div'); + document.body.appendChild(attachTo); + const component = mountComponent({ attachTo }); + const wrapper = component.find('[data-test-subj="dscResizableContainerWrapper"]'); + const resizeButton = component.find('button[data-test-subj="dsc-resizable-button"]'); + const resizeButtonInner = component.find('[data-test-subj="dscResizableButtonInner"]'); + const mouseEvent = { + pageX: 0, + pageY: 0, + clientX: 0, + clientY: 0, + }; + resizeButtonInner.simulate('mousedown', mouseEvent); + resizeButton.simulate('mousedown', mouseEvent); + (resizeButton.getDOMNode() as HTMLElement).focus(); + wrapper.simulate('mouseup', mouseEvent); + resizeButton.simulate('click', mouseEvent); + expect(resizeButton.getDOMNode()).toHaveFocus(); + await waitFor(() => { + expect(resizeButton.getDOMNode()).not.toHaveFocus(); + }); + }); +}); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_panels_resizable.tsx b/src/plugins/discover/public/application/main/components/layout/discover_panels_resizable.tsx new file mode 100644 index 0000000000000..88a92b4380b76 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/layout/discover_panels_resizable.tsx @@ -0,0 +1,178 @@ +/* + * 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 { EuiResizableContainer, useGeneratedHtmlId, useResizeObserver } from '@elastic/eui'; +import { css } from '@emotion/react'; +import React, { ReactElement, RefObject, useCallback, useEffect, useState } from 'react'; + +const percentToPixels = (containerHeight: number, percentage: number) => + Math.round(containerHeight * (percentage / 100)); + +const pixelsToPercent = (containerHeight: number, pixels: number) => + +((pixels / containerHeight) * 100).toFixed(4); + +export const DiscoverPanelsResizable = ({ + className, + resizeRef, + initialTopPanelHeight, + minTopPanelHeight, + minMainPanelHeight, + topPanel, + mainPanel, +}: { + className?: string; + resizeRef: RefObject; + initialTopPanelHeight: number; + minTopPanelHeight: number; + minMainPanelHeight: number; + topPanel: ReactElement; + mainPanel: ReactElement; +}) => { + const topPanelId = useGeneratedHtmlId({ prefix: 'topPanel' }); + const { height: containerHeight } = useResizeObserver(resizeRef.current); + const [topPanelHeight, setTopPanelHeight] = useState(initialTopPanelHeight); + const [panelSizes, setPanelSizes] = useState({ topPanelSize: 0, mainPanelSize: 0 }); + + // EuiResizableContainer doesn't work properly when used with react-reverse-portal and + // will cancel the resize. To work around this we keep track of when resizes start and + // end to toggle the rendering of a transparent overlay which prevents the cancellation. + // EUI issue: https://github.com/elastic/eui/issues/6199 + const [resizeWithPortalsHackIsResizing, setResizeWithPortalsHackIsResizing] = useState(false); + const enableResizeWithPortalsHack = () => setResizeWithPortalsHackIsResizing(true); + const disableResizeWithPortalsHack = () => setResizeWithPortalsHackIsResizing(false); + const resizeWithPortalsHackFillCss = css` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + `; + const resizeWithPortalsHackButtonCss = css` + z-index: 3; + `; + const resizeWithPortalsHackButtonInnerCss = css` + ${resizeWithPortalsHackFillCss} + z-index: 1; + `; + const resizeWithPortalsHackOverlayCss = css` + ${resizeWithPortalsHackFillCss} + z-index: 2; + `; + + // Instead of setting the panel sizes directly, we convert the top panel height + // from a percentage of the container height to a pixel value. This will trigger + // the effect below to update the panel sizes. + const onPanelSizeChange = useCallback( + ({ [topPanelId]: topPanelSize }: { [key: string]: number }) => { + setTopPanelHeight(percentToPixels(containerHeight, topPanelSize)); + }, + [containerHeight, topPanelId] + ); + + // This effect will update the panel sizes based on the top panel height whenever + // it or the container height changes. This allows us to keep the height of the + // top panel panel fixed when the window is resized. + useEffect(() => { + if (!containerHeight) { + return; + } + + let topPanelSize: number; + let mainPanelSize: number; + + // If the container height is less than the minimum main content height + // plus the current top panel height, then we need to make some adjustments. + if (containerHeight < minMainPanelHeight + topPanelHeight) { + const newTopPanelHeight = containerHeight - minMainPanelHeight; + + // Try to make the top panel height fit within the container, but if it + // doesn't then just use the minimum heights. + if (newTopPanelHeight < minTopPanelHeight) { + topPanelSize = pixelsToPercent(containerHeight, minTopPanelHeight); + mainPanelSize = pixelsToPercent(containerHeight, minMainPanelHeight); + } else { + topPanelSize = pixelsToPercent(containerHeight, newTopPanelHeight); + mainPanelSize = 100 - topPanelSize; + } + } else { + topPanelSize = pixelsToPercent(containerHeight, topPanelHeight); + mainPanelSize = 100 - topPanelSize; + } + + setPanelSizes({ topPanelSize, mainPanelSize }); + }, [containerHeight, topPanelHeight, minTopPanelHeight, minMainPanelHeight]); + + const onResizeEnd = () => { + // We don't want the resize button to retain focus after the resize is complete, + // but EuiResizableContainer will force focus it onClick. To work around this we + // use setTimeout to wait until after onClick has been called before blurring. + if (resizeWithPortalsHackIsResizing && document.activeElement instanceof HTMLElement) { + const button = document.activeElement; + setTimeout(() => { + button.blur(); + }); + } + + disableResizeWithPortalsHack(); + }; + + return ( +
+ + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + {topPanel} + + + + + + {mainPanel} + + {resizeWithPortalsHackIsResizing ? ( +
+ ) : ( + <> + )} + + )} + +
+ ); +}; diff --git a/src/plugins/discover/public/components/view_mode_toggle/_index.scss b/src/plugins/discover/public/components/view_mode_toggle/_index.scss deleted file mode 100644 index a76c3453de32a..0000000000000 --- a/src/plugins/discover/public/components/view_mode_toggle/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'view_mode_toggle'; diff --git a/src/plugins/discover/public/components/view_mode_toggle/_view_mode_toggle.scss b/src/plugins/discover/public/components/view_mode_toggle/_view_mode_toggle.scss deleted file mode 100644 index 1009ab0511957..0000000000000 --- a/src/plugins/discover/public/components/view_mode_toggle/_view_mode_toggle.scss +++ /dev/null @@ -1,12 +0,0 @@ -.dscViewModeToggle { - padding-right: $euiSize; -} - -.fieldStatsButton { - display: flex; - align-items: center; -} - -.fieldStatsBetaBadge { - margin-left: $euiSizeXS; -} diff --git a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.test.tsx b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.test.tsx new file mode 100644 index 0000000000000..450d7c2816d7d --- /dev/null +++ b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.test.tsx @@ -0,0 +1,68 @@ +/* + * 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 { EuiTab } from '@elastic/eui'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import React from 'react'; +import { VIEW_MODE } from './constants'; +import { DocumentViewModeToggle } from './view_mode_toggle'; + +describe('Document view mode toggle component', () => { + const mountComponent = ({ + showFieldStatistics = true, + viewMode = VIEW_MODE.DOCUMENT_LEVEL, + setDiscoverViewMode = jest.fn(), + } = {}) => { + const serivces = { + uiSettings: { + get: () => showFieldStatistics, + }, + }; + + return mountWithIntl( + + + + ); + }; + + it('should render if SHOW_FIELD_STATISTICS is true', () => { + const component = mountComponent(); + expect(component.isEmptyRender()).toBe(false); + }); + + it('should not render if SHOW_FIELD_STATISTICS is false', () => { + const component = mountComponent({ showFieldStatistics: false }); + expect(component.isEmptyRender()).toBe(true); + }); + + it('should set the view mode to VIEW_MODE.DOCUMENT_LEVEL when dscViewModeDocumentButton is clicked', () => { + const setDiscoverViewMode = jest.fn(); + const component = mountComponent({ setDiscoverViewMode }); + component.find('[data-test-subj="dscViewModeDocumentButton"]').at(0).simulate('click'); + expect(setDiscoverViewMode).toHaveBeenCalledWith(VIEW_MODE.DOCUMENT_LEVEL); + }); + + it('should set the view mode to VIEW_MODE.AGGREGATED_LEVEL when dscViewModeFieldStatsButton is clicked', () => { + const setDiscoverViewMode = jest.fn(); + const component = mountComponent({ setDiscoverViewMode }); + component.find('[data-test-subj="dscViewModeFieldStatsButton"]').at(0).simulate('click'); + expect(setDiscoverViewMode).toHaveBeenCalledWith(VIEW_MODE.AGGREGATED_LEVEL); + }); + + it('should select the Documents tab if viewMode is VIEW_MODE.DOCUMENT_LEVEL', () => { + const component = mountComponent(); + expect(component.find(EuiTab).at(0).prop('isSelected')).toBe(true); + }); + + it('should select the Field statistics tab if viewMode is VIEW_MODE.AGGREGATED_LEVEL', () => { + const component = mountComponent({ viewMode: VIEW_MODE.AGGREGATED_LEVEL }); + expect(component.find(EuiTab).at(1).prop('isSelected')).toBe(true); + }); +}); diff --git a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx index af48673bf31be..f499df891ef34 100644 --- a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx +++ b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx @@ -6,12 +6,22 @@ * Side Public License, v 1. */ -import { EuiButtonGroup, EuiBetaBadge } from '@elastic/eui'; -import React, { useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; +import { + EuiTabs, + EuiTab, + useEuiPaddingSize, + EuiBetaBadge, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { i18n } from '@kbn/i18n'; import { VIEW_MODE } from './constants'; -import './_index.scss'; +import { SHOW_FIELD_STATISTICS } from '../../../common'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; export const DocumentViewModeToggle = ({ viewMode, @@ -20,23 +30,47 @@ export const DocumentViewModeToggle = ({ viewMode: VIEW_MODE; setDiscoverViewMode: (viewMode: VIEW_MODE) => void; }) => { - const toggleButtons = useMemo( - () => [ - { - id: VIEW_MODE.DOCUMENT_LEVEL, - label: i18n.translate('discover.viewModes.document.label', { - defaultMessage: 'Documents', - }), - 'data-test-subj': 'dscViewModeDocumentButton', - }, - { - id: VIEW_MODE.AGGREGATED_LEVEL, - label: ( -
+ const { uiSettings } = useDiscoverServices(); + + const tabsCss = css` + padding: 0 ${useEuiPaddingSize('s')}; + background-color: ${euiThemeVars.euiPageBackgroundColor}; + `; + + const badgeCellCss = css` + margin-left: ${useEuiPaddingSize('s')}; + `; + + const showViewModeToggle = uiSettings.get(SHOW_FIELD_STATISTICS) ?? false; + + if (!showViewModeToggle) { + return null; + } + + return ( + + setDiscoverViewMode(VIEW_MODE.DOCUMENT_LEVEL)} + className="dscViewModeToggle__tab" + data-test-subj="dscViewModeDocumentButton" + > + + + setDiscoverViewMode(VIEW_MODE.AGGREGATED_LEVEL)} + className="dscViewModeToggle__tab" + data-test-subj="dscViewModeFieldStatsButton" + > + + + + -
- ), - }, - ], - [] - ); - - return ( - setDiscoverViewMode(id as VIEW_MODE)} - data-test-subj={'dscViewModeToggle'} - /> + + + + ); }; diff --git a/test/functional/apps/discover/group1/_discover.ts b/test/functional/apps/discover/group1/_discover.ts index 3b4e85089db4f..39793c9c047f0 100644 --- a/test/functional/apps/discover/group1/_discover.ts +++ b/test/functional/apps/discover/group1/_discover.ts @@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const queryBar = getService('queryBar'); const inspector = getService('inspector'); const elasticChart = getService('elasticChart'); + const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); const defaultSettings = { @@ -342,5 +343,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.pauseAutoRefresh(); }); }); + + describe('resizable layout panels', () => { + it('should allow resizing the layout panels', async () => { + const resizeDistance = 100; + const topPanel = await testSubjects.find('dscResizablePanelTop'); + const mainPanel = await testSubjects.find('dscResizablePanelMain'); + const resizeButton = await testSubjects.find('dsc-resizable-button'); + const topPanelSize = (await topPanel.getPosition()).height; + const mainPanelSize = (await mainPanel.getPosition()).height; + await browser.dragAndDrop( + { location: resizeButton }, + { location: { x: 0, y: resizeDistance } } + ); + const newTopPanelSize = (await topPanel.getPosition()).height; + const newMainPanelSize = (await mainPanel.getPosition()).height; + expect(newTopPanelSize).to.be(topPanelSize + resizeDistance); + expect(newMainPanelSize).to.be(mainPanelSize - resizeDistance); + }); + }); }); } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index b066ae3b70d59..7fe192b985fa1 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2327,7 +2327,6 @@ "discover.viewModes.document.label": "Documents", "discover.viewModes.fieldStatistics.betaTitle": "Bêta", "discover.viewModes.fieldStatistics.label": "Statistiques de champ", - "discover.viewModes.legend": "Modes d'affichage", "embeddableApi.addPanel.savedObjectAddedToContainerSuccessMessageTitle": "{savedObjectName} a été ajouté.", "embeddableApi.attributeService.saveToLibraryError": "Une erreur s'est produite lors de l'enregistrement. Erreur : {errorMessage}.", "embeddableApi.errors.embeddableFactoryNotFound": "Impossible de charger {type}. Veuillez effectuer une mise à niveau vers la distribution par défaut d'Elasticsearch et de Kibana avec la licence appropriée.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7f1d687e3e25e..58700be1e9741 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2323,7 +2323,6 @@ "discover.viewModes.document.label": "ドキュメント", "discover.viewModes.fieldStatistics.betaTitle": "ベータ", "discover.viewModes.fieldStatistics.label": "フィールド統計情報", - "discover.viewModes.legend": "表示モード", "embeddableApi.addPanel.savedObjectAddedToContainerSuccessMessageTitle": "{savedObjectName} が追加されました", "embeddableApi.attributeService.saveToLibraryError": "保存中にエラーが発生しました。エラー:{errorMessage}", "embeddableApi.errors.embeddableFactoryNotFound": "{type} を読み込めません。Elasticsearch と Kibana のデフォルトのディストリビューションを適切なライセンスでアップグレードしてください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e9e751a71ee83..b4a7a585353d2 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2327,7 +2327,6 @@ "discover.viewModes.document.label": "文档", "discover.viewModes.fieldStatistics.betaTitle": "公测版", "discover.viewModes.fieldStatistics.label": "字段统计信息", - "discover.viewModes.legend": "视图模式", "embeddableApi.addPanel.savedObjectAddedToContainerSuccessMessageTitle": "{savedObjectName} 已添加", "embeddableApi.attributeService.saveToLibraryError": "保存时出错。错误:{errorMessage}", "embeddableApi.errors.embeddableFactoryNotFound": "{type} 无法加载。请升级到具有适当许可的默认 Elasticsearch 和 Kibana 分发。",