({ + isCollapsed: true, + toggle: () => {}, + }) + } + isChartAvailable={undefined} + renderedFor="root" + /> + ), }; const component = mountWithIntl( @@ -128,15 +151,36 @@ describe('Discover main content component', () => { expect(component.find(DiscoverDocuments).prop('viewModeToggle')).toBeDefined(); }); - it('should not show DocumentViewModeToggle when isPlainRecord is true', async () => { + it('should include DocumentViewModeToggle when isPlainRecord is true', async () => { const component = await mountComponent({ isPlainRecord: true }); - expect(component.find(DiscoverDocuments).prop('viewModeToggle')).toBeUndefined(); + expect(component.find(DiscoverDocuments).prop('viewModeToggle')).toBeDefined(); }); it('should show DocumentViewModeToggle for Field Statistics', async () => { const component = await mountComponent({ viewMode: VIEW_MODE.AGGREGATED_LEVEL }); expect(component.find(DocumentViewModeToggle).exists()).toBe(true); }); + + it('should include PanelsToggle when chart is available', async () => { + const component = await mountComponent({ isChartAvailable: true }); + expect(component.find(PanelsToggle).prop('isChartAvailable')).toBe(true); + expect(component.find(PanelsToggle).prop('renderedFor')).toBe('tabs'); + expect(component.find(EuiHorizontalRule).exists()).toBe(true); + }); + + it('should include PanelsToggle when chart is available and hidden', async () => { + const component = await mountComponent({ isChartAvailable: true, hideChart: true }); + expect(component.find(PanelsToggle).prop('isChartAvailable')).toBe(true); + expect(component.find(PanelsToggle).prop('renderedFor')).toBe('tabs'); + expect(component.find(EuiHorizontalRule).exists()).toBe(false); + }); + + it('should include PanelsToggle when chart is not available', async () => { + const component = await mountComponent({ isChartAvailable: false }); + expect(component.find(PanelsToggle).prop('isChartAvailable')).toBe(false); + expect(component.find(PanelsToggle).prop('renderedFor')).toBe('tabs'); + expect(component.find(EuiHorizontalRule).exists()).toBe(false); + }); }); describe('Document view', () => { 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 index 8b6ff5880d3dc3a..07a37e3ba1bc3df 100644 --- 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 @@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import { DragDrop, type DropType, DropOverlayWrapper } from '@kbn/dom-drag-drop'; -import React, { useCallback, useMemo } from 'react'; +import React, { ReactElement, useCallback, useMemo } from 'react'; import { DataView } from '@kbn/data-views-plugin/common'; import { METRIC_TYPE } from '@kbn/analytics'; import { i18n } from '@kbn/i18n'; @@ -21,6 +21,7 @@ import { FieldStatisticsTab } from '../field_stats_table'; import { DiscoverDocuments } from './discover_documents'; import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stats_table/constants'; import { useAppStateSelector } from '../../services/discover_app_state_container'; +import type { PanelsToggleProps } from '../../../../components/panels_toggle'; const DROP_PROPS = { value: { @@ -44,6 +45,8 @@ export interface DiscoverMainContentProps { onFieldEdited: () => Promise; onDropFieldToTable?: () => void; columns: string[]; + panelsToggle: ReactElement; + isChartAvailable?: boolean; // it will be injected by UnifiedHistogram } export const DiscoverMainContent = ({ @@ -55,6 +58,8 @@ export const DiscoverMainContent = ({ columns, stateContainer, onDropFieldToTable, + panelsToggle, + isChartAvailable, }: DiscoverMainContentProps) => { const { trackUiMetric } = useDiscoverServices(); @@ -76,10 +81,27 @@ export const DiscoverMainContent = ({ const isDropAllowed = Boolean(onDropFieldToTable); const viewModeToggle = useMemo(() => { - return !isPlainRecord ? ( - - ) : undefined; - }, [viewMode, setDiscoverViewMode, isPlainRecord]); + return ( + + ); + }, [ + viewMode, + setDiscoverViewMode, + isPlainRecord, + stateContainer, + panelsToggle, + isChartAvailable, + ]); const showChart = useAppStateSelector((state) => !state.hideChart); @@ -99,7 +121,7 @@ export const DiscoverMainContent = ({ responsive={false} data-test-subj="dscMainContent" > - {showChart && } + {showChart && isChartAvailable && } {viewMode === VIEW_MODE.DOCUMENT_LEVEL ? ( { const wrapper = mount( ({ + isCollapsed: true, + toggle: () => {}, + }) + } sidebarPanel={
} mainPanel={
} /> @@ -69,7 +74,12 @@ describe('DiscoverResizableLayout', () => { const wrapper = mount( ({ + isCollapsed: true, + toggle: () => {}, + }) + } sidebarPanel={
} mainPanel={
} /> @@ -82,7 +92,12 @@ describe('DiscoverResizableLayout', () => { const wrapper = mount( ({ + isCollapsed: true, + toggle: () => {}, + }) + } sidebarPanel={
} mainPanel={
} /> @@ -95,8 +110,11 @@ describe('DiscoverResizableLayout', () => { const wrapper = mount( ({ + isCollapsed: false, + toggle: () => {}, + }) } sidebarPanel={
} mainPanel={
} @@ -110,8 +128,11 @@ describe('DiscoverResizableLayout', () => { const wrapper = mount( ({ + isCollapsed: false, + toggle: () => {}, + }) } sidebarPanel={
} mainPanel={
} @@ -125,8 +146,11 @@ describe('DiscoverResizableLayout', () => { const wrapper = mount( ({ + isCollapsed: true, + toggle: () => {}, + }) } sidebarPanel={
} mainPanel={
} @@ -140,8 +164,11 @@ describe('DiscoverResizableLayout', () => { const wrapper = mount( ({ + isCollapsed: false, + toggle: () => {}, + }) } sidebarPanel={
} mainPanel={
} @@ -157,8 +184,11 @@ describe('DiscoverResizableLayout', () => { const wrapper = mount( ({ + isCollapsed: false, + toggle: () => {}, + }) } sidebarPanel={
} mainPanel={
} diff --git a/src/plugins/discover/public/application/main/components/layout/discover_resizable_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_resizable_layout.tsx index e0859617f005749..179914b9fb68a69 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_resizable_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_resizable_layout.tsx @@ -12,23 +12,23 @@ import { ResizableLayoutDirection, ResizableLayoutMode, } from '@kbn/resizable-layout'; -import type { UnifiedFieldListSidebarContainerApi } from '@kbn/unified-field-list'; import React, { ReactNode, useState } from 'react'; import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal'; import useLocalStorage from 'react-use/lib/useLocalStorage'; import useObservable from 'react-use/lib/useObservable'; -import { of } from 'rxjs'; +import { BehaviorSubject } from 'rxjs'; +import { SidebarToggleState } from '../../../types'; export const SIDEBAR_WIDTH_KEY = 'discover:sidebarWidth'; export const DiscoverResizableLayout = ({ container, - unifiedFieldListSidebarContainerApi, + sidebarToggleState$, sidebarPanel, mainPanel, }: { container: HTMLElement | null; - unifiedFieldListSidebarContainerApi: UnifiedFieldListSidebarContainerApi | null; + sidebarToggleState$: BehaviorSubject; sidebarPanel: ReactNode; mainPanel: ReactNode; }) => { @@ -45,10 +45,9 @@ export const DiscoverResizableLayout = ({ const minMainPanelWidth = euiTheme.base * 30; const [sidebarWidth, setSidebarWidth] = useLocalStorage(SIDEBAR_WIDTH_KEY, defaultSidebarWidth); - const isSidebarCollapsed = useObservable( - unifiedFieldListSidebarContainerApi?.isSidebarCollapsed$ ?? of(true), - true - ); + + const sidebarToggleState = useObservable(sidebarToggleState$); + const isSidebarCollapsed = sidebarToggleState?.isCollapsed ?? false; const isMobile = useIsWithinBreakpoints(['xs', 's']); const layoutMode = diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.tsx b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.tsx index ae73126afde88d1..068f21863de6c2b 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.tsx @@ -261,20 +261,14 @@ describe('useDiscoverHistogram', () => { hook.result.current.ref(api); }); stateContainer.appState.update({ hideChart: true, interval: '1m', breakdownField: 'test' }); - expect(api.setTotalHits).toHaveBeenCalled(); + expect(api.setTotalHits).not.toHaveBeenCalled(); expect(api.setChartHidden).toHaveBeenCalled(); expect(api.setTimeInterval).toHaveBeenCalled(); expect(api.setBreakdownField).toHaveBeenCalled(); - expect(Object.keys(params ?? {})).toEqual([ - 'totalHitsStatus', - 'totalHitsResult', - 'breakdownField', - 'timeInterval', - 'chartHidden', - ]); + expect(Object.keys(params ?? {})).toEqual(['breakdownField', 'timeInterval', 'chartHidden']); }); - it('should exclude totalHitsStatus and totalHitsResult from Unified Histogram state updates after the first load', async () => { + it('should exclude totalHitsStatus and totalHitsResult from Unified Histogram state updates', async () => { const stateContainer = getStateContainer(); const { hook } = await renderUseDiscoverHistogram({ stateContainer }); const containerState = stateContainer.appState.getState(); @@ -290,20 +284,13 @@ describe('useDiscoverHistogram', () => { api.setChartHidden = jest.fn((chartHidden) => { params = { ...params, chartHidden }; }); - api.setTotalHits = jest.fn((p) => { - params = { ...params, ...p }; - }); const subject$ = new BehaviorSubject(state); api.state$ = subject$; act(() => { hook.result.current.ref(api); }); stateContainer.appState.update({ hideChart: true }); - expect(Object.keys(params ?? {})).toEqual([ - 'totalHitsStatus', - 'totalHitsResult', - 'chartHidden', - ]); + expect(Object.keys(params ?? {})).toEqual(['chartHidden']); params = {}; stateContainer.appState.update({ hideChart: false }); act(() => { @@ -434,14 +421,14 @@ describe('useDiscoverHistogram', () => { act(() => { hook.result.current.ref(api); }); - expect(api.refetch).not.toHaveBeenCalled(); + expect(api.refetch).toHaveBeenCalled(); act(() => { savedSearchFetch$.next({ options: { reset: false, fetchMore: false }, searchSessionId: '1234', }); }); - expect(api.refetch).toHaveBeenCalled(); + expect(api.refetch).toHaveBeenCalledTimes(2); }); it('should skip the next refetch when hideChart changes from true to false', async () => { @@ -459,6 +446,7 @@ describe('useDiscoverHistogram', () => { act(() => { hook.result.current.ref(api); }); + expect(api.refetch).toHaveBeenCalled(); act(() => { hook.rerender({ ...initialProps, hideChart: true }); }); @@ -471,7 +459,7 @@ describe('useDiscoverHistogram', () => { searchSessionId: '1234', }); }); - expect(api.refetch).not.toHaveBeenCalled(); + expect(api.refetch).toHaveBeenCalledTimes(1); }); it('should skip the next refetch when fetching more', async () => { @@ -489,13 +477,14 @@ describe('useDiscoverHistogram', () => { act(() => { hook.result.current.ref(api); }); + expect(api.refetch).toHaveBeenCalledTimes(1); act(() => { savedSearchFetch$.next({ options: { reset: false, fetchMore: true }, searchSessionId: '1234', }); }); - expect(api.refetch).not.toHaveBeenCalled(); + expect(api.refetch).toHaveBeenCalledTimes(1); act(() => { savedSearchFetch$.next({ @@ -503,7 +492,7 @@ describe('useDiscoverHistogram', () => { searchSessionId: '1234', }); }); - expect(api.refetch).toHaveBeenCalled(); + expect(api.refetch).toHaveBeenCalledTimes(2); }); }); diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index 764145d72aac1c3..871edb89d15aab0 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -30,7 +30,6 @@ import { useDiscoverCustomization } from '../../../../customizations'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { getUiActions } from '../../../../kibana_services'; import { FetchStatus } from '../../../types'; -import { useDataState } from '../../hooks/use_data_state'; import type { InspectorAdapters } from '../../hooks/use_inspector'; import { checkHitCount, sendErrorTo } from '../../hooks/use_saved_search_messages'; import type { DiscoverStateContainer } from '../../services/discover_state'; @@ -68,9 +67,6 @@ export const useDiscoverHistogram = ({ breakdownField, } = stateContainer.appState.getState(); - const { fetchStatus: totalHitsStatus, result: totalHitsResult } = - savedSearchData$.totalHits$.getValue(); - return { localStorageKeyPrefix: 'discover', disableAutoFetching: true, @@ -78,11 +74,11 @@ export const useDiscoverHistogram = ({ chartHidden, timeInterval, breakdownField, - totalHitsStatus: totalHitsStatus.toString() as UnifiedHistogramFetchStatus, - totalHitsResult, + totalHitsStatus: UnifiedHistogramFetchStatus.loading, + totalHitsResult: undefined, }, }; - }, [savedSearchData$.totalHits$, stateContainer.appState]); + }, [stateContainer.appState]); /** * Sync Unified Histogram state with Discover state @@ -115,28 +111,6 @@ export const useDiscoverHistogram = ({ }; }, [inspectorAdapters, stateContainer.appState, unifiedHistogram?.state$]); - /** - * Override Unified Histgoram total hits with Discover partial results - */ - - const firstLoadComplete = useRef(false); - - const { fetchStatus: totalHitsStatus, result: totalHitsResult } = useDataState( - savedSearchData$.totalHits$ - ); - - useEffect(() => { - // We only want to show the partial results on the first load, - // or there will be a flickering effect as the loading spinner - // is quickly shown and hidden again on fetches - if (!firstLoadComplete.current) { - unifiedHistogram?.setTotalHits({ - totalHitsStatus: totalHitsStatus.toString() as UnifiedHistogramFetchStatus, - totalHitsResult, - }); - } - }, [totalHitsResult, totalHitsStatus, unifiedHistogram]); - /** * Sync URL query params with Unified Histogram */ @@ -181,7 +155,17 @@ export const useDiscoverHistogram = ({ return; } - const { recordRawType } = savedSearchData$.totalHits$.getValue(); + const { recordRawType, result: totalHitsResult } = savedSearchData$.totalHits$.getValue(); + + if ( + (status === UnifiedHistogramFetchStatus.loading || + status === UnifiedHistogramFetchStatus.uninitialized) && + totalHitsResult && + typeof result !== 'number' + ) { + // ignore the histogram initial loading state if discover state already has a total hits value + return; + } // Sync the totalHits$ observable with the unified histogram state savedSearchData$.totalHits$.next({ @@ -196,10 +180,6 @@ export const useDiscoverHistogram = ({ // Check the hits count to set a partial or no results state checkHitCount(savedSearchData$.main$, result); - - // Indicate the first load has completed so we don't show - // partial results on subsequent fetches - firstLoadComplete.current = true; } ); @@ -317,6 +297,11 @@ export const useDiscoverHistogram = ({ skipRefetch.current = false; }); + // triggering the initial request for total hits hook + if (!isPlainRecord && !skipRefetch.current) { + unifiedHistogram.refetch(); + } + return () => { subscription.unsubscribe(); }; @@ -326,14 +311,24 @@ export const useDiscoverHistogram = ({ const histogramCustomization = useDiscoverCustomization('unified_histogram'); + const servicesMemoized = useMemo(() => ({ ...services, uiActions: getUiActions() }), [services]); + + const filtersMemoized = useMemo( + () => [...(filters ?? []), ...customFilters], + [filters, customFilters] + ); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const timeRangeMemoized = useMemo(() => timeRange, [timeRange?.from, timeRange?.to]); + return { ref, getCreationOptions, - services: { ...services, uiActions: getUiActions() }, + services: servicesMemoized, dataView: isPlainRecord ? textBasedDataView : dataView, query: isPlainRecord ? textBasedQuery : query, - filters: [...(filters ?? []), ...customFilters], - timeRange, + filters: filtersMemoized, + timeRange: timeRangeMemoized, relativeTimeRange, columns, onFilter: histogramCustomization?.onFilter, diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx index c4558f4590c5b0e..0e5e9838f420b28 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx @@ -13,13 +13,13 @@ import { EuiProgress } from '@elastic/eui'; import { getDataTableRecords, realHits } from '../../../../__fixtures__/real_hits'; import { act } from 'react-dom/test-utils'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import React, { useState } from 'react'; +import React from 'react'; import { DiscoverSidebarResponsive, DiscoverSidebarResponsiveProps, } from './discover_sidebar_responsive'; import { DiscoverServices } from '../../../../build_services'; -import { FetchStatus } from '../../../types'; +import { FetchStatus, SidebarToggleState } from '../../../types'; import { AvailableFields$, DataDocuments$, @@ -37,7 +37,6 @@ import { buildDataTableRecord } from '@kbn/discover-utils'; import type { DataTableRecord } from '@kbn/discover-utils/types'; import type { DiscoverCustomizationId } from '../../../../customizations/customization_service'; import type { SearchBarCustomization } from '../../../../customizations'; -import type { UnifiedFieldListSidebarContainerApi } from '@kbn/unified-field-list'; const mockSearchBarCustomization: SearchBarCustomization = { id: 'search_bar', @@ -169,8 +168,10 @@ function getCompProps(options?: { hits?: DataTableRecord[] }): DiscoverSidebarRe trackUiMetric: jest.fn(), onFieldEdited: jest.fn(), onDataViewCreated: jest.fn(), - unifiedFieldListSidebarContainerApi: null, - setUnifiedFieldListSidebarContainerApi: jest.fn(), + sidebarToggleState$: new BehaviorSubject({ + isCollapsed: false, + toggle: () => {}, + }), }; } @@ -202,21 +203,10 @@ async function mountComponent( mockedServices.data.query.getState = jest.fn().mockImplementation(() => appState.getState()); await act(async () => { - const SidebarWrapper = () => { - const [api, setApi] = useState(null); - return ( - - ); - }; - comp = mountWithIntl( - + ); diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx index 3177adefdf49ba7..b820b63b461b374 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx @@ -6,9 +6,13 @@ * Side Public License, v 1. */ -import React, { useCallback, useEffect, useMemo, useReducer, useRef } from 'react'; +import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; import { UiCounterMetricType } from '@kbn/analytics'; import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; +import { EuiFlexGroup, EuiFlexItem, EuiHideFor, useEuiTheme } from '@elastic/eui'; +import useObservable from 'react-use/lib/useObservable'; +import { BehaviorSubject, of } from 'rxjs'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; import { DataViewPicker } from '@kbn/unified-search-plugin/public'; import { @@ -25,7 +29,7 @@ import { RecordRawType, } from '../../services/discover_data_state_container'; import { calcFieldCounts } from '../../utils/calc_field_counts'; -import { FetchStatus } from '../../../types'; +import { FetchStatus, SidebarToggleState } from '../../../types'; import { DISCOVER_TOUR_STEP_ANCHOR_IDS } from '../../../../components/discover_tour'; import { getUiActions } from '../../../../kibana_services'; import { @@ -134,8 +138,7 @@ export interface DiscoverSidebarResponsiveProps { */ fieldListVariant?: UnifiedFieldListSidebarContainerProps['variant']; - unifiedFieldListSidebarContainerApi: UnifiedFieldListSidebarContainerApi | null; - setUnifiedFieldListSidebarContainerApi: (api: UnifiedFieldListSidebarContainerApi) => void; + sidebarToggleState$: BehaviorSubject; } /** @@ -144,6 +147,9 @@ export interface DiscoverSidebarResponsiveProps { * Mobile: Data view selector is visible and a button to trigger a flyout with all elements */ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) { + const [unifiedFieldListSidebarContainerApi, setUnifiedFieldListSidebarContainerApi] = + useState(null); + const { euiTheme } = useEuiTheme(); const services = useDiscoverServices(); const { fieldListVariant, @@ -156,8 +162,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) onChangeDataView, onAddField, onRemoveField, - unifiedFieldListSidebarContainerApi, - setUnifiedFieldListSidebarContainerApi, + sidebarToggleState$, } = props; const [sidebarState, dispatchSidebarStateAction] = useReducer( discoverSidebarReducer, @@ -373,27 +378,55 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) [onRemoveField] ); - if (!selectedDataView) { - return null; - } + const isSidebarCollapsed = useObservable( + unifiedFieldListSidebarContainerApi?.sidebarVisibility.isCollapsed$ ?? of(false), + false + ); + + useEffect(() => { + sidebarToggleState$.next({ + isCollapsed: isSidebarCollapsed, + toggle: unifiedFieldListSidebarContainerApi?.sidebarVisibility.toggle, + }); + }, [isSidebarCollapsed, unifiedFieldListSidebarContainerApi, sidebarToggleState$]); return ( - + + + {selectedDataView ? ( + + ) : null} + + + + + ); } diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.ts b/src/plugins/discover/public/application/main/utils/fetch_all.ts index 289ad9e336b042b..16f2a1c50de560b 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.ts @@ -89,7 +89,7 @@ export function fetchAll( // Mark all subjects as loading sendLoadingMsg(dataSubjects.main$, { recordRawType }); sendLoadingMsg(dataSubjects.documents$, { recordRawType, query }); - sendLoadingMsg(dataSubjects.totalHits$, { recordRawType }); + // histogram will send `loading` for totalHits$ // Start fetching all required requests const response = @@ -116,9 +116,12 @@ export function fetchAll( meta: { fetchType }, }); } + + const currentTotalHits = dataSubjects.totalHits$.getValue(); // If the total hits (or chart) query is still loading, emit a partial // hit count that's at least our retrieved document count - if (dataSubjects.totalHits$.getValue().fetchStatus === FetchStatus.LOADING) { + if (currentTotalHits.fetchStatus === FetchStatus.LOADING && !currentTotalHits.result) { + // trigger `partial` only for the first request (if no total hits value yet) dataSubjects.totalHits$.next({ fetchStatus: FetchStatus.PARTIAL, result: records.length, diff --git a/src/plugins/discover/public/application/types.ts b/src/plugins/discover/public/application/types.ts index 70773d2db521fa4..d3f8ccd8f990d22 100644 --- a/src/plugins/discover/public/application/types.ts +++ b/src/plugins/discover/public/application/types.ts @@ -38,3 +38,8 @@ export interface RecordsFetchResponse { textBasedHeaderWarning?: string; interceptedWarnings?: SearchResponseWarning[]; } + +export interface SidebarToggleState { + isCollapsed: boolean; + toggle: undefined | ((isCollapsed: boolean) => void); +} diff --git a/src/plugins/discover/public/components/hits_counter/hits_counter.test.tsx b/src/plugins/discover/public/components/hits_counter/hits_counter.test.tsx new file mode 100644 index 000000000000000..8d84cdcef5a0c45 --- /dev/null +++ b/src/plugins/discover/public/components/hits_counter/hits_counter.test.tsx @@ -0,0 +1,96 @@ +/* + * 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 { mountWithIntl } from '@kbn/test-jest-helpers'; +import { HitsCounter, HitsCounterMode } from './hits_counter'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { BehaviorSubject } from 'rxjs'; +import { getDiscoverStateMock } from '../../__mocks__/discover_state.mock'; +import { DataTotalHits$ } from '../../application/main/services/discover_data_state_container'; +import { FetchStatus } from '../../application/types'; + +describe('hits counter', function () { + it('expect to render the number of hits', function () { + const stateContainer = getDiscoverStateMock({ isTimeBased: true }); + stateContainer.dataState.data$.totalHits$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + result: 1, + }) as DataTotalHits$; + const component1 = mountWithIntl( + + ); + expect(findTestSubject(component1, 'discoverQueryHits').text()).toBe('1'); + expect(findTestSubject(component1, 'discoverQueryTotalHits').text()).toBe('1'); + expect(component1.find('[data-test-subj="discoverQueryHits"]').length).toBe(1); + + const component2 = mountWithIntl( + + ); + expect(findTestSubject(component2, 'discoverQueryHits').text()).toBe('1'); + expect(findTestSubject(component2, 'discoverQueryTotalHits').text()).toBe('1 result'); + expect(component2.find('[data-test-subj="discoverQueryHits"]').length).toBe(1); + }); + + it('expect to render 1,899 hits if 1899 hits given', function () { + const stateContainer = getDiscoverStateMock({ isTimeBased: true }); + stateContainer.dataState.data$.totalHits$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + result: 1899, + }) as DataTotalHits$; + const component1 = mountWithIntl( + + ); + expect(findTestSubject(component1, 'discoverQueryHits').text()).toBe('1,899'); + expect(findTestSubject(component1, 'discoverQueryTotalHits').text()).toBe('1,899'); + + const component2 = mountWithIntl( + + ); + expect(findTestSubject(component2, 'discoverQueryHits').text()).toBe('1,899'); + expect(findTestSubject(component2, 'discoverQueryTotalHits').text()).toBe('1,899 results'); + }); + + it('should render a EuiLoadingSpinner when status is partial', () => { + const stateContainer = getDiscoverStateMock({ isTimeBased: true }); + stateContainer.dataState.data$.totalHits$ = new BehaviorSubject({ + fetchStatus: FetchStatus.PARTIAL, + result: 2, + }) as DataTotalHits$; + const component = mountWithIntl( + + ); + expect(component.find(EuiLoadingSpinner).length).toBe(1); + }); + + it('should render discoverQueryHitsPartial when status is partial', () => { + const stateContainer = getDiscoverStateMock({ isTimeBased: true }); + stateContainer.dataState.data$.totalHits$ = new BehaviorSubject({ + fetchStatus: FetchStatus.PARTIAL, + result: 2, + }) as DataTotalHits$; + const component = mountWithIntl( + + ); + expect(component.find('[data-test-subj="discoverQueryHitsPartial"]').length).toBe(1); + expect(findTestSubject(component, 'discoverQueryTotalHits').text()).toBe('≥2 results'); + }); + + it('should not render if loading', () => { + const stateContainer = getDiscoverStateMock({ isTimeBased: true }); + stateContainer.dataState.data$.totalHits$ = new BehaviorSubject({ + fetchStatus: FetchStatus.LOADING, + result: undefined, + }) as DataTotalHits$; + const component = mountWithIntl( + + ); + expect(component.isEmptyRender()).toBe(true); + }); +}); diff --git a/src/plugins/discover/public/components/hits_counter/hits_counter.tsx b/src/plugins/discover/public/components/hits_counter/hits_counter.tsx new file mode 100644 index 000000000000000..be3e819a5e073fc --- /dev/null +++ b/src/plugins/discover/public/components/hits_counter/hits_counter.tsx @@ -0,0 +1,117 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiText, EuiLoadingSpinner } from '@elastic/eui'; +import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; +import type { DiscoverStateContainer } from '../../application/main/services/discover_state'; +import { FetchStatus } from '../../application/types'; +import { useDataState } from '../../application/main/hooks/use_data_state'; + +export enum HitsCounterMode { + standalone = 'standalone', + appended = 'appended', +} + +export interface HitsCounterProps { + mode: HitsCounterMode; + stateContainer: DiscoverStateContainer; +} + +export const HitsCounter: React.FC = ({ mode, stateContainer }) => { + const totalHits$ = stateContainer.dataState.data$.totalHits$; + const totalHitsState = useDataState(totalHits$); + const hitsTotal = totalHitsState.result; + const hitsStatus = totalHitsState.fetchStatus; + + if (!hitsTotal && hitsStatus === FetchStatus.LOADING) { + return null; + } + + const formattedHits = ( + + + + ); + + const hitsCounterCss = css` + display: inline-flex; + `; + const hitsCounterTextCss = css` + overflow: hidden; + `; + + const element = ( + + + + + {hitsStatus === FetchStatus.PARTIAL && + (mode === HitsCounterMode.standalone ? ( + + ) : ( + + ))} + {hitsStatus !== FetchStatus.PARTIAL && + (mode === HitsCounterMode.standalone ? ( + + ) : ( + formattedHits + ))} + + + + {hitsStatus === FetchStatus.PARTIAL && ( + + + + )} + + ); + + return mode === HitsCounterMode.appended ? ( + <> + {' ('} + {element} + {')'} + + ) : ( + element + ); +}; diff --git a/src/plugins/unified_histogram/public/hits_counter/index.ts b/src/plugins/discover/public/components/hits_counter/index.ts similarity index 84% rename from src/plugins/unified_histogram/public/hits_counter/index.ts rename to src/plugins/discover/public/components/hits_counter/index.ts index 593608c9cac8699..8d7f69c3af27506 100644 --- a/src/plugins/unified_histogram/public/hits_counter/index.ts +++ b/src/plugins/discover/public/components/hits_counter/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { HitsCounter } from './hits_counter'; +export { HitsCounter, HitsCounterMode } from './hits_counter'; diff --git a/src/plugins/discover/public/components/panels_toggle/index.ts b/src/plugins/discover/public/components/panels_toggle/index.ts new file mode 100644 index 000000000000000..7586567d3665c35 --- /dev/null +++ b/src/plugins/discover/public/components/panels_toggle/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 { PanelsToggle, type PanelsToggleProps } from './panels_toggle'; diff --git a/src/plugins/discover/public/components/panels_toggle/panels_toggle.test.tsx b/src/plugins/discover/public/components/panels_toggle/panels_toggle.test.tsx new file mode 100644 index 000000000000000..54a41fbb9255b71 --- /dev/null +++ b/src/plugins/discover/public/components/panels_toggle/panels_toggle.test.tsx @@ -0,0 +1,206 @@ +/* + * 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 { mountWithIntl } from '@kbn/test-jest-helpers'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import { BehaviorSubject } from 'rxjs'; +import { getDiscoverStateMock } from '../../__mocks__/discover_state.mock'; +import { PanelsToggle, type PanelsToggleProps } from './panels_toggle'; +import { DiscoverAppStateProvider } from '../../application/main/services/discover_app_state_container'; +import { SidebarToggleState } from '../../application/types'; + +describe('Panels toggle component', () => { + const mountComponent = ({ + sidebarToggleState$, + isChartAvailable, + renderedFor, + hideChart, + }: Omit & { hideChart: boolean }) => { + const stateContainer = getDiscoverStateMock({ isTimeBased: true }); + const appStateContainer = stateContainer.appState; + appStateContainer.set({ + hideChart, + }); + + return mountWithIntl( + + + + ); + }; + + describe('inside histogram toolbar', function () { + it('should render correctly when sidebar is visible and histogram is visible', () => { + const sidebarToggleState$ = new BehaviorSubject({ + isCollapsed: false, + toggle: jest.fn(), + }); + const component = mountComponent({ + hideChart: false, + isChartAvailable: undefined, + renderedFor: 'histogram', + sidebarToggleState$, + }); + expect(findTestSubject(component, 'dscShowSidebarButton').exists()).toBe(false); + expect(findTestSubject(component, 'dscHideHistogramButton').exists()).toBe(true); + }); + + it('should render correctly when sidebar is collapsed and histogram is visible', () => { + const sidebarToggleState$ = new BehaviorSubject({ + isCollapsed: true, + toggle: jest.fn(), + }); + const component = mountComponent({ + hideChart: false, + isChartAvailable: undefined, + renderedFor: 'histogram', + sidebarToggleState$, + }); + expect(findTestSubject(component, 'dscShowSidebarButton').exists()).toBe(true); + expect(findTestSubject(component, 'dscHideHistogramButton').exists()).toBe(true); + + findTestSubject(component, 'dscShowSidebarButton').simulate('click'); + + expect(sidebarToggleState$.getValue().toggle).toHaveBeenCalledWith(false); + }); + }); + + describe('inside view mode tabs', function () { + it('should render correctly when sidebar is visible and histogram is visible', () => { + const sidebarToggleState$ = new BehaviorSubject({ + isCollapsed: false, + toggle: jest.fn(), + }); + const component = mountComponent({ + hideChart: false, + isChartAvailable: true, + renderedFor: 'tabs', + sidebarToggleState$, + }); + expect(findTestSubject(component, 'dscShowSidebarButton').exists()).toBe(false); + expect(findTestSubject(component, 'dscHideHistogramButton').exists()).toBe(false); + expect(findTestSubject(component, 'dscShowHistogramButton').exists()).toBe(false); + }); + + it('should render correctly when sidebar is visible and histogram is visible but chart is not available', () => { + const sidebarToggleState$ = new BehaviorSubject({ + isCollapsed: false, + toggle: jest.fn(), + }); + const component = mountComponent({ + hideChart: false, + isChartAvailable: false, + renderedFor: 'tabs', + sidebarToggleState$, + }); + expect(findTestSubject(component, 'dscShowSidebarButton').exists()).toBe(false); + expect(findTestSubject(component, 'dscHideHistogramButton').exists()).toBe(false); + expect(findTestSubject(component, 'dscShowHistogramButton').exists()).toBe(false); + }); + + it('should render correctly when sidebar is hidden and histogram is visible', () => { + const sidebarToggleState$ = new BehaviorSubject({ + isCollapsed: true, + toggle: jest.fn(), + }); + const component = mountComponent({ + hideChart: false, + isChartAvailable: true, + renderedFor: 'tabs', + sidebarToggleState$, + }); + expect(findTestSubject(component, 'dscShowSidebarButton').exists()).toBe(false); + expect(findTestSubject(component, 'dscHideHistogramButton').exists()).toBe(false); + expect(findTestSubject(component, 'dscShowHistogramButton').exists()).toBe(false); + }); + + it('should render correctly when sidebar is hidden and histogram is visible but chart is not available', () => { + const sidebarToggleState$ = new BehaviorSubject({ + isCollapsed: true, + toggle: jest.fn(), + }); + const component = mountComponent({ + hideChart: false, + isChartAvailable: false, + renderedFor: 'tabs', + sidebarToggleState$, + }); + expect(findTestSubject(component, 'dscShowSidebarButton').exists()).toBe(true); + expect(findTestSubject(component, 'dscHideHistogramButton').exists()).toBe(false); + expect(findTestSubject(component, 'dscShowHistogramButton').exists()).toBe(false); + }); + + it('should render correctly when sidebar is visible and histogram is hidden', () => { + const sidebarToggleState$ = new BehaviorSubject({ + isCollapsed: false, + toggle: jest.fn(), + }); + const component = mountComponent({ + hideChart: true, + isChartAvailable: true, + renderedFor: 'tabs', + sidebarToggleState$, + }); + expect(findTestSubject(component, 'dscShowSidebarButton').exists()).toBe(false); + expect(findTestSubject(component, 'dscShowHistogramButton').exists()).toBe(true); + }); + + it('should render correctly when sidebar is visible and histogram is hidden but chart is not available', () => { + const sidebarToggleState$ = new BehaviorSubject({ + isCollapsed: false, + toggle: jest.fn(), + }); + const component = mountComponent({ + hideChart: true, + isChartAvailable: false, + renderedFor: 'tabs', + sidebarToggleState$, + }); + expect(findTestSubject(component, 'dscShowSidebarButton').exists()).toBe(false); + expect(findTestSubject(component, 'dscShowHistogramButton').exists()).toBe(false); + expect(findTestSubject(component, 'dscHideHistogramButton').exists()).toBe(false); + }); + + it('should render correctly when sidebar is hidden and histogram is hidden', () => { + const sidebarToggleState$ = new BehaviorSubject({ + isCollapsed: true, + toggle: jest.fn(), + }); + const component = mountComponent({ + hideChart: true, + isChartAvailable: true, + renderedFor: 'tabs', + sidebarToggleState$, + }); + expect(findTestSubject(component, 'dscShowSidebarButton').exists()).toBe(true); + expect(findTestSubject(component, 'dscShowHistogramButton').exists()).toBe(true); + }); + + it('should render correctly when sidebar is hidden and histogram is hidden but chart is not available', () => { + const sidebarToggleState$ = new BehaviorSubject({ + isCollapsed: true, + toggle: jest.fn(), + }); + const component = mountComponent({ + hideChart: true, + isChartAvailable: false, + renderedFor: 'tabs', + sidebarToggleState$, + }); + expect(findTestSubject(component, 'dscShowSidebarButton').exists()).toBe(true); + expect(findTestSubject(component, 'dscShowHistogramButton').exists()).toBe(false); + expect(findTestSubject(component, 'dscHideHistogramButton').exists()).toBe(false); + }); + }); +}); diff --git a/src/plugins/discover/public/components/panels_toggle/panels_toggle.tsx b/src/plugins/discover/public/components/panels_toggle/panels_toggle.tsx new file mode 100644 index 000000000000000..bd04823affd80e1 --- /dev/null +++ b/src/plugins/discover/public/components/panels_toggle/panels_toggle.tsx @@ -0,0 +1,101 @@ +/* + * 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, { useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import useObservable from 'react-use/lib/useObservable'; +import { BehaviorSubject } from 'rxjs'; +import { IconButtonGroup } from '@kbn/shared-ux-button-toolbar'; +import { useAppStateSelector } from '../../application/main/services/discover_app_state_container'; +import { DiscoverStateContainer } from '../../application/main/services/discover_state'; +import { SidebarToggleState } from '../../application/types'; + +export interface PanelsToggleProps { + stateContainer: DiscoverStateContainer; + sidebarToggleState$: BehaviorSubject; + renderedFor: 'histogram' | 'prompt' | 'tabs' | 'root'; + isChartAvailable: boolean | undefined; // it will be injected in `DiscoverMainContent` when rendering View mode tabs or in `DiscoverLayout` when rendering No results or Error prompt +} + +/** + * An element of this component is created in DiscoverLayout + * @param stateContainer + * @param sidebarToggleState$ + * @param renderedIn + * @param isChartAvailable + * @constructor + */ +export const PanelsToggle: React.FC = ({ + stateContainer, + sidebarToggleState$, + renderedFor, + isChartAvailable, +}) => { + const isChartHidden = useAppStateSelector((state) => Boolean(state.hideChart)); + + const onToggleChart = useCallback(() => { + stateContainer.appState.update({ hideChart: !isChartHidden }); + }, [stateContainer, isChartHidden]); + + const sidebarToggleState = useObservable(sidebarToggleState$); + const isSidebarCollapsed = sidebarToggleState?.isCollapsed ?? false; + + const isInsideHistogram = renderedFor === 'histogram'; + const isInsideDiscoverContent = !isInsideHistogram; + + const buttons = [ + ...((isInsideHistogram && isSidebarCollapsed) || + (isInsideDiscoverContent && isSidebarCollapsed && (isChartHidden || !isChartAvailable)) + ? [ + { + label: i18n.translate('discover.panelsToggle.showSidebarButton', { + defaultMessage: 'Show sidebar', + }), + iconType: 'transitionLeftIn', + 'data-test-subj': 'dscShowSidebarButton', + 'aria-expanded': !isSidebarCollapsed, + 'aria-controls': 'discover-sidebar', + onClick: () => sidebarToggleState?.toggle?.(false), + }, + ] + : []), + ...(isInsideHistogram || (isInsideDiscoverContent && isChartAvailable && isChartHidden) + ? [ + { + label: isChartHidden + ? i18n.translate('discover.panelsToggle.showChartButton', { + defaultMessage: 'Show chart', + }) + : i18n.translate('discover.panelsToggle.hideChartButton', { + defaultMessage: 'Hide chart', + }), + iconType: isChartHidden ? 'transitionTopIn' : 'transitionTopOut', + 'data-test-subj': isChartHidden ? 'dscShowHistogramButton' : 'dscHideHistogramButton', + 'aria-expanded': !isChartHidden, + 'aria-controls': 'unifiedHistogramCollapsablePanel', + onClick: onToggleChart, + }, + ] + : []), + ]; + + if (!buttons.length) { + return null; + } + + return ( + + ); +}; 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 index 7c17e5e1a31ef54..e1788389d3caff2 100644 --- 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 @@ -11,12 +11,18 @@ import { VIEW_MODE } from '../../../common/constants'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; +import { findTestSubject } from '@elastic/eui/lib/test'; import { DocumentViewModeToggle } from './view_mode_toggle'; +import { BehaviorSubject } from 'rxjs'; +import { getDiscoverStateMock } from '../../__mocks__/discover_state.mock'; +import { DataTotalHits$ } from '../../application/main/services/discover_data_state_container'; +import { FetchStatus } from '../../application/types'; describe('Document view mode toggle component', () => { const mountComponent = ({ showFieldStatistics = true, viewMode = VIEW_MODE.DOCUMENT_LEVEL, + isTextBasedQuery = false, setDiscoverViewMode = jest.fn(), } = {}) => { const serivces = { @@ -25,21 +31,40 @@ describe('Document view mode toggle component', () => { }, }; + const stateContainer = getDiscoverStateMock({ isTimeBased: true }); + stateContainer.dataState.data$.totalHits$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + result: 10, + }) as DataTotalHits$; + return mountWithIntl( - + ); }; it('should render if SHOW_FIELD_STATISTICS is true', () => { const component = mountComponent(); - expect(component.isEmptyRender()).toBe(false); + expect(findTestSubject(component, 'dscViewModeToggle').exists()).toBe(true); + expect(findTestSubject(component, 'discoverQueryTotalHits').exists()).toBe(true); }); it('should not render if SHOW_FIELD_STATISTICS is false', () => { const component = mountComponent({ showFieldStatistics: false }); - expect(component.isEmptyRender()).toBe(true); + expect(findTestSubject(component, 'dscViewModeToggle').exists()).toBe(false); + expect(findTestSubject(component, 'discoverQueryTotalHits').exists()).toBe(true); + }); + + it('should not render if text-based', () => { + const component = mountComponent({ isTextBasedQuery: true }); + expect(findTestSubject(component, 'dscViewModeToggle').exists()).toBe(false); + expect(findTestSubject(component, 'discoverQueryTotalHits').exists()).toBe(true); }); it('should set the view mode to VIEW_MODE.DOCUMENT_LEVEL when dscViewModeDocumentButton is clicked', () => { 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 79c9213e763951a..147486ac6dc6e4d 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,19 +6,27 @@ * Side Public License, v 1. */ -import React, { useMemo } from 'react'; -import { EuiTab, EuiTabs, useEuiTheme } from '@elastic/eui'; +import React, { useMemo, ReactElement } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiTab, EuiTabs, useEuiTheme } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; import { DOC_TABLE_LEGACY, SHOW_FIELD_STATISTICS } from '@kbn/discover-utils'; import { VIEW_MODE } from '../../../common/constants'; import { useDiscoverServices } from '../../hooks/use_discover_services'; +import { DiscoverStateContainer } from '../../application/main/services/discover_state'; +import { HitsCounter, HitsCounterMode } from '../hits_counter'; export const DocumentViewModeToggle = ({ viewMode, + isTextBasedQuery, + prepend, + stateContainer, setDiscoverViewMode, }: { viewMode: VIEW_MODE; + isTextBasedQuery: boolean; + prepend?: ReactElement; + stateContainer: DiscoverStateContainer; setDiscoverViewMode: (viewMode: VIEW_MODE) => void; }) => { const { euiTheme } = useEuiTheme(); @@ -26,10 +34,12 @@ export const DocumentViewModeToggle = ({ const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]); const includesNormalTabsStyle = viewMode === VIEW_MODE.AGGREGATED_LEVEL || isLegacy; - const tabsPadding = includesNormalTabsStyle ? euiTheme.size.s : 0; - const tabsCss = css` - padding: ${tabsPadding} ${tabsPadding} 0 ${tabsPadding}; + const containerPadding = includesNormalTabsStyle ? euiTheme.size.s : 0; + const containerCss = css` + padding: ${containerPadding} ${containerPadding} 0 ${containerPadding}; + `; + const tabsCss = css` .euiTab__content { line-height: ${euiTheme.size.xl}; } @@ -37,29 +47,52 @@ export const DocumentViewModeToggle = ({ const showViewModeToggle = uiSettings.get(SHOW_FIELD_STATISTICS) ?? false; - if (!showViewModeToggle) { - return null; - } - return ( - - setDiscoverViewMode(VIEW_MODE.DOCUMENT_LEVEL)} - data-test-subj="dscViewModeDocumentButton" - > - - - setDiscoverViewMode(VIEW_MODE.AGGREGATED_LEVEL)} - data-test-subj="dscViewModeFieldStatsButton" - > - - - + + {prepend && ( + + {prepend} + + )} + + {isTextBasedQuery || !showViewModeToggle ? ( + + ) : ( + + setDiscoverViewMode(VIEW_MODE.DOCUMENT_LEVEL)} + data-test-subj="dscViewModeDocumentButton" + > + + + + setDiscoverViewMode(VIEW_MODE.AGGREGATED_LEVEL)} + data-test-subj="dscViewModeFieldStatsButton" + > + + + + )} + + ); }; diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index fe06c9323246029..b75f27c9266f85a 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -78,6 +78,7 @@ "@kbn/rule-data-utils", "@kbn/core-chrome-browser", "@kbn/core-plugins-server", + "@kbn/shared-ux-button-toolbar", "@kbn/serverless", "@kbn/deeplinks-observability" ], diff --git a/src/plugins/unified_histogram/README.md b/src/plugins/unified_histogram/README.md index 229af7851d8a3cb..4509f28a7a61eef 100755 --- a/src/plugins/unified_histogram/README.md +++ b/src/plugins/unified_histogram/README.md @@ -49,9 +49,6 @@ return ( // Pass a ref to the containing element to // handle top panel resize functionality resizeRef={resizeRef} - // Optionally append an element after the - // hits counter display - appendHitsCounter={} > @@ -165,7 +162,6 @@ return ( searchSessionId={searchSessionId} requestAdapter={requestAdapter} resizeRef={resizeRef} - appendHitsCounter={} > diff --git a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.test.tsx b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.test.tsx index c0c20a1e1a80ea3..86ec06ba48e677b 100644 --- a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.test.tsx +++ b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.test.tsx @@ -6,68 +6,132 @@ * Side Public License, v 1. */ -import { EuiComboBox } from '@elastic/eui'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { render, act, screen } from '@testing-library/react'; import React from 'react'; import { UnifiedHistogramBreakdownContext } from '../types'; import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; import { BreakdownFieldSelector } from './breakdown_field_selector'; -import { fieldSupportsBreakdown } from './utils/field_supports_breakdown'; describe('BreakdownFieldSelector', () => { - it('should pass fields that support breakdown as options to the EuiComboBox', () => { + it('should render correctly', () => { const onBreakdownFieldChange = jest.fn(); const breakdown: UnifiedHistogramBreakdownContext = { field: undefined, }; - const wrapper = mountWithIntl( + + render( ); - const comboBox = wrapper.find(EuiComboBox); - expect(comboBox.prop('options')).toEqual( - dataViewWithTimefieldMock.fields - .filter(fieldSupportsBreakdown) - .map((field) => ({ label: field.displayName, value: field.name })) - .sort((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase())) - ); + + const button = screen.getByTestId('unifiedHistogramBreakdownSelectorButton'); + expect(button.getAttribute('data-selected-value')).toBe(null); + + act(() => { + button.click(); + }); + + const options = screen.getAllByRole('option'); + expect( + options.map((option) => ({ + label: option.getAttribute('title'), + value: option.getAttribute('value'), + checked: option.getAttribute('aria-checked'), + })) + ).toMatchInlineSnapshot(` + Array [ + Object { + "checked": "true", + "label": "No breakdown", + "value": "__EMPTY_SELECTOR_OPTION__", + }, + Object { + "checked": "false", + "label": "bytes", + "value": "bytes", + }, + Object { + "checked": "false", + "label": "extension", + "value": "extension", + }, + ] + `); }); - it('should pass selectedOptions to the EuiComboBox if breakdown.field is defined', () => { + it('should mark the option as checked if breakdown.field is defined', () => { const onBreakdownFieldChange = jest.fn(); const field = dataViewWithTimefieldMock.fields.find((f) => f.name === 'extension')!; const breakdown: UnifiedHistogramBreakdownContext = { field }; - const wrapper = mountWithIntl( + + render( ); - const comboBox = wrapper.find(EuiComboBox); - expect(comboBox.prop('selectedOptions')).toEqual([ - { label: field.displayName, value: field.name }, - ]); + + const button = screen.getByTestId('unifiedHistogramBreakdownSelectorButton'); + expect(button.getAttribute('data-selected-value')).toBe('extension'); + + act(() => { + button.click(); + }); + + const options = screen.getAllByRole('option'); + expect( + options.map((option) => ({ + label: option.getAttribute('title'), + value: option.getAttribute('value'), + checked: option.getAttribute('aria-checked'), + })) + ).toMatchInlineSnapshot(` + Array [ + Object { + "checked": "false", + "label": "No breakdown", + "value": "__EMPTY_SELECTOR_OPTION__", + }, + Object { + "checked": "false", + "label": "bytes", + "value": "bytes", + }, + Object { + "checked": "true", + "label": "extension", + "value": "extension", + }, + ] + `); }); it('should call onBreakdownFieldChange with the selected field when the user selects a field', () => { const onBreakdownFieldChange = jest.fn(); + const selectedField = dataViewWithTimefieldMock.fields.find((f) => f.name === 'bytes')!; const breakdown: UnifiedHistogramBreakdownContext = { field: undefined, }; - const wrapper = mountWithIntl( + render( ); - const comboBox = wrapper.find(EuiComboBox); - const selectedField = dataViewWithTimefieldMock.fields.find((f) => f.name === 'extension')!; - comboBox.prop('onChange')!([{ label: selectedField.displayName, value: selectedField.name }]); + + act(() => { + screen.getByTestId('unifiedHistogramBreakdownSelectorButton').click(); + }); + + act(() => { + screen.getByTitle('bytes').click(); + }); + expect(onBreakdownFieldChange).toHaveBeenCalledWith(selectedField); }); }); diff --git a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx index 77e00e157d62ba2..78df66f50873efa 100644 --- a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx +++ b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx @@ -6,14 +6,20 @@ * Side Public License, v 1. */ -import { EuiComboBox, EuiComboBoxOptionOption, EuiToolTip, useEuiTheme } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { EuiSelectableOption } from '@elastic/eui'; +import { FieldIcon, getFieldIconProps } from '@kbn/field-utils'; import { css } from '@emotion/react'; -import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; -import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import { i18n } from '@kbn/i18n'; -import React, { useCallback, useState } from 'react'; import { UnifiedHistogramBreakdownContext } from '../types'; import { fieldSupportsBreakdown } from './utils/field_supports_breakdown'; +import { + ToolbarSelector, + ToolbarSelectorProps, + EMPTY_OPTION, + SelectableEntry, +} from './toolbar_selector'; export interface BreakdownFieldSelectorProps { dataView: DataView; @@ -21,77 +27,83 @@ export interface BreakdownFieldSelectorProps { onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void; } -const TRUNCATION_PROPS = { truncation: 'middle' as const }; -const SINGLE_SELECTION = { asPlainText: true }; - export const BreakdownFieldSelector = ({ dataView, breakdown, onBreakdownFieldChange, }: BreakdownFieldSelectorProps) => { - const fieldOptions = dataView.fields - .filter(fieldSupportsBreakdown) - .map((field) => ({ label: field.displayName, value: field.name })) - .sort((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase())); + const fieldOptions: SelectableEntry[] = useMemo(() => { + const options: SelectableEntry[] = dataView.fields + .filter(fieldSupportsBreakdown) + .map((field) => ({ + key: field.name, + label: field.displayName, + value: field.name, + checked: + breakdown?.field?.name === field.name + ? ('on' as EuiSelectableOption['checked']) + : undefined, + prepend: ( + + + + ), + })) + .sort((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase())); - const selectedFields = breakdown.field - ? [{ label: breakdown.field.displayName, value: breakdown.field.name }] - : []; + options.unshift({ + key: EMPTY_OPTION, + value: EMPTY_OPTION, + label: i18n.translate('unifiedHistogram.breakdownFieldSelector.noBreakdownButtonLabel', { + defaultMessage: 'No breakdown', + }), + checked: !breakdown?.field ? ('on' as EuiSelectableOption['checked']) : undefined, + }); - const onFieldChange = useCallback( - (newOptions: EuiComboBoxOptionOption[]) => { - const field = newOptions.length - ? dataView.fields.find((currentField) => currentField.name === newOptions[0].value) - : undefined; + return options; + }, [dataView, breakdown.field]); + const onChange: ToolbarSelectorProps['onChange'] = useCallback( + (chosenOption) => { + const field = chosenOption?.value + ? dataView.fields.find((currentField) => currentField.name === chosenOption.value) + : undefined; onBreakdownFieldChange?.(field); }, [dataView.fields, onBreakdownFieldChange] ); - const [fieldPopoverDisabled, setFieldPopoverDisabled] = useState(false); - const disableFieldPopover = useCallback(() => setFieldPopoverDisabled(true), []); - const enableFieldPopover = useCallback( - () => setTimeout(() => setFieldPopoverDisabled(false)), - [] - ); - - const { euiTheme } = useEuiTheme(); - const breakdownCss = css` - width: 100%; - max-width: ${euiTheme.base * 22}px; - `; - - const panelMinWidth = calculateWidthFromEntries(fieldOptions, ['label']); - return ( - - - + ); }; diff --git a/src/plugins/unified_histogram/public/chart/chart.test.tsx b/src/plugins/unified_histogram/public/chart/chart.test.tsx index d561a3310ceae51..474da6bce5bf76f 100644 --- a/src/plugins/unified_histogram/public/chart/chart.test.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.test.tsx @@ -18,11 +18,11 @@ import type { ReactWrapper } from 'enzyme'; import { unifiedHistogramServicesMock } from '../__mocks__/services'; import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; import { of } from 'rxjs'; -import { HitsCounter } from '../hits_counter'; import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; import { dataViewMock } from '../__mocks__/data_view'; import { BreakdownFieldSelector } from './breakdown_field_selector'; import { SuggestionSelector } from './suggestion_selector'; +import { checkChartAvailability } from './check_chart_availability'; import { currentSuggestionMock, allSuggestionsMock } from '../__mocks__/suggestions'; @@ -33,6 +33,7 @@ jest.mock('./hooks/use_edit_visualization', () => ({ })); async function mountComponent({ + customToggle, noChart, noHits, noBreakdown, @@ -45,6 +46,7 @@ async function mountComponent({ hasDashboardPermissions, isChartLoading, }: { + customToggle?: ReactElement; noChart?: boolean; noHits?: boolean; noBreakdown?: boolean; @@ -70,6 +72,19 @@ async function mountComponent({ } as unknown as Capabilities, }; + const chart = noChart + ? undefined + : { + status: 'complete' as UnifiedHistogramFetchStatus, + hidden: chartHidden, + timeInterval: 'auto', + bucketInterval: { + scaled: true, + description: 'test', + scale: 2, + }, + }; + const props = { dataView, query: { @@ -85,28 +100,18 @@ async function mountComponent({ status: 'complete' as UnifiedHistogramFetchStatus, number: 2, }, - chart: noChart - ? undefined - : { - status: 'complete' as UnifiedHistogramFetchStatus, - hidden: chartHidden, - timeInterval: 'auto', - bucketInterval: { - scaled: true, - description: 'test', - scale: 2, - }, - }, + chart, breakdown: noBreakdown ? undefined : { field: undefined }, currentSuggestion, allSuggestions, isChartLoading: Boolean(isChartLoading), isPlainRecord, appendHistogram, - onResetChartHeight: jest.fn(), onChartHiddenChange: jest.fn(), onTimeIntervalChange: jest.fn(), withDefaultActions: undefined, + isChartAvailable: checkChartAvailability({ chart, dataView, isPlainRecord }), + renderCustomChartToggleActions: customToggle ? () => customToggle : undefined, }; let instance: ReactWrapper = {} as ReactWrapper; @@ -126,16 +131,33 @@ describe('Chart', () => { test('render when chart is undefined', async () => { const component = await mountComponent({ noChart: true }); - expect( - component.find('[data-test-subj="unifiedHistogramChartOptionsToggle"]').exists() - ).toBeFalsy(); + expect(component.find('[data-test-subj="unifiedHistogramToggleChartButton"]').exists()).toBe( + true + ); + }); + + test('should render a custom toggle when provided', async () => { + const component = await mountComponent({ + customToggle: , + }); + expect(component.find('[data-test-subj="custom-toggle"]').exists()).toBe(true); + expect(component.find('[data-test-subj="unifiedHistogramToggleChartButton"]').exists()).toBe( + false + ); + }); + + test('should not render when custom toggle is provided and chart is hidden', async () => { + const component = await mountComponent({ customToggle: , chartHidden: true }); + expect(component.find('[data-test-subj="unifiedHistogramChartPanelHidden"]').exists()).toBe( + true + ); }); test('render when chart is defined and onEditVisualization is undefined', async () => { mockUseEditVisualization = undefined; const component = await mountComponent(); expect( - component.find('[data-test-subj="unifiedHistogramChartOptionsToggle"]').exists() + component.find('[data-test-subj="unifiedHistogramToggleChartButton"]').exists() ).toBeTruthy(); expect( component.find('[data-test-subj="unifiedHistogramEditVisualization"]').exists() @@ -145,7 +167,7 @@ describe('Chart', () => { test('render when chart is defined and onEditVisualization is defined', async () => { const component = await mountComponent(); expect( - component.find('[data-test-subj="unifiedHistogramChartOptionsToggle"]').exists() + component.find('[data-test-subj="unifiedHistogramToggleChartButton"]').exists() ).toBeTruthy(); expect( component.find('[data-test-subj="unifiedHistogramEditVisualization"]').exists() @@ -155,7 +177,7 @@ describe('Chart', () => { test('render when chart.hidden is true', async () => { const component = await mountComponent({ chartHidden: true }); expect( - component.find('[data-test-subj="unifiedHistogramChartOptionsToggle"]').exists() + component.find('[data-test-subj="unifiedHistogramToggleChartButton"]').exists() ).toBeTruthy(); expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBeFalsy(); }); @@ -163,7 +185,7 @@ describe('Chart', () => { test('render when chart.hidden is false', async () => { const component = await mountComponent({ chartHidden: false }); expect( - component.find('[data-test-subj="unifiedHistogramChartOptionsToggle"]').exists() + component.find('[data-test-subj="unifiedHistogramToggleChartButton"]').exists() ).toBeTruthy(); expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBeTruthy(); }); @@ -171,7 +193,7 @@ describe('Chart', () => { test('render when is text based and not timebased', async () => { const component = await mountComponent({ isPlainRecord: true, dataView: dataViewMock }); expect( - component.find('[data-test-subj="unifiedHistogramChartOptionsToggle"]').exists() + component.find('[data-test-subj="unifiedHistogramToggleChartButton"]').exists() ).toBeTruthy(); expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBeTruthy(); }); @@ -187,22 +209,12 @@ describe('Chart', () => { await act(async () => { component .find('[data-test-subj="unifiedHistogramEditVisualization"]') - .first() + .last() .simulate('click'); }); expect(mockUseEditVisualization).toHaveBeenCalled(); }); - it('should render HitsCounter when hits is defined', async () => { - const component = await mountComponent(); - expect(component.find(HitsCounter).exists()).toBeTruthy(); - }); - - it('should not render HitsCounter when hits is undefined', async () => { - const component = await mountComponent({ noHits: true }); - expect(component.find(HitsCounter).exists()).toBeFalsy(); - }); - it('should render the element passed to appendHistogram', async () => { const appendHistogram =
; const component = await mountComponent({ appendHistogram }); diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index d494786240eda66..657f27a72c0702a 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -8,28 +8,19 @@ import React, { ReactElement, useMemo, useState, useEffect, useCallback, memo } from 'react'; import type { Observable } from 'rxjs'; -import { - EuiButtonIcon, - EuiContextMenu, - EuiFlexGroup, - EuiFlexItem, - EuiPopover, - EuiToolTip, - EuiProgress, -} from '@elastic/eui'; +import { IconButtonGroup, type IconButtonGroupProps } from '@kbn/shared-ux-button-toolbar'; +import { EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { EmbeddableComponentProps, Suggestion, LensEmbeddableOutput, } from '@kbn/lens-plugin/public'; -import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/public'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; import type { LensEmbeddableInput } from '@kbn/lens-plugin/public'; import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; import { Subject } from 'rxjs'; -import { HitsCounter } from '../hits_counter'; import { Histogram } from './histogram'; -import { useChartPanels } from './hooks/use_chart_panels'; import type { UnifiedHistogramBreakdownContext, UnifiedHistogramChartContext, @@ -43,6 +34,7 @@ import type { } from '../types'; import { BreakdownFieldSelector } from './breakdown_field_selector'; import { SuggestionSelector } from './suggestion_selector'; +import { TimeIntervalSelector } from './time_interval_selector'; import { useTotalHits } from './hooks/use_total_hits'; import { useRequestParams } from './hooks/use_request_params'; import { useChartStyles } from './hooks/use_chart_styles'; @@ -53,6 +45,8 @@ import { useRefetch } from './hooks/use_refetch'; import { useEditVisualization } from './hooks/use_edit_visualization'; export interface ChartProps { + isChartAvailable: boolean; + hiddenPanel?: boolean; className?: string; services: UnifiedHistogramServices; dataView: DataView; @@ -67,7 +61,7 @@ export interface ChartProps { hits?: UnifiedHistogramHitsContext; chart?: UnifiedHistogramChartContext; breakdown?: UnifiedHistogramBreakdownContext; - appendHitsCounter?: ReactElement; + renderCustomChartToggleActions?: () => ReactElement | undefined; appendHistogram?: ReactElement; disableAutoFetching?: boolean; disableTriggers?: LensEmbeddableInput['disableTriggers']; @@ -78,7 +72,6 @@ export interface ChartProps { isOnHistogramMode?: boolean; histogramQuery?: AggregateQuery; isChartLoading?: boolean; - onResetChartHeight?: () => void; onChartHiddenChange?: (chartHidden: boolean) => void; onTimeIntervalChange?: (timeInterval: string) => void; onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void; @@ -93,6 +86,7 @@ export interface ChartProps { const HistogramMemoized = memo(Histogram); export function Chart({ + isChartAvailable, className, services, dataView, @@ -107,7 +101,7 @@ export function Chart({ currentSuggestion, allSuggestions, isPlainRecord, - appendHitsCounter, + renderCustomChartToggleActions, appendHistogram, disableAutoFetching, disableTriggers, @@ -118,7 +112,6 @@ export function Chart({ isOnHistogramMode, histogramQuery, isChartLoading, - onResetChartHeight, onChartHiddenChange, onTimeIntervalChange, onSuggestionChange, @@ -131,33 +124,12 @@ export function Chart({ }: ChartProps) { const [isSaveModalVisible, setIsSaveModalVisible] = useState(false); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); - const { - showChartOptionsPopover, - chartRef, - toggleChartOptions, - closeChartOptions, - toggleHideChart, - } = useChartActions({ + const { chartRef, toggleHideChart } = useChartActions({ chart, onChartHiddenChange, }); - const panels = useChartPanels({ - chart, - toggleHideChart, - onTimeIntervalChange, - closePopover: closeChartOptions, - onResetChartHeight, - isPlainRecord, - }); - - const chartVisible = !!( - chart && - !chart.hidden && - dataView.id && - dataView.type !== DataViewType.ROLLUP && - (isPlainRecord || (!isPlainRecord && dataView.isTimeBased())) - ); + const chartVisible = isChartAvailable && !!chart && !chart.hidden; const input$ = useMemo( () => originalInput$ ?? new Subject(), @@ -201,17 +173,7 @@ export function Chart({ isPlainRecord, }); - const { - resultCountCss, - resultCountInnerCss, - resultCountTitleCss, - resultCountToggleCss, - histogramCss, - breakdownFieldSelectorGroupCss, - breakdownFieldSelectorItemCss, - suggestionsSelectorItemCss, - chartToolButtonCss, - } = useChartStyles(chartVisible); + const { chartToolbarCss, histogramCss } = useChartStyles(chartVisible); const lensAttributesContext = useMemo( () => @@ -258,165 +220,135 @@ export function Chart({ lensAttributes: lensAttributesContext.attributes, isPlainRecord, }); + + const a11yCommonProps = { + id: 'unifiedHistogramCollapsablePanel', + }; + + if (Boolean(renderCustomChartToggleActions) && !chartVisible) { + return
; + } + const LensSaveModalComponent = services.lens.SaveModalComponent; const canSaveVisualization = chartVisible && currentSuggestion && services.capabilities.dashboard?.showWriteControls; + const canEditVisualizationOnTheFly = currentSuggestion && chartVisible; - const renderEditButton = useMemo( - () => ( - setIsFlyoutVisible(true)} - data-test-subj="unifiedHistogramEditFlyoutVisualization" - aria-label={i18n.translate('unifiedHistogram.editVisualizationButton', { - defaultMessage: 'Edit visualization', - })} - disabled={isFlyoutVisible} - /> - ), - [isFlyoutVisible] - ); + const actions: IconButtonGroupProps['buttons'] = []; - const canEditVisualizationOnTheFly = currentSuggestion && chartVisible; + if (canEditVisualizationOnTheFly) { + actions.push({ + label: i18n.translate('unifiedHistogram.editVisualizationButton', { + defaultMessage: 'Edit visualization', + }), + iconType: 'pencil', + isDisabled: isFlyoutVisible, + 'data-test-subj': 'unifiedHistogramEditFlyoutVisualization', + onClick: () => setIsFlyoutVisible(true), + }); + } else if (onEditVisualization) { + actions.push({ + label: i18n.translate('unifiedHistogram.editVisualizationButton', { + defaultMessage: 'Edit visualization', + }), + iconType: 'lensApp', + 'data-test-subj': 'unifiedHistogramEditVisualization', + onClick: onEditVisualization, + }); + } + if (canSaveVisualization) { + actions.push({ + label: i18n.translate('unifiedHistogram.saveVisualizationButton', { + defaultMessage: 'Save visualization', + }), + iconType: 'save', + 'data-test-subj': 'unifiedHistogramSaveVisualization', + onClick: () => setIsSaveModalVisible(true), + }); + } return ( - + - - {hits && } - - {chart && ( - - - {chartVisible && breakdown && ( - + + + + {renderCustomChartToggleActions ? ( + renderCustomChartToggleActions() + ) : ( + + )} + + {chartVisible && !isPlainRecord && !!onTimeIntervalChange && ( + + + + )} + +
+ {chartVisible && breakdown && ( - - )} - {chartVisible && - currentSuggestion && - allSuggestions && - allSuggestions?.length > 1 && ( - + )} + {chartVisible && + currentSuggestion && + allSuggestions && + allSuggestions?.length > 1 && ( - - )} - {canSaveVisualization && ( - <> - - - setIsSaveModalVisible(true)} - data-test-subj="unifiedHistogramSaveVisualization" - aria-label={i18n.translate('unifiedHistogram.saveVisualizationButton', { - defaultMessage: 'Save visualization', - })} - /> - - - - )} - {canEditVisualizationOnTheFly && ( - - {!isFlyoutVisible ? ( - - {renderEditButton} - - ) : ( - renderEditButton )} - - )} - {onEditVisualization && ( - - - - - - )} - - - - - } - isOpen={showChartOptionsPopover} - closePopover={closeChartOptions} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - - - +
+
+
+
+ {chartVisible && actions.length > 0 && ( + + )}
@@ -430,6 +362,7 @@ export function Chart({ defaultMessage: 'Histogram of found documents', })} css={histogramCss} + data-test-subj="unifiedHistogramRendered" > {isChartLoading && ( { + rows.forEach((r) => { rowsCount += r.rows; }); return rowsCount; diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_chart_actions.test.ts b/src/plugins/unified_histogram/public/chart/hooks/use_chart_actions.test.ts index 120dc0b3d088404..7696f0f9782b720 100644 --- a/src/plugins/unified_histogram/public/chart/hooks/use_chart_actions.test.ts +++ b/src/plugins/unified_histogram/public/chart/hooks/use_chart_actions.test.ts @@ -27,31 +27,6 @@ describe('useChartActions', () => { }; }; - it('should toggle chart options', () => { - const { hook } = render(); - expect(hook.result.current.showChartOptionsPopover).toBe(false); - act(() => { - hook.result.current.toggleChartOptions(); - }); - expect(hook.result.current.showChartOptionsPopover).toBe(true); - act(() => { - hook.result.current.toggleChartOptions(); - }); - expect(hook.result.current.showChartOptionsPopover).toBe(false); - }); - - it('should close chart options', () => { - const { hook } = render(); - act(() => { - hook.result.current.toggleChartOptions(); - }); - expect(hook.result.current.showChartOptionsPopover).toBe(true); - act(() => { - hook.result.current.closeChartOptions(); - }); - expect(hook.result.current.showChartOptionsPopover).toBe(false); - }); - it('should toggle hide chart', () => { const { chart, onChartHiddenChange, hook } = render(); act(() => { diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_chart_actions.ts b/src/plugins/unified_histogram/public/chart/hooks/use_chart_actions.ts index 168db2ca0c4d9f5..3c4bd2434e3dd8f 100644 --- a/src/plugins/unified_histogram/public/chart/hooks/use_chart_actions.ts +++ b/src/plugins/unified_histogram/public/chart/hooks/use_chart_actions.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import type { UnifiedHistogramChartContext } from '../../types'; export const useChartActions = ({ @@ -16,16 +16,6 @@ export const useChartActions = ({ chart: UnifiedHistogramChartContext | undefined; onChartHiddenChange?: (chartHidden: boolean) => void; }) => { - const [showChartOptionsPopover, setShowChartOptionsPopover] = useState(false); - - const toggleChartOptions = useCallback(() => { - setShowChartOptionsPopover(!showChartOptionsPopover); - }, [showChartOptionsPopover]); - - const closeChartOptions = useCallback(() => { - setShowChartOptionsPopover(false); - }, [setShowChartOptionsPopover]); - const chartRef = useRef<{ element: HTMLElement | null; moveFocus: boolean }>({ element: null, moveFocus: false, @@ -44,10 +34,7 @@ export const useChartActions = ({ }, [chart?.hidden, onChartHiddenChange]); return { - showChartOptionsPopover, chartRef, - toggleChartOptions, - closeChartOptions, toggleHideChart, }; }; diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_chart_panels.test.ts b/src/plugins/unified_histogram/public/chart/hooks/use_chart_panels.test.ts deleted file mode 100644 index e5ee2b2c55cd97f..000000000000000 --- a/src/plugins/unified_histogram/public/chart/hooks/use_chart_panels.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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 { useChartPanels } from './use_chart_panels'; -import { EuiContextMenuPanelDescriptor } from '@elastic/eui'; - -describe('test useChartPanels', () => { - test('useChartsPanel when hideChart is true', async () => { - const { result } = renderHook(() => { - return useChartPanels({ - toggleHideChart: jest.fn(), - onTimeIntervalChange: jest.fn(), - closePopover: jest.fn(), - onResetChartHeight: jest.fn(), - chart: { - hidden: true, - timeInterval: 'auto', - }, - }); - }); - const panels: EuiContextMenuPanelDescriptor[] = result.current; - const panel0: EuiContextMenuPanelDescriptor = result.current[0]; - expect(panels.length).toBe(1); - expect(panel0!.items).toHaveLength(1); - expect(panel0!.items![0].icon).toBe('eye'); - }); - test('useChartsPanel when hideChart is false', async () => { - const { result } = renderHook(() => { - return useChartPanels({ - toggleHideChart: jest.fn(), - onTimeIntervalChange: jest.fn(), - closePopover: jest.fn(), - onResetChartHeight: jest.fn(), - chart: { - hidden: false, - timeInterval: 'auto', - }, - }); - }); - const panels: EuiContextMenuPanelDescriptor[] = result.current; - const panel0: EuiContextMenuPanelDescriptor = result.current[0]; - expect(panels.length).toBe(2); - expect(panel0!.items).toHaveLength(3); - expect(panel0!.items![0].icon).toBe('eyeClosed'); - expect(panel0!.items![1].icon).toBe('refresh'); - }); - test('should not show reset chart height when onResetChartHeight is undefined', async () => { - const { result } = renderHook(() => { - return useChartPanels({ - toggleHideChart: jest.fn(), - onTimeIntervalChange: jest.fn(), - closePopover: jest.fn(), - chart: { - hidden: false, - timeInterval: 'auto', - }, - }); - }); - const panel0: EuiContextMenuPanelDescriptor = result.current[0]; - expect(panel0!.items).toHaveLength(2); - expect(panel0!.items![0].icon).toBe('eyeClosed'); - }); - test('onResetChartHeight is called when the reset chart height button is clicked', async () => { - const onResetChartHeight = jest.fn(); - const { result } = renderHook(() => { - return useChartPanels({ - toggleHideChart: jest.fn(), - onTimeIntervalChange: jest.fn(), - closePopover: jest.fn(), - onResetChartHeight, - chart: { - hidden: false, - timeInterval: 'auto', - }, - }); - }); - const panel0: EuiContextMenuPanelDescriptor = result.current[0]; - const resetChartHeightButton = panel0!.items![1]; - (resetChartHeightButton.onClick as Function)(); - expect(onResetChartHeight).toBeCalled(); - }); - test('useChartsPanel when isPlainRecord', async () => { - const { result } = renderHook(() => { - return useChartPanels({ - toggleHideChart: jest.fn(), - onTimeIntervalChange: jest.fn(), - closePopover: jest.fn(), - onResetChartHeight: jest.fn(), - isPlainRecord: true, - chart: { - hidden: true, - timeInterval: 'auto', - }, - }); - }); - const panels: EuiContextMenuPanelDescriptor[] = result.current; - const panel0: EuiContextMenuPanelDescriptor = result.current[0]; - expect(panels.length).toBe(1); - expect(panel0!.items).toHaveLength(1); - expect(panel0!.items![0].icon).toBe('eye'); - }); -}); diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_chart_panels.ts b/src/plugins/unified_histogram/public/chart/hooks/use_chart_panels.ts deleted file mode 100644 index bf1bf4d6b95cdd9..000000000000000 --- a/src/plugins/unified_histogram/public/chart/hooks/use_chart_panels.ts +++ /dev/null @@ -1,124 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; -import type { - EuiContextMenuPanelItemDescriptor, - EuiContextMenuPanelDescriptor, -} from '@elastic/eui'; -import { search } from '@kbn/data-plugin/public'; -import type { UnifiedHistogramChartContext } from '../../types'; - -export function useChartPanels({ - chart, - toggleHideChart, - onTimeIntervalChange, - closePopover, - onResetChartHeight, - isPlainRecord, -}: { - chart?: UnifiedHistogramChartContext; - toggleHideChart: () => void; - onTimeIntervalChange?: (timeInterval: string) => void; - closePopover: () => void; - onResetChartHeight?: () => void; - isPlainRecord?: boolean; -}) { - if (!chart) { - return []; - } - - const selectedOptionIdx = search.aggs.intervalOptions.findIndex( - (opt) => opt.val === chart.timeInterval - ); - const intervalDisplay = - selectedOptionIdx > -1 - ? search.aggs.intervalOptions[selectedOptionIdx].display - : search.aggs.intervalOptions[0].display; - - const mainPanelItems: EuiContextMenuPanelItemDescriptor[] = [ - { - name: !chart.hidden - ? i18n.translate('unifiedHistogram.hideChart', { - defaultMessage: 'Hide chart', - }) - : i18n.translate('unifiedHistogram.showChart', { - defaultMessage: 'Show chart', - }), - icon: !chart.hidden ? 'eyeClosed' : 'eye', - onClick: () => { - toggleHideChart(); - closePopover(); - }, - 'data-test-subj': 'unifiedHistogramChartToggle', - }, - ]; - if (!chart.hidden) { - if (onResetChartHeight) { - mainPanelItems.push({ - name: i18n.translate('unifiedHistogram.resetChartHeight', { - defaultMessage: 'Reset to default height', - }), - icon: 'refresh', - onClick: () => { - onResetChartHeight(); - closePopover(); - }, - 'data-test-subj': 'unifiedHistogramChartResetHeight', - }); - } - - if (!isPlainRecord) { - mainPanelItems.push({ - name: i18n.translate('unifiedHistogram.timeIntervalWithValue', { - defaultMessage: 'Time interval: {timeInterval}', - values: { - timeInterval: intervalDisplay, - }, - }), - panel: 1, - 'data-test-subj': 'unifiedHistogramTimeIntervalPanel', - }); - } - } - - const panels: EuiContextMenuPanelDescriptor[] = [ - { - id: 0, - title: i18n.translate('unifiedHistogram.chartOptions', { - defaultMessage: 'Chart options', - }), - items: mainPanelItems, - }, - ]; - if (!chart.hidden && !isPlainRecord) { - panels.push({ - id: 1, - initialFocusedItemIndex: selectedOptionIdx > -1 ? selectedOptionIdx : 0, - title: i18n.translate('unifiedHistogram.timeIntervals', { - defaultMessage: 'Time intervals', - }), - items: search.aggs.intervalOptions - .filter(({ val }) => val !== 'custom') - .map(({ display, val }) => { - return { - name: display, - label: display, - icon: val === chart.timeInterval ? 'check' : 'empty', - onClick: () => { - onTimeIntervalChange?.(val); - closePopover(); - }, - 'data-test-subj': `unifiedHistogramTimeInterval-${display}`, - className: val === chart.timeInterval ? 'unifiedHistogramIntervalSelected' : '', - }; - }), - }); - } - return panels; -} diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_chart_styles.tsx b/src/plugins/unified_histogram/public/chart/hooks/use_chart_styles.tsx index 13b527be702c1bf..5a5bf41ca395d9c 100644 --- a/src/plugins/unified_histogram/public/chart/hooks/use_chart_styles.tsx +++ b/src/plugins/unified_histogram/public/chart/hooks/use_chart_styles.tsx @@ -6,36 +6,18 @@ * Side Public License, v 1. */ -import { useEuiBreakpoint, useEuiTheme } from '@elastic/eui'; +import { useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; export const useChartStyles = (chartVisible: boolean) => { const { euiTheme } = useEuiTheme(); - const resultCountCss = css` + + const chartToolbarCss = css` padding: ${euiTheme.size.s} ${euiTheme.size.s} ${chartVisible ? 0 : euiTheme.size.s} ${euiTheme.size.s}; min-height: ${euiTheme.base * 2.5}px; `; - const resultCountInnerCss = css` - ${useEuiBreakpoint(['xs', 's'])} { - align-items: center; - } - `; - const resultCountTitleCss = css` - flex-basis: auto; - ${useEuiBreakpoint(['xs', 's'])} { - margin-bottom: 0 !important; - } - `; - const resultCountToggleCss = css` - flex-basis: auto; - min-width: 0; - - ${useEuiBreakpoint(['xs', 's'])} { - align-items: flex-end; - } - `; const histogramCss = css` flex-grow: 1; display: flex; @@ -48,34 +30,9 @@ export const useChartStyles = (chartVisible: boolean) => { stroke-width: 1; } `; - const breakdownFieldSelectorGroupCss = css` - width: 100%; - `; - const breakdownFieldSelectorItemCss = css` - min-width: 0; - align-items: flex-end; - padding-left: ${euiTheme.size.s}; - `; - const suggestionsSelectorItemCss = css` - min-width: 0; - align-items: flex-start; - padding-left: ${euiTheme.size.s}; - `; - const chartToolButtonCss = css` - display: flex; - justify-content: center; - padding-left: ${euiTheme.size.s}; - `; return { - resultCountCss, - resultCountInnerCss, - resultCountTitleCss, - resultCountToggleCss, + chartToolbarCss, histogramCss, - breakdownFieldSelectorGroupCss, - breakdownFieldSelectorItemCss, - suggestionsSelectorItemCss, - chartToolButtonCss, }; }; diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.test.ts b/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.test.ts index 3135f3c86f4653b..b6250f8fa82b7eb 100644 --- a/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.test.ts +++ b/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.test.ts @@ -85,7 +85,7 @@ describe('useTotalHits', () => { const query = { query: 'test query', language: 'kuery' }; const filters: Filter[] = [{ meta: { index: 'test' }, query: { match_all: {} } }]; const adapter = new RequestAdapter(); - renderHook(() => + const { rerender } = renderHook(() => useTotalHits({ ...getDeps(), services: { data } as any, @@ -99,6 +99,8 @@ describe('useTotalHits', () => { onTotalHitsChange, }) ); + refetch$.next({ type: 'refetch' }); + rerender(); expect(onTotalHitsChange).toBeCalledTimes(1); expect(onTotalHitsChange).toBeCalledWith(UnifiedHistogramFetchStatus.loading, undefined); expect(setFieldSpy).toHaveBeenCalledWith('index', dataViewWithTimefieldMock); @@ -125,7 +127,9 @@ describe('useTotalHits', () => { onTotalHitsChange, query: { esql: 'from test' }, }; - renderHook(() => useTotalHits(deps)); + const { rerender } = renderHook(() => useTotalHits(deps)); + refetch$.next({ type: 'refetch' }); + rerender(); expect(onTotalHitsChange).toBeCalledTimes(1); await waitFor(() => { expect(deps.services.expressions.run).toBeCalledTimes(1); @@ -153,22 +157,16 @@ describe('useTotalHits', () => { expect(fetchSpy).not.toHaveBeenCalled(); }); - it('should not fetch a second time if refetch$ is not triggered', async () => { + it('should not fetch if refetch$ is not triggered', async () => { const onTotalHitsChange = jest.fn(); const fetchSpy = jest.spyOn(searchSourceInstanceMock, 'fetch$').mockClear(); const setFieldSpy = jest.spyOn(searchSourceInstanceMock, 'setField').mockClear(); const options = { ...getDeps(), onTotalHitsChange }; const { rerender } = renderHook(() => useTotalHits(options)); - expect(onTotalHitsChange).toBeCalledTimes(1); - expect(setFieldSpy).toHaveBeenCalled(); - expect(fetchSpy).toHaveBeenCalled(); - await waitFor(() => { - expect(onTotalHitsChange).toBeCalledTimes(2); - }); rerender(); - expect(onTotalHitsChange).toBeCalledTimes(2); - expect(setFieldSpy).toHaveBeenCalledTimes(5); - expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(onTotalHitsChange).toBeCalledTimes(0); + expect(setFieldSpy).toHaveBeenCalledTimes(0); + expect(fetchSpy).toHaveBeenCalledTimes(0); }); it('should fetch a second time if refetch$ is triggered', async () => { @@ -178,6 +176,8 @@ describe('useTotalHits', () => { const setFieldSpy = jest.spyOn(searchSourceInstanceMock, 'setField').mockClear(); const options = { ...getDeps(), onTotalHitsChange }; const { rerender } = renderHook(() => useTotalHits(options)); + refetch$.next({ type: 'refetch' }); + rerender(); expect(onTotalHitsChange).toBeCalledTimes(1); expect(setFieldSpy).toHaveBeenCalled(); expect(fetchSpy).toHaveBeenCalled(); @@ -202,7 +202,9 @@ describe('useTotalHits', () => { .spyOn(searchSourceInstanceMock, 'fetch$') .mockClear() .mockReturnValue(throwError(() => error)); - renderHook(() => useTotalHits({ ...getDeps(), onTotalHitsChange })); + const { rerender } = renderHook(() => useTotalHits({ ...getDeps(), onTotalHitsChange })); + refetch$.next({ type: 'refetch' }); + rerender(); await waitFor(() => { expect(onTotalHitsChange).toBeCalledTimes(2); expect(onTotalHitsChange).toBeCalledWith(UnifiedHistogramFetchStatus.error, error); @@ -220,7 +222,7 @@ describe('useTotalHits', () => { .mockClear() .mockReturnValue(timeRange as any); const filters: Filter[] = [{ meta: { index: 'test' }, query: { match_all: {} } }]; - renderHook(() => + const { rerender } = renderHook(() => useTotalHits({ ...getDeps(), dataView: { @@ -230,6 +232,8 @@ describe('useTotalHits', () => { filters, }) ); + refetch$.next({ type: 'refetch' }); + rerender(); expect(setOverwriteDataViewTypeSpy).toHaveBeenCalledWith(undefined); expect(setFieldSpy).toHaveBeenCalledWith('filter', filters); }); diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.ts b/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.ts index 5bb927747e66932..c16bb2335be240c 100644 --- a/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.ts +++ b/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.ts @@ -13,7 +13,6 @@ import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; import { Datatable, isExpressionValueError } from '@kbn/expressions-plugin/common'; import { i18n } from '@kbn/i18n'; import { MutableRefObject, useEffect, useRef } from 'react'; -import useEffectOnce from 'react-use/lib/useEffectOnce'; import { catchError, filter, lastValueFrom, map, Observable, of, pluck } from 'rxjs'; import { UnifiedHistogramFetchStatus, @@ -66,8 +65,6 @@ export const useTotalHits = ({ }); }); - useEffectOnce(fetch); - useEffect(() => { const subscription = refetch$.subscribe(fetch); return () => subscription.unsubscribe(); @@ -102,13 +99,11 @@ const fetchTotalHits = async ({ abortController.current?.abort(); abortController.current = undefined; - // Either the chart is visible, in which case Lens will make the request, - // or there is no hits context, which means the total hits should be hidden - if (chartVisible || !hits) { + if (chartVisible) { return; } - onTotalHitsChange?.(UnifiedHistogramFetchStatus.loading, hits.total); + onTotalHitsChange?.(UnifiedHistogramFetchStatus.loading, hits?.total); const newAbortController = new AbortController(); diff --git a/src/plugins/unified_histogram/public/chart/index.ts b/src/plugins/unified_histogram/public/chart/index.ts index 6a6d2d65f6f9201..4a8b758f7d86e0f 100644 --- a/src/plugins/unified_histogram/public/chart/index.ts +++ b/src/plugins/unified_histogram/public/chart/index.ts @@ -7,3 +7,4 @@ */ export { Chart } from './chart'; +export { checkChartAvailability } from './check_chart_availability'; diff --git a/src/plugins/unified_histogram/public/chart/suggestion_selector.tsx b/src/plugins/unified_histogram/public/chart/suggestion_selector.tsx index 0196387633396b2..cad20279bfdf015 100644 --- a/src/plugins/unified_histogram/public/chart/suggestion_selector.tsx +++ b/src/plugins/unified_histogram/public/chart/suggestion_selector.tsx @@ -75,6 +75,8 @@ export const SuggestionSelector = ({ position="top" content={suggestionsPopoverDisabled ? undefined : activeSuggestion?.title} anchorProps={{ css: suggestionComboCss }} + display="block" + delay="long" > { + it('should render correctly', () => { + const onTimeIntervalChange = jest.fn(); + + render( + + ); + + const button = screen.getByTestId('unifiedHistogramTimeIntervalSelectorButton'); + expect(button.getAttribute('data-selected-value')).toBe('auto'); + + act(() => { + button.click(); + }); + + const options = screen.getAllByRole('option'); + expect( + options.map((option) => ({ + label: option.getAttribute('title'), + value: option.getAttribute('value'), + checked: option.getAttribute('aria-checked'), + })) + ).toMatchInlineSnapshot(` + Array [ + Object { + "checked": "true", + "label": "Auto", + "value": "auto", + }, + Object { + "checked": "false", + "label": "Millisecond", + "value": "ms", + }, + Object { + "checked": "false", + "label": "Second", + "value": "s", + }, + Object { + "checked": "false", + "label": "Minute", + "value": "m", + }, + Object { + "checked": "false", + "label": "Hour", + "value": "h", + }, + Object { + "checked": "false", + "label": "Day", + "value": "d", + }, + Object { + "checked": "false", + "label": "Week", + "value": "w", + }, + Object { + "checked": "false", + "label": "Month", + "value": "M", + }, + Object { + "checked": "false", + "label": "Year", + "value": "y", + }, + ] + `); + }); + + it('should mark the selected option as checked', () => { + const onTimeIntervalChange = jest.fn(); + + render( + + ); + + const button = screen.getByTestId('unifiedHistogramTimeIntervalSelectorButton'); + expect(button.getAttribute('data-selected-value')).toBe('y'); + + act(() => { + button.click(); + }); + + const options = screen.getAllByRole('option'); + expect( + options.map((option) => ({ + label: option.getAttribute('title'), + value: option.getAttribute('value'), + checked: option.getAttribute('aria-checked'), + })) + ).toMatchInlineSnapshot(` + Array [ + Object { + "checked": "false", + "label": "Auto", + "value": "auto", + }, + Object { + "checked": "false", + "label": "Millisecond", + "value": "ms", + }, + Object { + "checked": "false", + "label": "Second", + "value": "s", + }, + Object { + "checked": "false", + "label": "Minute", + "value": "m", + }, + Object { + "checked": "false", + "label": "Hour", + "value": "h", + }, + Object { + "checked": "false", + "label": "Day", + "value": "d", + }, + Object { + "checked": "false", + "label": "Week", + "value": "w", + }, + Object { + "checked": "false", + "label": "Month", + "value": "M", + }, + Object { + "checked": "true", + "label": "Year", + "value": "y", + }, + ] + `); + }); + + it('should call onTimeIntervalChange with the selected option when the user selects an interval', () => { + const onTimeIntervalChange = jest.fn(); + + render( + + ); + + act(() => { + screen.getByTestId('unifiedHistogramTimeIntervalSelectorButton').click(); + }); + + act(() => { + screen.getByTitle('Week').click(); + }); + + expect(onTimeIntervalChange).toHaveBeenCalledWith('w'); + }); +}); diff --git a/src/plugins/unified_histogram/public/chart/time_interval_selector.tsx b/src/plugins/unified_histogram/public/chart/time_interval_selector.tsx new file mode 100644 index 000000000000000..86c17fdc79172f6 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/time_interval_selector.tsx @@ -0,0 +1,81 @@ +/* + * 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, { useCallback } from 'react'; +import { EuiSelectableOption } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { search } from '@kbn/data-plugin/public'; +import type { UnifiedHistogramChartContext } from '../types'; +import { ToolbarSelector, ToolbarSelectorProps, SelectableEntry } from './toolbar_selector'; + +export interface TimeIntervalSelectorProps { + chart: UnifiedHistogramChartContext; + onTimeIntervalChange: (timeInterval: string) => void; +} + +export const TimeIntervalSelector: React.FC = ({ + chart, + onTimeIntervalChange, +}) => { + const onChange: ToolbarSelectorProps['onChange'] = useCallback( + (chosenOption) => { + const selectedOption = chosenOption?.value; + if (selectedOption) { + onTimeIntervalChange(selectedOption); + } + }, + [onTimeIntervalChange] + ); + + const selectedOptionIdx = search.aggs.intervalOptions.findIndex( + (opt) => opt.val === chart.timeInterval + ); + const intervalDisplay = + selectedOptionIdx > -1 + ? search.aggs.intervalOptions[selectedOptionIdx].display + : search.aggs.intervalOptions[0].display; + + const options: SelectableEntry[] = search.aggs.intervalOptions + .filter(({ val }) => val !== 'custom') + .map(({ display, val }) => { + return { + key: val, + value: val, + label: display, + checked: val === chart.timeInterval ? ('on' as EuiSelectableOption['checked']) : undefined, + }; + }); + + return ( + + ); +}; diff --git a/src/plugins/unified_histogram/public/chart/toolbar_selector.tsx b/src/plugins/unified_histogram/public/chart/toolbar_selector.tsx new file mode 100644 index 000000000000000..1a83a736bb534ed --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/toolbar_selector.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 React, { useCallback, ReactElement, useState, useMemo } from 'react'; +import { + EuiPopover, + EuiPopoverTitle, + EuiSelectable, + EuiSelectableProps, + EuiSelectableOption, + useEuiTheme, + EuiPanel, + EuiToolTip, +} from '@elastic/eui'; +import { ToolbarButton } from '@kbn/shared-ux-button-toolbar'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; +import { i18n } from '@kbn/i18n'; + +export const EMPTY_OPTION = '__EMPTY_SELECTOR_OPTION__'; + +export type SelectableEntry = EuiSelectableOption<{ value: string }>; + +export interface ToolbarSelectorProps { + 'data-test-subj': string; + 'data-selected-value'?: string; // currently selected value + buttonLabel: ReactElement | string; + popoverTitle: string; + options: SelectableEntry[]; + searchable: boolean; + onChange?: (chosenOption: SelectableEntry | undefined) => void; +} + +export const ToolbarSelector: React.FC = ({ + 'data-test-subj': dataTestSubj, + 'data-selected-value': dataSelectedValue, + buttonLabel, + popoverTitle, + options, + searchable, + onChange, +}) => { + const { euiTheme } = useEuiTheme(); + const [isOpen, setIsOpen] = useState(false); + const [searchTerm, setSearchTerm] = useState(); + const [labelPopoverDisabled, setLabelPopoverDisabled] = useState(false); + + const disableLabelPopover = useCallback(() => setLabelPopoverDisabled(true), []); + + const enableLabelPopover = useCallback( + () => setTimeout(() => setLabelPopoverDisabled(false)), + [] + ); + + const onSelectionChange = useCallback( + (newOptions) => { + const chosenOption = newOptions.find(({ checked }: SelectableEntry) => checked === 'on'); + + onChange?.( + chosenOption?.value && chosenOption?.value !== EMPTY_OPTION ? chosenOption : undefined + ); + setIsOpen(false); + disableLabelPopover(); + }, + [disableLabelPopover, onChange] + ); + + const searchProps: EuiSelectableProps['searchProps'] = useMemo( + () => + searchable + ? { + id: `${dataTestSubj}SelectableInput`, + 'data-test-subj': `${dataTestSubj}SelectorSearch`, + compressed: true, + placeholder: i18n.translate( + 'unifiedHistogram.toolbarSelectorPopover.searchPlaceholder', + { + defaultMessage: 'Search', + } + ), + onChange: (value) => setSearchTerm(value), + } + : undefined, + [dataTestSubj, searchable, setSearchTerm] + ); + + const panelMinWidth = calculateWidthFromEntries(options, ['label']) + 2 * euiTheme.base; // plus extra width for the right Enter button + + return ( + + setIsOpen(!isOpen)} + onBlur={enableLabelPopover} + /> + + } + isOpen={isOpen} + closePopover={() => setIsOpen(false)} + anchorPosition="downLeft" + > + {popoverTitle} + + {searchTerm}, + }} + /> +

+ ), + } + : {})} + > + {(list, search) => ( + <> + {search && ( + + {search} + + )} + {list} + + )} +
+
+ ); +}; diff --git a/src/plugins/unified_histogram/public/container/container.tsx b/src/plugins/unified_histogram/public/container/container.tsx index c65f1e2b43c0457..fb152a1921e2321 100644 --- a/src/plugins/unified_histogram/public/container/container.tsx +++ b/src/plugins/unified_histogram/public/container/container.tsx @@ -54,7 +54,7 @@ export type UnifiedHistogramContainerProps = { | 'relativeTimeRange' | 'columns' | 'container' - | 'appendHitsCounter' + | 'renderCustomChartToggleActions' | 'children' | 'onBrushEnd' | 'onFilter' diff --git a/src/plugins/unified_histogram/public/container/services/state_service.test.ts b/src/plugins/unified_histogram/public/container/services/state_service.test.ts index 73a493e167c1949..40304a967243ab1 100644 --- a/src/plugins/unified_histogram/public/container/services/state_service.test.ts +++ b/src/plugins/unified_histogram/public/container/services/state_service.test.ts @@ -211,28 +211,4 @@ describe('UnifiedHistogramStateService', () => { expect(setTopPanelHeight as jest.Mock).not.toHaveBeenCalled(); expect(setBreakdownField as jest.Mock).not.toHaveBeenCalled(); }); - - it('should not update total hits to loading when the current status is partial', () => { - const stateService = createStateService({ - services: unifiedHistogramServicesMock, - initialState: { - ...initialState, - totalHitsStatus: UnifiedHistogramFetchStatus.partial, - }, - }); - let state: UnifiedHistogramState | undefined; - stateService.state$.subscribe((s) => (state = s)); - expect(state).toEqual({ - ...initialState, - totalHitsStatus: UnifiedHistogramFetchStatus.partial, - }); - stateService.setTotalHits({ - totalHitsStatus: UnifiedHistogramFetchStatus.loading, - totalHitsResult: 100, - }); - expect(state).toEqual({ - ...initialState, - totalHitsStatus: UnifiedHistogramFetchStatus.partial, - }); - }); }); diff --git a/src/plugins/unified_histogram/public/container/services/state_service.ts b/src/plugins/unified_histogram/public/container/services/state_service.ts index f96a4b5b7b033e7..1a79389e2bc6f47 100644 --- a/src/plugins/unified_histogram/public/container/services/state_service.ts +++ b/src/plugins/unified_histogram/public/container/services/state_service.ts @@ -217,15 +217,6 @@ export const createStateService = ( totalHitsStatus: UnifiedHistogramFetchStatus; totalHitsResult: number | Error | undefined; }) => { - // If we have a partial result already, we don't - // want to update the total hits back to loading - if ( - state$.getValue().totalHitsStatus === UnifiedHistogramFetchStatus.partial && - totalHits.totalHitsStatus === UnifiedHistogramFetchStatus.loading - ) { - return; - } - updateState(totalHits); }, }; diff --git a/src/plugins/unified_histogram/public/hits_counter/hits_counter.test.tsx b/src/plugins/unified_histogram/public/hits_counter/hits_counter.test.tsx deleted file mode 100644 index 03b350448e9c299..000000000000000 --- a/src/plugins/unified_histogram/public/hits_counter/hits_counter.test.tsx +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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 { mountWithIntl } from '@kbn/test-jest-helpers'; -import type { ReactWrapper } from 'enzyme'; -import type { HitsCounterProps } from './hits_counter'; -import { HitsCounter } from './hits_counter'; -import { findTestSubject } from '@elastic/eui/lib/test'; -import { EuiLoadingSpinner } from '@elastic/eui'; -import { UnifiedHistogramFetchStatus } from '../types'; - -describe('hits counter', function () { - let props: HitsCounterProps; - let component: ReactWrapper; - - beforeAll(() => { - props = { - hits: { - status: UnifiedHistogramFetchStatus.complete, - total: 2, - }, - }; - }); - - it('expect to render the number of hits', function () { - component = mountWithIntl(); - const hits = findTestSubject(component, 'unifiedHistogramQueryHits'); - expect(hits.text()).toBe('2'); - }); - - it('expect to render 1,899 hits if 1899 hits given', function () { - component = mountWithIntl( - - ); - const hits = findTestSubject(component, 'unifiedHistogramQueryHits'); - expect(hits.text()).toBe('1,899'); - }); - - it('should render the element passed to the append prop', () => { - const appendHitsCounter =
appendHitsCounter
; - component = mountWithIntl(); - expect(findTestSubject(component, 'appendHitsCounter').length).toBe(1); - }); - - it('should render a EuiLoadingSpinner when status is partial', () => { - component = mountWithIntl( - - ); - expect(component.find(EuiLoadingSpinner).length).toBe(1); - }); - - it('should render unifiedHistogramQueryHitsPartial when status is partial', () => { - component = mountWithIntl( - - ); - expect(component.find('[data-test-subj="unifiedHistogramQueryHitsPartial"]').length).toBe(1); - }); - - it('should render unifiedHistogramQueryHits when status is complete', () => { - component = mountWithIntl(); - expect(component.find('[data-test-subj="unifiedHistogramQueryHits"]').length).toBe(1); - }); -}); diff --git a/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx b/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx deleted file mode 100644 index b6f1212bfeaedfb..000000000000000 --- a/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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 { ReactElement } from 'react'; -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiLoadingSpinner } from '@elastic/eui'; -import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react'; -import { i18n } from '@kbn/i18n'; -import { css } from '@emotion/react'; -import type { UnifiedHistogramHitsContext } from '../types'; - -export interface HitsCounterProps { - hits: UnifiedHistogramHitsContext; - append?: ReactElement; -} - -export function HitsCounter({ hits, append }: HitsCounterProps) { - if (!hits.total && hits.status === 'loading') { - return null; - } - - const formattedHits = ( - - - - ); - - const hitsCounterCss = css` - flex-grow: 0; - `; - const hitsCounterTextCss = css` - overflow: hidden; - `; - - return ( - - - - {hits.status === 'partial' && ( - - )} - {hits.status !== 'partial' && ( - - )} - - - {hits.status === 'partial' && ( - - - - )} - {append} - - ); -} diff --git a/src/plugins/unified_histogram/public/layout/layout.test.tsx b/src/plugins/unified_histogram/public/layout/layout.test.tsx index a12c8cf46430e56..a10df63e7c328c1 100644 --- a/src/plugins/unified_histogram/public/layout/layout.test.tsx +++ b/src/plugins/unified_histogram/public/layout/layout.test.tsx @@ -10,7 +10,6 @@ import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_ import { mountWithIntl } from '@kbn/test-jest-helpers'; import type { ReactWrapper } from 'enzyme'; import React from 'react'; -import { act } from 'react-dom/test-utils'; import { of } from 'rxjs'; import { Chart } from '../chart'; import { @@ -153,13 +152,6 @@ describe('Layout', () => { height: `${expectedHeight}px`, }); }); - - it('should pass undefined for onResetChartHeight to Chart when layout mode is ResizableLayoutMode.Static', async () => { - const component = await mountComponent({ topPanelHeight: 123 }); - expect(component.find(Chart).prop('onResetChartHeight')).toBeDefined(); - setBreakpoint(component, 's'); - expect(component.find(Chart).prop('onResetChartHeight')).toBeUndefined(); - }); }); describe('topPanelHeight', () => { @@ -167,39 +159,5 @@ describe('Layout', () => { const component = await mountComponent({ topPanelHeight: undefined }); expect(component.find(ResizableLayout).prop('fixedPanelSize')).toBeGreaterThan(0); }); - - it('should reset the fixedPanelSize to the default when onResetChartHeight is called on Chart', async () => { - const component: ReactWrapper = await mountComponent({ - onTopPanelHeightChange: jest.fn((topPanelHeight) => { - component.setProps({ topPanelHeight }); - }), - }); - const defaultTopPanelHeight = component.find(ResizableLayout).prop('fixedPanelSize'); - const newTopPanelHeight = 123; - expect(component.find(ResizableLayout).prop('fixedPanelSize')).not.toBe(newTopPanelHeight); - act(() => { - component.find(ResizableLayout).prop('onFixedPanelSizeChange')!(newTopPanelHeight); - }); - expect(component.find(ResizableLayout).prop('fixedPanelSize')).toBe(newTopPanelHeight); - act(() => { - component.find(Chart).prop('onResetChartHeight')!(); - }); - expect(component.find(ResizableLayout).prop('fixedPanelSize')).toBe(defaultTopPanelHeight); - }); - - it('should pass undefined for onResetChartHeight to Chart when the chart is the default height', async () => { - const component = await mountComponent({ - topPanelHeight: 123, - onTopPanelHeightChange: jest.fn((topPanelHeight) => { - component.setProps({ topPanelHeight }); - }), - }); - expect(component.find(Chart).prop('onResetChartHeight')).toBeDefined(); - act(() => { - component.find(Chart).prop('onResetChartHeight')!(); - }); - component.update(); - expect(component.find(Chart).prop('onResetChartHeight')).toBeUndefined(); - }); }); }); diff --git a/src/plugins/unified_histogram/public/layout/layout.tsx b/src/plugins/unified_histogram/public/layout/layout.tsx index 17eaf65fcde5f08..1a175690eb4471b 100644 --- a/src/plugins/unified_histogram/public/layout/layout.tsx +++ b/src/plugins/unified_histogram/public/layout/layout.tsx @@ -7,7 +7,7 @@ */ import { EuiSpacer, useEuiTheme, useIsWithinBreakpoints } from '@elastic/eui'; -import React, { PropsWithChildren, ReactElement, useMemo, useState } from 'react'; +import React, { PropsWithChildren, ReactElement, useState } from 'react'; import { Observable } from 'rxjs'; import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal'; import { css } from '@emotion/css'; @@ -26,7 +26,7 @@ import { ResizableLayoutMode, ResizableLayoutDirection, } from '@kbn/resizable-layout'; -import { Chart } from '../chart'; +import { Chart, checkChartAvailability } from '../chart'; import type { UnifiedHistogramChartContext, UnifiedHistogramServices, @@ -39,6 +39,10 @@ import type { } from '../types'; import { useLensSuggestions } from './hooks/use_lens_suggestions'; +const ChartMemoized = React.memo(Chart); + +const chartSpacer = ; + export interface UnifiedHistogramLayoutProps extends PropsWithChildren { /** * Optional class name to add to the layout container @@ -107,9 +111,9 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren */ topPanelHeight?: number; /** - * Append a custom element to the right of the hits count + * This element would replace the default chart toggle buttons */ - appendHitsCounter?: ReactElement; + renderCustomChartToggleActions?: () => ReactElement | undefined; /** * Disable automatic refetching based on props changes, and instead wait for a `refetch` message */ @@ -197,7 +201,7 @@ export const UnifiedHistogramLayout = ({ breakdown, container, topPanelHeight, - appendHitsCounter, + renderCustomChartToggleActions, disableAutoFetching, disableTriggers, disabledActions, @@ -234,6 +238,8 @@ export const UnifiedHistogramLayout = ({ }); const chart = suggestionUnsupported ? undefined : originalChart; + const isChartAvailable = checkChartAvailability({ chart, dataView, isPlainRecord }); + const [topPanelNode] = useState(() => createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } }) ); @@ -263,17 +269,11 @@ export const UnifiedHistogramLayout = ({ const currentTopPanelHeight = topPanelHeight ?? defaultTopPanelHeight; - const onResetChartHeight = useMemo(() => { - return currentTopPanelHeight !== defaultTopPanelHeight && - panelsMode === ResizableLayoutMode.Resizable - ? () => onTopPanelHeightChange?.(undefined) - : undefined; - }, [currentTopPanelHeight, defaultTopPanelHeight, onTopPanelHeightChange, panelsMode]); - return ( <> - } + renderCustomChartToggleActions={renderCustomChartToggleActions} + appendHistogram={chartSpacer} disableAutoFetching={disableAutoFetching} disableTriggers={disableTriggers} disabledActions={disabledActions} input$={input$} - onResetChartHeight={onResetChartHeight} onChartHiddenChange={onChartHiddenChange} onTimeIntervalChange={onTimeIntervalChange} onBreakdownFieldChange={onBreakdownFieldChange} @@ -311,7 +310,11 @@ export const UnifiedHistogramLayout = ({ withDefaultActions={withDefaultActions} /> - {children} + + {React.isValidElement(children) + ? React.cloneElement(children, { isChartAvailable }) + : children} + { - await testSubjects.click('unifiedHistogramChartOptionsToggle'); - await a11y.testAppSnapshot(); - }); - it('a11y test for data grid with hidden chart', async () => { - await testSubjects.click('unifiedHistogramChartToggle'); + await PageObjects.discover.closeHistogramPanel(); await a11y.testAppSnapshot(); - await testSubjects.click('unifiedHistogramChartOptionsToggle'); - await testSubjects.click('unifiedHistogramChartToggle'); + await PageObjects.discover.openHistogramPanel(); }); it('a11y test for time interval panel', async () => { - await testSubjects.click('unifiedHistogramChartOptionsToggle'); - await testSubjects.click('unifiedHistogramTimeIntervalPanel'); + await testSubjects.click('unifiedHistogramTimeIntervalSelectorButton'); await a11y.testAppSnapshot(); - await testSubjects.click('contextMenuPanelTitleButton'); - await testSubjects.click('unifiedHistogramChartOptionsToggle'); }); it('a11y test for data grid sort panel', async () => { @@ -205,7 +196,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('a11y test for data grid with collapsed side bar', async () => { await PageObjects.discover.closeSidebar(); await a11y.testAppSnapshot(); - await PageObjects.discover.toggleSidebarCollapse(); + await PageObjects.discover.openSidebar(); }); it('a11y test for adding a field from side bar', async () => { diff --git a/test/functional/apps/discover/group1/_discover.ts b/test/functional/apps/discover/group1/_discover.ts index a041939f2b809ed..0210c7d8cc7f255 100644 --- a/test/functional/apps/discover/group1/_discover.ts +++ b/test/functional/apps/discover/group1/_discover.ts @@ -114,10 +114,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show correct initial chart interval of Auto', async function () { await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.discover.waitUntilSearchingHasFinished(); - await testSubjects.click('unifiedHistogramQueryHits'); // to cancel out tooltips + await testSubjects.click('discoverQueryHits'); // to cancel out tooltips const actualInterval = await PageObjects.discover.getChartInterval(); - const expectedInterval = 'Auto'; + const expectedInterval = 'auto'; expect(actualInterval).to.be(expectedInterval); }); diff --git a/test/functional/apps/discover/group1/_discover_histogram.ts b/test/functional/apps/discover/group1/_discover_histogram.ts index 64e9b0e47dc9003..ad5563e78f918f7 100644 --- a/test/functional/apps/discover/group1/_discover_histogram.ts +++ b/test/functional/apps/discover/group1/_discover_histogram.ts @@ -156,6 +156,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const chartCanvasExist = await elasticChart.canvasExists(); expect(chartCanvasExist).to.be(true); const chartIntervalIconTip = await PageObjects.discover.getChartIntervalWarningIcon(); + expect(chartIntervalIconTip).to.be(false); + }); + it('should visualize monthly data with different years scaled to seconds', async () => { + const from = 'Jan 1, 2010 @ 00:00:00.000'; + const to = 'Mar 21, 2019 @ 00:00:00.000'; + await prepareTest({ from, to }, 'Second'); + const chartCanvasExist = await elasticChart.canvasExists(); + expect(chartCanvasExist).to.be(true); + const chartIntervalIconTip = await PageObjects.discover.getChartIntervalWarningIcon(); expect(chartIntervalIconTip).to.be(true); }); it('should allow hide/show histogram, persisted in url state', async () => { @@ -164,8 +173,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await prepareTest({ from, to }); let canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(true); - await testSubjects.click('unifiedHistogramChartOptionsToggle'); - await testSubjects.click('unifiedHistogramChartToggle'); + await PageObjects.discover.toggleChartVisibility(); await retry.try(async () => { canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(false); @@ -174,8 +182,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.refresh(); canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(false); - await testSubjects.click('unifiedHistogramChartOptionsToggle'); - await testSubjects.click('unifiedHistogramChartToggle'); + await PageObjects.discover.toggleChartVisibility(); await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async () => { canvasExists = await elasticChart.canvasExists(); @@ -189,8 +196,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await prepareTest({ from, to }); // close chart for saved search - await testSubjects.click('unifiedHistogramChartOptionsToggle'); - await testSubjects.click('unifiedHistogramChartToggle'); + await PageObjects.discover.toggleChartVisibility(); let canvasExists: boolean; await retry.try(async () => { canvasExists = await elasticChart.canvasExists(); @@ -212,8 +218,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(canvasExists).to.be(false); // open chart for saved search - await testSubjects.click('unifiedHistogramChartOptionsToggle'); - await testSubjects.click('unifiedHistogramChartToggle'); + await PageObjects.discover.toggleChartVisibility(); await retry.waitFor(`Discover histogram to be displayed`, async () => { canvasExists = await elasticChart.canvasExists(); return canvasExists; @@ -235,8 +240,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should show permitted hidden histogram state when returning back to discover', async () => { // close chart - await testSubjects.click('unifiedHistogramChartOptionsToggle'); - await testSubjects.click('unifiedHistogramChartToggle'); + await PageObjects.discover.toggleChartVisibility(); let canvasExists: boolean; await retry.try(async () => { canvasExists = await elasticChart.canvasExists(); @@ -248,8 +252,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); // open chart - await testSubjects.click('unifiedHistogramChartOptionsToggle'); - await testSubjects.click('unifiedHistogramChartToggle'); + await PageObjects.discover.toggleChartVisibility(); await retry.try(async () => { canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(true); @@ -266,8 +269,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(canvasExists).to.be(true); // close chart - await testSubjects.click('unifiedHistogramChartOptionsToggle'); - await testSubjects.click('unifiedHistogramChartToggle'); + await PageObjects.discover.toggleChartVisibility(); await retry.try(async () => { canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(false); @@ -278,8 +280,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('discover'); await PageObjects.discover.waitUntilSearchingHasFinished(); // Make sure the chart is visible - await testSubjects.click('unifiedHistogramChartOptionsToggle'); - await testSubjects.click('unifiedHistogramChartToggle'); + await PageObjects.discover.toggleChartVisibility(); await PageObjects.discover.waitUntilSearchingHasFinished(); // type an invalid search query, hit refresh await queryBar.setQuery('this is > not valid'); diff --git a/test/functional/apps/discover/group2/_data_grid.ts b/test/functional/apps/discover/group2/_data_grid.ts index 2facbc95c93ce1e..cdce56db6e856e2 100644 --- a/test/functional/apps/discover/group2/_data_grid.ts +++ b/test/functional/apps/discover/group2/_data_grid.ts @@ -71,17 +71,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should hide elements beneath the table when in full screen mode regardless of their z-index', async () => { await retry.try(async () => { - expect(await isVisible('unifiedHistogramQueryHits')).to.be(true); + expect(await isVisible('discover-dataView-switch-link')).to.be(true); expect(await isVisible('unifiedHistogramResizableButton')).to.be(true); }); await testSubjects.click('dataGridFullScreenButton'); await retry.try(async () => { - expect(await isVisible('unifiedHistogramQueryHits')).to.be(false); + expect(await isVisible('discover-dataView-switch-link')).to.be(false); expect(await isVisible('unifiedHistogramResizableButton')).to.be(false); }); await testSubjects.click('dataGridFullScreenButton'); await retry.try(async () => { - expect(await isVisible('unifiedHistogramQueryHits')).to.be(true); + expect(await isVisible('discover-dataView-switch-link')).to.be(true); expect(await isVisible('unifiedHistogramResizableButton')).to.be(true); }); }); diff --git a/test/functional/apps/discover/group3/_panels_toggle.ts b/test/functional/apps/discover/group3/_panels_toggle.ts new file mode 100644 index 000000000000000..d471969d3528fc1 --- /dev/null +++ b/test/functional/apps/discover/group3/_panels_toggle.ts @@ -0,0 +1,261 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + const monacoEditor = getService('monacoEditor'); + const PageObjects = getPageObjects([ + 'settings', + 'common', + 'discover', + 'header', + 'timePicker', + 'unifiedFieldList', + ]); + const security = getService('security'); + const defaultSettings = { + defaultIndex: 'logstash-*', + hideAnnouncements: true, + }; + + describe('discover panels toggle', function () { + before(async () => { + await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + }); + + after(async () => { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.uiSettings.replace({}); + await kibanaServer.savedObjects.cleanStandardList(); + }); + + async function checkSidebarAndHistogram({ + shouldSidebarBeOpen, + shouldHistogramBeOpen, + isChartAvailable, + totalHits, + }: { + shouldSidebarBeOpen: boolean; + shouldHistogramBeOpen: boolean; + isChartAvailable: boolean; + totalHits: string; + }) { + expect(await PageObjects.discover.getHitCount()).to.be(totalHits); + + if (shouldSidebarBeOpen) { + expect(await PageObjects.discover.isSidebarPanelOpen()).to.be(true); + await testSubjects.existOrFail('unifiedFieldListSidebar__toggle-collapse'); + await testSubjects.missingOrFail('dscShowSidebarButton'); + } else { + expect(await PageObjects.discover.isSidebarPanelOpen()).to.be(false); + await testSubjects.missingOrFail('unifiedFieldListSidebar__toggle-collapse'); + await testSubjects.existOrFail('dscShowSidebarButton'); + } + + if (isChartAvailable) { + expect(await PageObjects.discover.isChartVisible()).to.be(shouldHistogramBeOpen); + if (shouldHistogramBeOpen) { + await testSubjects.existOrFail('dscPanelsToggleInHistogram'); + await testSubjects.existOrFail('dscHideHistogramButton'); + + await testSubjects.missingOrFail('dscPanelsToggleInPage'); + await testSubjects.missingOrFail('dscShowHistogramButton'); + } else { + await testSubjects.existOrFail('dscPanelsToggleInPage'); + await testSubjects.existOrFail('dscShowHistogramButton'); + + await testSubjects.missingOrFail('dscPanelsToggleInHistogram'); + await testSubjects.missingOrFail('dscHideHistogramButton'); + } + } else { + expect(await PageObjects.discover.isChartVisible()).to.be(false); + await testSubjects.missingOrFail('dscPanelsToggleInHistogram'); + await testSubjects.missingOrFail('dscHideHistogramButton'); + await testSubjects.missingOrFail('dscShowHistogramButton'); + + if (shouldSidebarBeOpen) { + await testSubjects.missingOrFail('dscPanelsToggleInPage'); + } else { + await testSubjects.existOrFail('dscPanelsToggleInPage'); + } + } + } + + function checkPanelsToggle({ + isChartAvailable, + totalHits, + }: { + isChartAvailable: boolean; + totalHits: string; + }) { + it('sidebar can be toggled', async () => { + await checkSidebarAndHistogram({ + shouldSidebarBeOpen: true, + shouldHistogramBeOpen: true, + isChartAvailable, + totalHits, + }); + + await PageObjects.discover.closeSidebar(); + + await checkSidebarAndHistogram({ + shouldSidebarBeOpen: false, + shouldHistogramBeOpen: true, + isChartAvailable, + totalHits, + }); + + await PageObjects.discover.openSidebar(); + + await checkSidebarAndHistogram({ + shouldSidebarBeOpen: true, + shouldHistogramBeOpen: true, + isChartAvailable, + totalHits, + }); + }); + + if (isChartAvailable) { + it('histogram can be toggled', async () => { + await checkSidebarAndHistogram({ + shouldSidebarBeOpen: true, + shouldHistogramBeOpen: true, + isChartAvailable, + totalHits, + }); + + await PageObjects.discover.closeHistogramPanel(); + + await checkSidebarAndHistogram({ + shouldSidebarBeOpen: true, + shouldHistogramBeOpen: false, + isChartAvailable, + totalHits, + }); + + await PageObjects.discover.openHistogramPanel(); + + await checkSidebarAndHistogram({ + shouldSidebarBeOpen: true, + shouldHistogramBeOpen: true, + isChartAvailable, + totalHits, + }); + }); + + it('sidebar and histogram can be toggled', async () => { + await checkSidebarAndHistogram({ + shouldSidebarBeOpen: true, + shouldHistogramBeOpen: true, + isChartAvailable, + totalHits, + }); + + await PageObjects.discover.closeSidebar(); + await PageObjects.discover.closeHistogramPanel(); + + await checkSidebarAndHistogram({ + shouldSidebarBeOpen: false, + shouldHistogramBeOpen: false, + isChartAvailable, + totalHits, + }); + + await PageObjects.discover.openSidebar(); + await PageObjects.discover.openHistogramPanel(); + + await checkSidebarAndHistogram({ + shouldSidebarBeOpen: true, + shouldHistogramBeOpen: true, + isChartAvailable, + totalHits, + }); + }); + } + } + + describe('time based data view', function () { + before(async function () { + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.uiSettings.update(defaultSettings); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded(); + }); + + checkPanelsToggle({ isChartAvailable: true, totalHits: '14,004' }); + }); + + describe('non-time based data view', function () { + before(async function () { + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.uiSettings.update(defaultSettings); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.createAdHocDataView('log*', false); + await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded(); + }); + + checkPanelsToggle({ isChartAvailable: false, totalHits: '14,004' }); + }); + + describe('text-based with histogram chart', function () { + before(async function () { + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.uiSettings.update(defaultSettings); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.selectTextBaseLang(); + await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded(); + }); + + checkPanelsToggle({ isChartAvailable: true, totalHits: '10' }); + }); + + describe('text-based with aggs chart', function () { + before(async function () { + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.uiSettings.update(defaultSettings); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.selectTextBaseLang(); + await monacoEditor.setCodeEditorValue( + 'from logstash-* | stats avg(bytes) by extension | limit 100' + ); + await testSubjects.click('querySubmitButton'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded(); + }); + + checkPanelsToggle({ isChartAvailable: true, totalHits: '5' }); + }); + + describe('text-based without a time field', function () { + before(async function () { + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.uiSettings.update(defaultSettings); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.createAdHocDataView('log*', false); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.selectTextBaseLang(); + await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded(); + }); + + checkPanelsToggle({ isChartAvailable: false, totalHits: '10' }); + }); + }); +} diff --git a/test/functional/apps/discover/group3/_request_counts.ts b/test/functional/apps/discover/group3/_request_counts.ts index a1038b3f7e4eed1..d462155a3e02932 100644 --- a/test/functional/apps/discover/group3/_request_counts.ts +++ b/test/functional/apps/discover/group3/_request_counts.ts @@ -240,7 +240,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { savedSearch: 'esql test', query1: 'from logstash-* | where bytes > 1000 | stats countB = count(bytes) ', query2: 'from logstash-* | where bytes < 2000 | stats countB = count(bytes) ', - savedSearchesRequests: 4, + savedSearchesRequests: 3, setQuery: (query) => monacoEditor.setCodeEditorValue(query), }); }); diff --git a/test/functional/apps/discover/group3/_sidebar.ts b/test/functional/apps/discover/group3/_sidebar.ts index 313c350209930b2..cae06dd375b468c 100644 --- a/test/functional/apps/discover/group3/_sidebar.ts +++ b/test/functional/apps/discover/group3/_sidebar.ts @@ -273,13 +273,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should collapse when clicked', async function () { - await PageObjects.discover.toggleSidebarCollapse(); - await testSubjects.existOrFail('discover-sidebar'); + await PageObjects.discover.closeSidebar(); + await testSubjects.existOrFail('dscShowSidebarButton'); await testSubjects.missingOrFail('fieldList'); }); it('should expand when clicked', async function () { - await PageObjects.discover.toggleSidebarCollapse(); + await PageObjects.discover.openSidebar(); await testSubjects.existOrFail('discover-sidebar'); await testSubjects.existOrFail('fieldList'); }); diff --git a/test/functional/apps/discover/group3/index.ts b/test/functional/apps/discover/group3/index.ts index 848bdc84def4d1d..e2e0706cd6b4a3a 100644 --- a/test/functional/apps/discover/group3/index.ts +++ b/test/functional/apps/discover/group3/index.ts @@ -27,5 +27,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_doc_viewer')); loadTestFile(require.resolve('./_view_mode_toggle')); loadTestFile(require.resolve('./_unsaved_changes_badge')); + loadTestFile(require.resolve('./_panels_toggle')); }); } diff --git a/test/functional/apps/discover/group4/_esql_view.ts b/test/functional/apps/discover/group4/_esql_view.ts index 2b6547152970d0e..fd9060f9b9ec85f 100644 --- a/test/functional/apps/discover/group4/_esql_view.ts +++ b/test/functional/apps/discover/group4/_esql_view.ts @@ -52,7 +52,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await testSubjects.exists('addFilter')).to.be(true); expect(await testSubjects.exists('dscViewModeDocumentButton')).to.be(true); expect(await testSubjects.exists('unifiedHistogramChart')).to.be(true); - expect(await testSubjects.exists('unifiedHistogramQueryHits')).to.be(true); + expect(await testSubjects.exists('discoverQueryHits')).to.be(true); expect(await testSubjects.exists('discoverAlertsButton')).to.be(true); expect(await testSubjects.exists('shareTopNavButton')).to.be(true); expect(await testSubjects.exists('docTableExpandToggleColumn')).to.be(true); @@ -74,7 +74,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await testSubjects.exists('dscViewModeDocumentButton')).to.be(false); // when Lens suggests a table, we render an ESQL based histogram expect(await testSubjects.exists('unifiedHistogramChart')).to.be(true); - expect(await testSubjects.exists('unifiedHistogramQueryHits')).to.be(true); + expect(await testSubjects.exists('discoverQueryHits')).to.be(true); expect(await testSubjects.exists('discoverAlertsButton')).to.be(true); expect(await testSubjects.exists('shareTopNavButton')).to.be(true); expect(await testSubjects.exists('dataGridColumnSortingButton')).to.be(false); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 3ec60eae8e40792..658e235c77d3325 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -215,12 +215,26 @@ export class DiscoverPageObject extends FtrService { ); } - public async chooseBreakdownField(field: string) { - await this.comboBox.set('unifiedHistogramBreakdownFieldSelector', field); + public async chooseBreakdownField(field: string, value?: string) { + await this.retry.try(async () => { + await this.testSubjects.click('unifiedHistogramBreakdownSelectorButton'); + await this.testSubjects.existOrFail('unifiedHistogramBreakdownSelectorSelectable'); + }); + + await ( + await this.testSubjects.find('unifiedHistogramBreakdownSelectorSelectorSearch') + ).type(field); + + const option = await this.find.byCssSelector( + `[data-test-subj="unifiedHistogramBreakdownSelectorSelectable"] .euiSelectableListItem[value="${ + value ?? field + }"]` + ); + await option.click(); } public async clearBreakdownField() { - await this.comboBox.clear('unifiedHistogramBreakdownFieldSelector'); + await this.chooseBreakdownField('No breakdown', '__EMPTY_SELECTOR_OPTION__'); } public async chooseLensChart(chart: string) { @@ -248,36 +262,52 @@ export class DiscoverPageObject extends FtrService { } public async toggleChartVisibility() { - await this.testSubjects.moveMouseTo('unifiedHistogramChartOptionsToggle'); - await this.testSubjects.click('unifiedHistogramChartOptionsToggle'); - await this.testSubjects.exists('unifiedHistogramChartToggle'); - await this.testSubjects.click('unifiedHistogramChartToggle'); + if (await this.isChartVisible()) { + await this.testSubjects.click('dscHideHistogramButton'); + } else { + await this.testSubjects.click('dscShowHistogramButton'); + } + await this.header.waitUntilLoadingHasFinished(); + } + + public async openHistogramPanel() { + await this.testSubjects.click('dscShowHistogramButton'); + await this.header.waitUntilLoadingHasFinished(); + } + + public async closeHistogramPanel() { + await this.testSubjects.click('dscHideHistogramButton'); await this.header.waitUntilLoadingHasFinished(); } public async getChartInterval() { - await this.testSubjects.click('unifiedHistogramChartOptionsToggle'); - await this.testSubjects.click('unifiedHistogramTimeIntervalPanel'); - const selectedOption = await this.find.byCssSelector(`.unifiedHistogramIntervalSelected`); - return selectedOption.getVisibleText(); + const button = await this.testSubjects.find('unifiedHistogramTimeIntervalSelectorButton'); + return await button.getAttribute('data-selected-value'); } public async getChartIntervalWarningIcon() { - await this.testSubjects.click('unifiedHistogramChartOptionsToggle'); await this.header.waitUntilLoadingHasFinished(); - return await this.find.existsByCssSelector('.euiToolTipAnchor'); + return await this.find.existsByCssSelector( + '[data-test-subj="unifiedHistogramRendered"] .euiToolTipAnchor' + ); } - public async setChartInterval(interval: string) { - await this.testSubjects.click('unifiedHistogramChartOptionsToggle'); - await this.testSubjects.click('unifiedHistogramTimeIntervalPanel'); - await this.testSubjects.click(`unifiedHistogramTimeInterval-${interval}`); + public async setChartInterval(intervalTitle: string) { + await this.retry.try(async () => { + await this.testSubjects.click('unifiedHistogramTimeIntervalSelectorButton'); + await this.testSubjects.existOrFail('unifiedHistogramTimeIntervalSelectorSelectable'); + }); + + const option = await this.find.byCssSelector( + `[data-test-subj="unifiedHistogramTimeIntervalSelectorSelectable"] .euiSelectableListItem[title="${intervalTitle}"]` + ); + await option.click(); return await this.header.waitUntilLoadingHasFinished(); } public async getHitCount() { await this.header.waitUntilLoadingHasFinished(); - return await this.testSubjects.getVisibleText('unifiedHistogramQueryHits'); + return await this.testSubjects.getVisibleText('discoverQueryHits'); } public async getHitCountInt() { @@ -398,8 +428,12 @@ export class DiscoverPageObject extends FtrService { return await Promise.all(marks.map((mark) => mark.getVisibleText())); } - public async toggleSidebarCollapse() { - return await this.testSubjects.click('unifiedFieldListSidebar__toggle'); + public async openSidebar() { + await this.testSubjects.click('dscShowSidebarButton'); + + await this.retry.waitFor('sidebar to appear', async () => { + return await this.isSidebarPanelOpen(); + }); } public async closeSidebar() { @@ -410,6 +444,13 @@ export class DiscoverPageObject extends FtrService { }); } + public async isSidebarPanelOpen() { + return ( + (await this.testSubjects.exists('fieldList')) && + (await this.testSubjects.exists('unifiedFieldListSidebar__toggle-collapse')) + ); + } + public async editField(field: string) { await this.retry.try(async () => { await this.unifiedFieldList.pressEnterFieldListItemToggle(field); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 6a1c64ae4f8abde..0c0f308dc2ed831 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2440,6 +2440,9 @@ "discover.viewAlert.searchSourceErrorTitle": "Erreur lors de la récupération de la source de recherche", "discover.viewModes.document.label": "Documents", "discover.viewModes.fieldStatistics.label": "Statistiques de champ", + "discover.hitsCounter.hitsPluralTitle": "{formattedHits} {hits, plural, one {résultat} many {résultats} other {résultats}}", + "discover.hitsCounter.partialHitsPluralTitle": "≥{formattedHits} {hits, plural, one {résultat} many {résultats} other {résultats}}", + "discover.hitsCounter.hitCountSpinnerAriaLabel": "Nombre final de résultats toujours en chargement", "domDragDrop.announce.cancelled": "Mouvement annulé. {label} revenu à sa position initiale", "domDragDrop.announce.cancelledItem": "Mouvement annulé. {label} revenu au groupe {groupLabel} à la position {position}", "domDragDrop.announce.dropped.combineCompatible": "{label} combiné dans le groupe {groupLabel} en {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition} dans le calque {dropLayerNumber}", @@ -6186,7 +6189,6 @@ "unifiedFieldList.fieldListSidebar.fieldsMobileButtonLabel": "Champs", "unifiedFieldList.fieldListSidebar.flyoutBackIcon": "Retour", "unifiedFieldList.fieldListSidebar.flyoutHeading": "Liste des champs", - "unifiedFieldList.fieldListSidebar.indexAndFieldsSectionAriaLabel": "Index et champs", "unifiedFieldList.fieldListSidebar.toggleSidebarLegend": "Activer/Désactiver la barre latérale", "unifiedFieldList.fieldNameSearch.filterByNameLabel": "Rechercher les noms de champs", "unifiedFieldList.fieldPopover.addExistsFilterLabel": "Filtrer sur le champ", @@ -6231,31 +6233,18 @@ "unifiedHistogram.breakdownColumnLabel": "Top 3 des valeurs de {fieldName}", "unifiedHistogram.bucketIntervalTooltip": "Cet intervalle crée {bucketsDescription} pour un affichage dans la plage temporelle sélectionnée. Il a donc été scalé à {bucketIntervalDescription}.", "unifiedHistogram.histogramTimeRangeIntervalDescription": "(intervalle : {value})", - "unifiedHistogram.hitsPluralTitle": "{formattedHits} {hits, plural, one {résultat} many {résultats} other {résultats}}", - "unifiedHistogram.partialHits": "≥{formattedHits} {hits, plural, one {résultat} many {résultats} other {résultats}}", - "unifiedHistogram.timeIntervalWithValue": "Intervalle de temps : {timeInterval}", - "unifiedHistogram.breakdownFieldSelectorAriaLabel": "Répartir par", - "unifiedHistogram.breakdownFieldSelectorLabel": "Répartir par", - "unifiedHistogram.breakdownFieldSelectorPlaceholder": "Sélectionner un champ", "unifiedHistogram.bucketIntervalTooltip.tooLargeBucketsText": "des compartiments trop volumineux", "unifiedHistogram.bucketIntervalTooltip.tooManyBucketsText": "un trop grand nombre de compartiments", - "unifiedHistogram.chartOptions": "Options de graphique", - "unifiedHistogram.chartOptionsButton": "Options de graphique", "unifiedHistogram.countColumnLabel": "Nombre d'enregistrements", "unifiedHistogram.editVisualizationButton": "Modifier la visualisation", - "unifiedHistogram.hideChart": "Masquer le graphique", "unifiedHistogram.histogramOfFoundDocumentsAriaLabel": "Histogramme des documents détectés", "unifiedHistogram.histogramTimeRangeIntervalAuto": "Auto", "unifiedHistogram.histogramTimeRangeIntervalLoading": "Chargement", - "unifiedHistogram.hitCountSpinnerAriaLabel": "Nombre final de résultats toujours en chargement", "unifiedHistogram.inspectorRequestDataTitleTotalHits": "Nombre total de résultats", "unifiedHistogram.inspectorRequestDescriptionTotalHits": "Cette requête interroge Elasticsearch afin de récupérer le nombre total de résultats.", "unifiedHistogram.lensTitle": "Modifier la visualisation", - "unifiedHistogram.resetChartHeight": "Réinitialiser à la hauteur par défaut", "unifiedHistogram.saveVisualizationButton": "Enregistrer la visualisation", - "unifiedHistogram.showChart": "Afficher le graphique", "unifiedHistogram.suggestionSelectorPlaceholder": "Sélectionner la visualisation", - "unifiedHistogram.timeIntervals": "Intervalles de temps", "unifiedHistogram.timeIntervalWithValueWarning": "Avertissement", "unifiedSearch.filter.filterBar.filterActionsMessage": "Filtrer : {innerText}. Sélectionner pour plus d’actions de filtrage.", "unifiedSearch.filter.filterBar.filterItemBadgeIconAriaLabel": "Supprimer {filter}", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index be447f43db5c8bf..45f502c6125ad9c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2454,6 +2454,9 @@ "discover.viewAlert.searchSourceErrorTitle": "検索ソースの取得エラー", "discover.viewModes.document.label": "ドキュメント", "discover.viewModes.fieldStatistics.label": "フィールド統計情報", + "discover.hitsCounter.hitsPluralTitle": "{formattedHits} {hits, plural, other {ヒット}}", + "discover.hitsCounter.partialHitsPluralTitle": "≥{formattedHits}{hits, plural, other {ヒット}}", + "discover.hitsCounter.hitCountSpinnerAriaLabel": "読み込み中の最終一致件数", "domDragDrop.announce.cancelled": "移動がキャンセルされました。{label}は初期位置に戻りました", "domDragDrop.announce.cancelledItem": "移動がキャンセルされました。{label}は位置{position}の{groupLabel}グループに戻りました", "domDragDrop.announce.dropped.combineCompatible": "レイヤー{dropLayerNumber}の位置{dropPosition}でグループ{dropGroupLabel}の{dropLabel}にグループ{groupLabel}の{label}を結合しました。", @@ -6201,7 +6204,6 @@ "unifiedFieldList.fieldListSidebar.fieldsMobileButtonLabel": "フィールド", "unifiedFieldList.fieldListSidebar.flyoutBackIcon": "戻る", "unifiedFieldList.fieldListSidebar.flyoutHeading": "フィールドリスト", - "unifiedFieldList.fieldListSidebar.indexAndFieldsSectionAriaLabel": "インデックスとフィールド", "unifiedFieldList.fieldListSidebar.toggleSidebarLegend": "サイドバーを切り替える", "unifiedFieldList.fieldNameSearch.filterByNameLabel": "検索フィールド名", "unifiedFieldList.fieldPopover.addExistsFilterLabel": "フィールド表示のフィルター", @@ -6246,31 +6248,18 @@ "unifiedHistogram.breakdownColumnLabel": "{fieldName}のトップ3の値", "unifiedHistogram.bucketIntervalTooltip": "この間隔は選択された時間範囲に表示される{bucketsDescription}を作成するため、{bucketIntervalDescription}にスケーリングされています。", "unifiedHistogram.histogramTimeRangeIntervalDescription": "(間隔:{value})", - "unifiedHistogram.hitsPluralTitle": "{formattedHits} {hits, plural, other {ヒット}}", - "unifiedHistogram.partialHits": "≥{formattedHits}{hits, plural, other {ヒット}}", - "unifiedHistogram.timeIntervalWithValue": "時間間隔:{timeInterval}", - "unifiedHistogram.breakdownFieldSelectorAriaLabel": "内訳の基準", - "unifiedHistogram.breakdownFieldSelectorLabel": "内訳の基準", - "unifiedHistogram.breakdownFieldSelectorPlaceholder": "フィールドを選択", "unifiedHistogram.bucketIntervalTooltip.tooLargeBucketsText": "大きすぎるバケット", "unifiedHistogram.bucketIntervalTooltip.tooManyBucketsText": "バケットが多すぎます", - "unifiedHistogram.chartOptions": "グラフオプション", - "unifiedHistogram.chartOptionsButton": "グラフオプション", "unifiedHistogram.countColumnLabel": "レコード数", "unifiedHistogram.editVisualizationButton": "ビジュアライゼーションを編集", - "unifiedHistogram.hideChart": "グラフを非表示", "unifiedHistogram.histogramOfFoundDocumentsAriaLabel": "検出されたドキュメントのヒストグラム", "unifiedHistogram.histogramTimeRangeIntervalAuto": "自動", "unifiedHistogram.histogramTimeRangeIntervalLoading": "読み込み中", - "unifiedHistogram.hitCountSpinnerAriaLabel": "読み込み中の最終一致件数", "unifiedHistogram.inspectorRequestDataTitleTotalHits": "総ヒット数", "unifiedHistogram.inspectorRequestDescriptionTotalHits": "このリクエストはElasticsearchにクエリをかけ、合計一致数を取得します。", "unifiedHistogram.lensTitle": "ビジュアライゼーションを編集", - "unifiedHistogram.resetChartHeight": "デフォルトの高さにリセット", "unifiedHistogram.saveVisualizationButton": "ビジュアライゼーションを保存", - "unifiedHistogram.showChart": "グラフを表示", "unifiedHistogram.suggestionSelectorPlaceholder": "ビジュアライゼーションを選択", - "unifiedHistogram.timeIntervals": "時間間隔", "unifiedHistogram.timeIntervalWithValueWarning": "警告", "unifiedSearch.filter.filterBar.filterActionsMessage": "フィルター:{innerText}。他のフィルターアクションを使用するには選択してください。", "unifiedSearch.filter.filterBar.filterItemBadgeIconAriaLabel": "{filter}削除", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0a23b53e0215d60..3590366f31a3468 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2454,6 +2454,9 @@ "discover.viewAlert.searchSourceErrorTitle": "提取搜索源时出错", "discover.viewModes.document.label": "文档", "discover.viewModes.fieldStatistics.label": "字段统计信息", + "discover.hitsCounter.hitsPluralTitle": "{formattedHits} {hits, plural, other {命中数}}", + "discover.hitsCounter.partialHitsPluralTitle": "≥{formattedHits} {hits, plural, other {命中数}}", + "discover.hitsCounter.hitCountSpinnerAriaLabel": "最终命中计数仍在加载", "domDragDrop.announce.cancelled": "移动已取消。{label} 已返回至其初始位置", "domDragDrop.announce.cancelledItem": "移动已取消。{label} 返回至 {groupLabel} 组中的位置 {position}", "domDragDrop.announce.dropped.combineCompatible": "已将组 {groupLabel} 中的 {label} 组合到图层 {dropLayerNumber} 的组 {dropGroupLabel} 中的位置 {dropPosition} 上的 {dropLabel}", @@ -6294,7 +6297,6 @@ "unifiedFieldList.fieldListSidebar.fieldsMobileButtonLabel": "字段", "unifiedFieldList.fieldListSidebar.flyoutBackIcon": "返回", "unifiedFieldList.fieldListSidebar.flyoutHeading": "字段列表", - "unifiedFieldList.fieldListSidebar.indexAndFieldsSectionAriaLabel": "索引和字段", "unifiedFieldList.fieldListSidebar.toggleSidebarLegend": "切换侧边栏", "unifiedFieldList.fieldNameSearch.filterByNameLabel": "搜索字段名称", "unifiedFieldList.fieldPopover.addExistsFilterLabel": "筛留存在的字段", @@ -6339,31 +6341,18 @@ "unifiedHistogram.breakdownColumnLabel": "{fieldName} 的排名前 3 值", "unifiedHistogram.bucketIntervalTooltip": "此时间间隔创建的{bucketsDescription}无法在选定时间范围内显示,因此已调整为 {bucketIntervalDescription}。", "unifiedHistogram.histogramTimeRangeIntervalDescription": "(时间间隔:{value})", - "unifiedHistogram.hitsPluralTitle": "{formattedHits} {hits, plural, other {命中数}}", - "unifiedHistogram.partialHits": "≥{formattedHits} {hits, plural, other {命中数}}", - "unifiedHistogram.timeIntervalWithValue": "时间间隔:{timeInterval}", - "unifiedHistogram.breakdownFieldSelectorAriaLabel": "细分方式", - "unifiedHistogram.breakdownFieldSelectorLabel": "细分方式", - "unifiedHistogram.breakdownFieldSelectorPlaceholder": "选择字段", "unifiedHistogram.bucketIntervalTooltip.tooLargeBucketsText": "存储桶过大", "unifiedHistogram.bucketIntervalTooltip.tooManyBucketsText": "存储桶过多", - "unifiedHistogram.chartOptions": "图表选项", - "unifiedHistogram.chartOptionsButton": "图表选项", "unifiedHistogram.countColumnLabel": "记录计数", "unifiedHistogram.editVisualizationButton": "编辑可视化", - "unifiedHistogram.hideChart": "隐藏图表", "unifiedHistogram.histogramOfFoundDocumentsAriaLabel": "已找到文档的直方图", "unifiedHistogram.histogramTimeRangeIntervalAuto": "自动", "unifiedHistogram.histogramTimeRangeIntervalLoading": "正在加载", - "unifiedHistogram.hitCountSpinnerAriaLabel": "最终命中计数仍在加载", "unifiedHistogram.inspectorRequestDataTitleTotalHits": "总命中数", "unifiedHistogram.inspectorRequestDescriptionTotalHits": "此请求将查询 Elasticsearch 以获取总命中数。", "unifiedHistogram.lensTitle": "编辑可视化", - "unifiedHistogram.resetChartHeight": "重置为默认高度", "unifiedHistogram.saveVisualizationButton": "保存可视化", - "unifiedHistogram.showChart": "显示图表", "unifiedHistogram.suggestionSelectorPlaceholder": "选择可视化", - "unifiedHistogram.timeIntervals": "时间间隔", "unifiedHistogram.timeIntervalWithValueWarning": "警告", "unifiedSearch.filter.filterBar.filterActionsMessage": "筛选:{innerText}。选择以获取更多筛选操作。", "unifiedSearch.filter.filterBar.filterItemBadgeIconAriaLabel": "删除 {filter}", diff --git a/x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts b/x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts index cfe9cc7e5f3045a..fc8cbd2b38d6643 100644 --- a/x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts +++ b/x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts @@ -69,7 +69,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ).getVisibleText(); expect(actualIndexPattern).to.be('*stash*'); - const actualDiscoverQueryHits = await testSubjects.getVisibleText('unifiedHistogramQueryHits'); + const actualDiscoverQueryHits = await testSubjects.getVisibleText('discoverQueryHits'); expect(actualDiscoverQueryHits).to.be('14,005'); expect(await PageObjects.unifiedSearch.isAdHocDataView()).to.be(true); }; @@ -208,9 +208,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ).getVisibleText(); expect(actualIndexPattern).to.be('*stash*'); - const actualDiscoverQueryHits = await testSubjects.getVisibleText( - 'unifiedHistogramQueryHits' - ); + const actualDiscoverQueryHits = await testSubjects.getVisibleText('discoverQueryHits'); expect(actualDiscoverQueryHits).to.be('14,005'); const prevDataViewId = await PageObjects.discover.getCurrentDataViewId(); diff --git a/x-pack/test/functional/services/aiops/log_pattern_analysis_page.ts b/x-pack/test/functional/services/aiops/log_pattern_analysis_page.ts index 9c0ab19ac64c4a8..18b5004393027b7 100644 --- a/x-pack/test/functional/services/aiops/log_pattern_analysis_page.ts +++ b/x-pack/test/functional/services/aiops/log_pattern_analysis_page.ts @@ -133,13 +133,13 @@ export function LogPatternAnalysisPageProvider({ getService, getPageObject }: Ft async assertDiscoverDocCountExists() { await retry.tryForTime(30 * 1000, async () => { - await testSubjects.existOrFail('unifiedHistogramQueryHits'); + await testSubjects.existOrFail('discoverQueryHits'); }); }, async assertDiscoverDocCount(expectedDocCount: number) { await retry.tryForTime(5000, async () => { - const docCount = await testSubjects.getVisibleText('unifiedHistogramQueryHits'); + const docCount = await testSubjects.getVisibleText('discoverQueryHits'); const formattedDocCount = docCount.replaceAll(',', ''); expect(formattedDocCount).to.eql( expectedDocCount, diff --git a/x-pack/test/functional/services/transform/discover.ts b/x-pack/test/functional/services/transform/discover.ts index 07bd66cce1780ff..02cf7a98310ebe2 100644 --- a/x-pack/test/functional/services/transform/discover.ts +++ b/x-pack/test/functional/services/transform/discover.ts @@ -14,11 +14,9 @@ export function TransformDiscoverProvider({ getService }: FtrProviderContext) { return { async assertDiscoverQueryHits(expectedDiscoverQueryHits: string) { - await testSubjects.existOrFail('unifiedHistogramQueryHits'); + await testSubjects.existOrFail('discoverQueryHits'); - const actualDiscoverQueryHits = await testSubjects.getVisibleText( - 'unifiedHistogramQueryHits' - ); + const actualDiscoverQueryHits = await testSubjects.getVisibleText('discoverQueryHits'); expect(actualDiscoverQueryHits).to.eql( expectedDiscoverQueryHits, @@ -27,18 +25,16 @@ export function TransformDiscoverProvider({ getService }: FtrProviderContext) { }, async assertDiscoverQueryHitsMoreThanZero() { - await testSubjects.existOrFail('unifiedHistogramQueryHits'); + await testSubjects.existOrFail('discoverQueryHits'); - const actualDiscoverQueryHits = await testSubjects.getVisibleText( - 'unifiedHistogramQueryHits' - ); + const actualDiscoverQueryHits = await testSubjects.getVisibleText('discoverQueryHits'); const hits = parseInt(actualDiscoverQueryHits, 10); expect(hits).to.greaterThan(0, `Discover query hits should be more than 0, got ${hits}`); }, async assertNoResults(expectedDestinationIndex: string) { - await testSubjects.missingOrFail('unifiedHistogramQueryHits'); + await testSubjects.missingOrFail('discoverQueryHits'); // Discover should use the destination index pattern const actualIndexPatternSwitchLinkText = await ( diff --git a/x-pack/test/security_solution_cypress/cypress/screens/discover.ts b/x-pack/test/security_solution_cypress/cypress/screens/discover.ts index 6b53754cde44f40..8b2d9ccac1bfa39 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/discover.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/discover.ts @@ -43,7 +43,7 @@ export const DISCOVER_FILTER_BADGES = `${DISCOVER_CONTAINER} ${getDataTestSubjec 'filter-badge-' )}`; -export const DISCOVER_RESULT_HITS = getDataTestSubjectSelector('unifiedHistogramQueryHits'); +export const DISCOVER_RESULT_HITS = getDataTestSubjectSelector('discoverQueryHits'); export const DISCOVER_FIELDS_LOADING = getDataTestSubjectSelector( 'fieldListGroupedAvailableFields-countLoading' diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group1/_discover.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group1/_discover.ts index 684278f7e863897..9768eb43e76ca4b 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/group1/_discover.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/group1/_discover.ts @@ -117,10 +117,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show correct initial chart interval of Auto', async function () { await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.discover.waitUntilSearchingHasFinished(); - await testSubjects.click('unifiedHistogramQueryHits'); // to cancel out tooltips + await testSubjects.click('discoverQueryHits'); // to cancel out tooltips const actualInterval = await PageObjects.discover.getChartInterval(); - const expectedInterval = 'Auto'; + const expectedInterval = 'auto'; expect(actualInterval).to.be(expectedInterval); }); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group1/_discover_histogram.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group1/_discover_histogram.ts index ccba699ffa71050..cf581ad3edb510d 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/group1/_discover_histogram.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/group1/_discover_histogram.ts @@ -159,6 +159,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const chartCanvasExist = await elasticChart.canvasExists(); expect(chartCanvasExist).to.be(true); const chartIntervalIconTip = await PageObjects.discover.getChartIntervalWarningIcon(); + expect(chartIntervalIconTip).to.be(false); + }); + it('should visualize monthly data with different years scaled to seconds', async () => { + const from = 'Jan 1, 2010 @ 00:00:00.000'; + const to = 'Mar 21, 2019 @ 00:00:00.000'; + await prepareTest({ from, to }, 'Second'); + const chartCanvasExist = await elasticChart.canvasExists(); + expect(chartCanvasExist).to.be(true); + const chartIntervalIconTip = await PageObjects.discover.getChartIntervalWarningIcon(); expect(chartIntervalIconTip).to.be(true); }); it('should allow hide/show histogram, persisted in url state', async () => { @@ -167,8 +176,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await prepareTest({ from, to }); let canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(true); - await testSubjects.click('unifiedHistogramChartOptionsToggle'); - await testSubjects.click('unifiedHistogramChartToggle'); + await PageObjects.discover.toggleChartVisibility(); await retry.try(async () => { canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(false); @@ -177,8 +185,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.refresh(); canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(false); - await testSubjects.click('unifiedHistogramChartOptionsToggle'); - await testSubjects.click('unifiedHistogramChartToggle'); + await PageObjects.discover.toggleChartVisibility(); await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async () => { canvasExists = await elasticChart.canvasExists(); @@ -192,8 +199,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await prepareTest({ from, to }); // close chart for saved search - await testSubjects.click('unifiedHistogramChartOptionsToggle'); - await testSubjects.click('unifiedHistogramChartToggle'); + await PageObjects.discover.toggleChartVisibility(); let canvasExists: boolean; await retry.try(async () => { canvasExists = await elasticChart.canvasExists(); @@ -215,8 +221,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(canvasExists).to.be(false); // open chart for saved search - await testSubjects.click('unifiedHistogramChartOptionsToggle'); - await testSubjects.click('unifiedHistogramChartToggle'); + await PageObjects.discover.toggleChartVisibility(); await retry.waitFor(`Discover histogram to be displayed`, async () => { canvasExists = await elasticChart.canvasExists(); return canvasExists; @@ -238,8 +243,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should show permitted hidden histogram state when returning back to discover', async () => { // close chart - await testSubjects.click('unifiedHistogramChartOptionsToggle'); - await testSubjects.click('unifiedHistogramChartToggle'); + await PageObjects.discover.toggleChartVisibility(); let canvasExists: boolean; await retry.try(async () => { canvasExists = await elasticChart.canvasExists(); @@ -251,8 +255,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); // open chart - await testSubjects.click('unifiedHistogramChartOptionsToggle'); - await testSubjects.click('unifiedHistogramChartToggle'); + await PageObjects.discover.toggleChartVisibility(); await retry.try(async () => { canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(true); @@ -269,8 +272,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(canvasExists).to.be(true); // close chart - await testSubjects.click('unifiedHistogramChartOptionsToggle'); - await testSubjects.click('unifiedHistogramChartToggle'); + await PageObjects.discover.toggleChartVisibility(); await retry.try(async () => { canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(false); @@ -281,8 +283,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('discover'); await PageObjects.discover.waitUntilSearchingHasFinished(); // Make sure the chart is visible - await testSubjects.click('unifiedHistogramChartOptionsToggle'); - await testSubjects.click('unifiedHistogramChartToggle'); + await PageObjects.discover.toggleChartVisibility(); await PageObjects.discover.waitUntilSearchingHasFinished(); // type an invalid search query, hit refresh await queryBar.setQuery('this is > not valid'); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group3/_sidebar.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group3/_sidebar.ts index 0435fdffa7ede41..0504049f89ed5c0 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/group3/_sidebar.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/group3/_sidebar.ts @@ -242,13 +242,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should collapse when clicked', async function () { - await PageObjects.discover.toggleSidebarCollapse(); - await testSubjects.existOrFail('discover-sidebar'); + await PageObjects.discover.closeSidebar(); + await testSubjects.existOrFail('dscShowSidebarButton'); await testSubjects.missingOrFail('fieldList'); }); it('should expand when clicked', async function () { - await PageObjects.discover.toggleSidebarCollapse(); + await PageObjects.discover.openSidebar(); await testSubjects.existOrFail('discover-sidebar'); await testSubjects.existOrFail('fieldList'); });