({ + 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 8b6ff5880d3dc..07a37e3ba1bc3 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 e0859617f0057..179914b9fb68a 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 ae73126afde88..068f21863de6c 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 764145d72aac1..871edb89d15aa 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 c4558f4590c5b..0e5e9838f420b 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 3177adefdf49b..b820b63b461b3 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 289ad9e336b04..16f2a1c50de56 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 70773d2db521f..d3f8ccd8f990d 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 0000000000000..8d84cdcef5a0c --- /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 0000000000000..be3e819a5e073 --- /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 593608c9cac86..8d7f69c3af275 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 0000000000000..7586567d3665c --- /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 0000000000000..54a41fbb9255b --- /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 0000000000000..bd04823affd80 --- /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 7c17e5e1a31ef..e1788389d3caf 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 79c9213e76395..147486ac6dc6e 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 fe06c93232460..b75f27c9266f8 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 229af7851d8a3..4509f28a7a61e 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 c0c20a1e1a80e..86ec06ba48e67 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 77e00e157d62b..78df66f50873e 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 d561a3310ceae..474da6bce5bf7 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 a64944d73b5fe..657f27a72c070 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,162 +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 && ( - - - - )} - {canSaveVisualization && ( - <> - - - setIsSaveModalVisible(true)} - data-test-subj="unifiedHistogramSaveVisualization" - aria-label={i18n.translate('unifiedHistogram.saveVisualizationButton', { - defaultMessage: 'Save visualization', - })} - /> - - - - )} - {canEditVisualizationOnTheFly && ( - - {!isFlyoutVisible ? ( - - {renderEditButton} - - ) : ( - renderEditButton - )} - - )} - {onEditVisualization && ( - - - 1 && ( + - - - )} - - - - - } - isOpen={showChartOptionsPopover} - closePopover={closeChartOptions} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - - - + )} +
+
+
+
+ {chartVisible && actions.length > 0 && ( + + )}
@@ -427,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 120dc0b3d0884..7696f0f9782b7 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 168db2ca0c4d9..3c4bd2434e3dd 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 e5ee2b2c55cd9..0000000000000 --- 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 bf1bf4d6b95cd..0000000000000 --- 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 13b527be702c1..5a5bf41ca395d 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 3135f3c86f465..b6250f8fa82b7 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 5bb927747e669..c16bb2335be24 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 6a6d2d65f6f92..4a8b758f7d86e 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 0196387633396..cad20279bfdf0 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 0000000000000..86c17fdc79172 --- /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 0000000000000..1a83a736bb534 --- /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 c65f1e2b43c04..fb152a1921e23 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 73a493e167c19..40304a967243a 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 f96a4b5b7b033..1a79389e2bc6f 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 03b350448e9c2..0000000000000 --- 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 b6f1212bfeaed..0000000000000 --- 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 a12c8cf46430e..a10df63e7c328 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 17eaf65fcde5f..1a175690eb447 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 a041939f2b809..0210c7d8cc7f2 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 64e9b0e47dc90..ad5563e78f918 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 2facbc95c93ce..cdce56db6e856 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 0000000000000..d471969d3528f --- /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 a1038b3f7e4ee..d462155a3e029 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 313c350209930..cae06dd375b46 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 848bdc84def4d..e2e0706cd6b4a 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 2b6547152970d..fd9060f9b9ec8 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 3ec60eae8e407..658e235c77d33 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 6a1c64ae4f8ab..0c0f308dc2ed8 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 be447f43db5c8..45f502c6125ad 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 0a23b53e0215d..3590366f31a34 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 cfe9cc7e5f304..fc8cbd2b38d66 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 9c0ab19ac64c4..18b5004393027 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 07bd66cce1780..02cf7a98310eb 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 6b53754cde44f..8b2d9ccac1bfa 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 684278f7e8638..9768eb43e76ca 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 ccba699ffa710..cf581ad3edb51 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 0435fdffa7ede..0504049f89ed5 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'); }); From 8314c73b10e4dcae547400aecc5e390c22f02cec Mon Sep 17 00:00:00 2001 From: "Eyo O. Eyo" <7893459+eokoneyo@users.noreply.github.com> Date: Fri, 12 Jan 2024 11:46:05 +0100 Subject: [PATCH 29/29] Update observability onboarding test to express state of the art (#174734) ## Summary TLDR; This PR updates the cypress test suite that tests the custom integration used to set up logs during onboarding. The PR https://github.com/elastic/kibana/pull/171720 surfaced the fact that this test suite isn't passing, especially given it doesn't run unless a file with the observability onboarding plugin directory is modified. The most notable changes to the test suite, is opting to delete the provided integration without verifying if it is installed. Why would we do this one might ask; - The existing API used to fetch integration packages expects packages to have assets definition, and given this particular one doesn't have assets so the request fails more details [here](https://github.com/elastic/kibana/issues/174739). - With the previous approach it required at least one call to determine if the integration should be deleted, the only difference here is when the test kicks off an attempt to delete an integration that doesn't exist will be made once, but that's handled by setting setting the cy.request `failOnStatusCode` option as false, so it doesn't error. - it also unblocks the aforementioned PR - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../custom_logs/install_elastic_agent.cy.ts | 4 +++- .../e2e/cypress/support/commands.ts | 23 ++++--------------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts b/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts index 19e84284b671b..c4d5edee7d3af 100644 --- a/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts +++ b/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts @@ -623,7 +623,9 @@ describe('[Logs onboarding] Custom logs - install elastic agent', () => { cy.getByTestSubj('obltOnboardingExploreLogs').should('exist').click(); cy.url().should('include', '/app/observability-log-explorer'); - cy.get('button').contains('[Mylogs] mylogs').should('exist'); + cy.get('[data-test-subj="datasetSelectorPopoverButton"]') + .contains('[Mylogs] mylogs', { matchCase: false }) + .should('exist'); }); }); }); diff --git a/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts b/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts index e989089f491eb..843aca4c258ea 100644 --- a/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts +++ b/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts @@ -125,30 +125,17 @@ Cypress.Commands.add('deleteIntegration', (integrationName: string) => { cy.request({ log: false, - method: 'GET', + method: 'DELETE', url: `${kibanaUrl}/api/fleet/epm/packages/${integrationName}`, + body: { + force: false, + }, headers: { 'kbn-xsrf': 'e2e_test', + 'Elastic-Api-Version': '1', }, auth: { user: 'editor', pass: 'changeme' }, failOnStatusCode: false, - }).then((response) => { - const status = response.body.item?.status; - if (status === 'installed') { - cy.request({ - log: false, - method: 'DELETE', - url: `${kibanaUrl}/api/fleet/epm/packages/${integrationName}`, - body: { - force: false, - }, - headers: { - 'kbn-xsrf': 'e2e_test', - 'Elastic-Api-Version': '1', - }, - auth: { user: 'editor', pass: 'changeme' }, - }); - } }); });