mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Discover] Move total hits counter from histogram to grid area. New controls in histogram. (#171638)
- Closes https://github.com/elastic/kibana/issues/168825 - Closes https://github.com/elastic/kibana/issues/171610 - Closes https://github.com/elastic/kibana/issues/167427 - Partially addresses https://github.com/elastic/kibana/issues/165192 ## Summary This PR moves the total hits counter closer to the grid, updates histogram controls and introduces new panel toggle buttons for toggling fields sidebar and histogram. <img width="500" alt="Screenshot 2023-12-05 at 15 37 20" src="5b9bd771
-1052-4205-849f-18c21cc299b8"> <img width="500" alt="Screenshot 2023-12-05 at 15 37 29" src="e5941b27
-c497-4d7e-b461-68b66931475a"> <img width="500" alt="Screenshot 2023-12-05 at 15 37 37" src="97abd32e
-9ff2-4d9a-b7e7-b9d6d9cf64db"> <img width="500" alt="Screenshot 2023-12-05 at 15 37 50" src="10f2b4f4
-ec37-41c3-b78b-78c64e14d655"> <img width="400" alt="Screenshot 2023-12-05 at 15 37 59" src="ef2e28b2
-f6ba-4ccb-aea4-3946ba2d5839"> <img width="300" alt="Screenshot 2023-12-05 at 15 38 05" src="07901ede
-0bcb-46a6-a398-4562189fd54f"> <img width="500" alt="Screenshot 2023-12-05 at 15 40 38" src="17830115
-2111-4b8f-ae40-7b5875c06879"> <img width="500" alt="Screenshot 2023-12-05 at 15 40 56" src="975d475b
-280b-495a-b7b7-31c7ade5f21e"> <img width="500" alt="Screenshot 2023-12-05 at 15 43 08" src="38b6053a
-e260-48d8-9591-3f3409df2876"> ## Testing When testing, please check collapsing/expanding the fields sidebar and histogram. Also for ES|QL mode with suggestions, legacy table, no results and error prompt, Field Statistics tab, data views without a time field, light/dark themes. ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [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 - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co> Co-authored-by: Davis McPhee <davis.mcphee@elastic.co>
This commit is contained in:
parent
6c36503a63
commit
aa33843863
82 changed files with 2440 additions and 1497 deletions
|
@ -304,26 +304,14 @@ export const UnifiedFieldListSidebarComponent: React.FC<UnifiedFieldListSidebarP
|
|||
return null;
|
||||
}
|
||||
|
||||
const sidebarToggleButton =
|
||||
typeof isSidebarCollapsed === 'boolean' && onToggleSidebar ? (
|
||||
<SidebarToggleButton
|
||||
buttonSize={compressed ? 's' : 'm'}
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
onChange={onToggleSidebar}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const pageSidebarProps: Partial<EuiPageSidebarProps> = {
|
||||
className: classnames('unifiedFieldListSidebar', {
|
||||
'unifiedFieldListSidebar--collapsed': isSidebarCollapsed,
|
||||
['unifiedFieldListSidebar--fullWidth']: fullWidth,
|
||||
}),
|
||||
'aria-label': i18n.translate(
|
||||
'unifiedFieldList.fieldListSidebar.indexAndFieldsSectionAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Index and fields',
|
||||
}
|
||||
),
|
||||
'aria-label': i18n.translate('unifiedFieldList.fieldListSidebar.fieldsSidebarAriaLabel', {
|
||||
defaultMessage: 'Fields',
|
||||
}),
|
||||
id:
|
||||
stateService.creationOptions.dataTestSubj?.fieldListSidebarDataTestSubj ??
|
||||
'unifiedFieldListSidebarId',
|
||||
|
@ -332,6 +320,16 @@ export const UnifiedFieldListSidebarComponent: React.FC<UnifiedFieldListSidebarP
|
|||
'unifiedFieldListSidebarId',
|
||||
};
|
||||
|
||||
const sidebarToggleButton =
|
||||
typeof isSidebarCollapsed === 'boolean' && onToggleSidebar ? (
|
||||
<SidebarToggleButton
|
||||
buttonSize={compressed ? 's' : 'm'}
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
panelId={pageSidebarProps.id}
|
||||
onChange={onToggleSidebar}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
if (isSidebarCollapsed && sidebarToggleButton) {
|
||||
return (
|
||||
<EuiHideFor sizes={['xs', 's']}>
|
||||
|
|
|
@ -17,6 +17,7 @@ import React, {
|
|||
} from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import type { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public';
|
||||
import {
|
||||
EuiBadge,
|
||||
|
@ -30,13 +31,12 @@ import {
|
|||
EuiShowFor,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import {
|
||||
useExistingFieldsFetcher,
|
||||
type ExistingFieldsFetcher,
|
||||
} from '../../hooks/use_existing_fields';
|
||||
import { useQuerySubscriber } from '../../hooks/use_query_subscriber';
|
||||
import { useSidebarToggle } from '../../hooks/use_sidebar_toggle';
|
||||
import { getSidebarVisibility, SidebarVisibility } from './get_sidebar_visibility';
|
||||
import {
|
||||
UnifiedFieldListSidebar,
|
||||
type UnifiedFieldListSidebarCustomizableProps,
|
||||
|
@ -50,7 +50,7 @@ import type {
|
|||
} from '../../types';
|
||||
|
||||
export interface UnifiedFieldListSidebarContainerApi {
|
||||
isSidebarCollapsed$: Observable<boolean>;
|
||||
sidebarVisibility: SidebarVisibility;
|
||||
refetchFieldsExistenceInfo: ExistingFieldsFetcher['refetchFieldsExistenceInfo'];
|
||||
closeFieldListFlyout: () => void;
|
||||
// no user permission or missing dataViewFieldEditor service will result in `undefined` API methods
|
||||
|
@ -122,8 +122,14 @@ const UnifiedFieldListSidebarContainer = forwardRef<
|
|||
);
|
||||
const { data, dataViewFieldEditor } = services;
|
||||
const [isFieldListFlyoutVisible, setIsFieldListFlyoutVisible] = useState<boolean>(false);
|
||||
const { isSidebarCollapsed, onToggleSidebar } = useSidebarToggle({ stateService });
|
||||
const [isSidebarCollapsed$] = useState(() => new BehaviorSubject(isSidebarCollapsed));
|
||||
const [sidebarVisibility] = useState(() =>
|
||||
getSidebarVisibility({
|
||||
localStorageKey: stateService.creationOptions.localStorageKeyPrefix
|
||||
? `${stateService.creationOptions.localStorageKeyPrefix}:sidebarClosed`
|
||||
: undefined,
|
||||
})
|
||||
);
|
||||
const isSidebarCollapsed = useObservable(sidebarVisibility.isCollapsed$, false);
|
||||
|
||||
const canEditDataView =
|
||||
Boolean(dataViewFieldEditor?.userPermissions.editIndexPattern()) ||
|
||||
|
@ -225,21 +231,17 @@ const UnifiedFieldListSidebarContainer = forwardRef<
|
|||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
isSidebarCollapsed$.next(isSidebarCollapsed);
|
||||
}, [isSidebarCollapsed, isSidebarCollapsed$]);
|
||||
|
||||
useImperativeHandle(
|
||||
componentRef,
|
||||
() => ({
|
||||
isSidebarCollapsed$,
|
||||
sidebarVisibility,
|
||||
refetchFieldsExistenceInfo,
|
||||
closeFieldListFlyout,
|
||||
createField: editField,
|
||||
editField,
|
||||
deleteField,
|
||||
}),
|
||||
[isSidebarCollapsed$, refetchFieldsExistenceInfo, closeFieldListFlyout, editField, deleteField]
|
||||
[sidebarVisibility, refetchFieldsExistenceInfo, closeFieldListFlyout, editField, deleteField]
|
||||
);
|
||||
|
||||
if (!dataView) {
|
||||
|
@ -260,7 +262,7 @@ const UnifiedFieldListSidebarContainer = forwardRef<
|
|||
|
||||
if (stateService.creationOptions.showSidebarToggleButton) {
|
||||
commonSidebarProps.isSidebarCollapsed = isSidebarCollapsed;
|
||||
commonSidebarProps.onToggleSidebar = onToggleSidebar;
|
||||
commonSidebarProps.onToggleSidebar = sidebarVisibility.toggle;
|
||||
}
|
||||
|
||||
const buttonPropsToTriggerFlyout = stateService.creationOptions.buttonPropsToTriggerFlyout;
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 { act } from '@testing-library/react-hooks';
|
||||
import { getSidebarVisibility } from './get_sidebar_visibility';
|
||||
|
||||
const localStorageKey = 'test-sidebar-visibility';
|
||||
|
||||
describe('UnifiedFieldList getSidebarVisibility', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.removeItem(localStorageKey);
|
||||
});
|
||||
|
||||
it('should toggle correctly', async () => {
|
||||
const state = getSidebarVisibility({ localStorageKey });
|
||||
|
||||
expect(state.isCollapsed$.getValue()).toBe(false);
|
||||
|
||||
act(() => {
|
||||
state.toggle(true);
|
||||
});
|
||||
|
||||
expect(state.isCollapsed$.getValue()).toBe(true);
|
||||
expect(localStorage.getItem(localStorageKey)).toBe('true');
|
||||
|
||||
act(() => {
|
||||
state.toggle(false);
|
||||
});
|
||||
|
||||
expect(state.isCollapsed$.getValue()).toBe(false);
|
||||
expect(localStorage.getItem(localStorageKey)).toBe('false');
|
||||
});
|
||||
|
||||
it('should restore collapsed state and expand from it', async () => {
|
||||
localStorage.setItem(localStorageKey, 'true');
|
||||
|
||||
const state = getSidebarVisibility({ localStorageKey });
|
||||
|
||||
expect(state.isCollapsed$.getValue()).toBe(true);
|
||||
|
||||
act(() => {
|
||||
state.toggle(false);
|
||||
});
|
||||
|
||||
expect(state.isCollapsed$.getValue()).toBe(false);
|
||||
expect(localStorage.getItem(localStorageKey)).toBe('false');
|
||||
});
|
||||
|
||||
it('should not persist if local storage key is not defined', async () => {
|
||||
const state = getSidebarVisibility({ localStorageKey: undefined });
|
||||
|
||||
expect(state.isCollapsed$.getValue()).toBe(false);
|
||||
|
||||
act(() => {
|
||||
state.toggle(true);
|
||||
});
|
||||
|
||||
expect(state.isCollapsed$.getValue()).toBe(true);
|
||||
expect(localStorage.getItem(localStorageKey)).toBe(null);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 { BehaviorSubject } from 'rxjs';
|
||||
|
||||
export interface SidebarVisibility {
|
||||
isCollapsed$: BehaviorSubject<boolean>;
|
||||
toggle: (isCollapsed: boolean) => void;
|
||||
}
|
||||
|
||||
export interface GetSidebarStateParams {
|
||||
localStorageKey?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* For managing sidebar visibility state
|
||||
* @param localStorageKey
|
||||
*/
|
||||
export const getSidebarVisibility = ({
|
||||
localStorageKey,
|
||||
}: GetSidebarStateParams): SidebarVisibility => {
|
||||
const isCollapsed$ = new BehaviorSubject<boolean>(
|
||||
localStorageKey ? getIsCollapsed(localStorageKey) : false
|
||||
);
|
||||
|
||||
return {
|
||||
isCollapsed$,
|
||||
toggle: (isCollapsed) => {
|
||||
isCollapsed$.next(isCollapsed);
|
||||
if (localStorageKey) {
|
||||
setIsCollapsed(localStorageKey, isCollapsed);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
function getIsCollapsed(localStorageKey: string) {
|
||||
let isCollapsed = false;
|
||||
try {
|
||||
isCollapsed = localStorage?.getItem(localStorageKey) === 'true';
|
||||
} catch {
|
||||
// nothing
|
||||
}
|
||||
|
||||
return isCollapsed;
|
||||
}
|
||||
|
||||
function setIsCollapsed(localStorageKey: string, isCollapsed: boolean) {
|
||||
try {
|
||||
localStorage?.setItem(localStorageKey, String(isCollapsed));
|
||||
} catch {
|
||||
// nothing
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ import { IconButtonGroup, type IconButtonGroupProps } from '@kbn/shared-ux-butto
|
|||
export interface SidebarToggleButtonProps {
|
||||
'data-test-subj'?: string;
|
||||
isSidebarCollapsed: boolean;
|
||||
panelId?: string;
|
||||
buttonSize: IconButtonGroupProps['buttonSize'];
|
||||
onChange: (isSidebarCollapsed: boolean) => void;
|
||||
}
|
||||
|
@ -24,12 +25,14 @@ export interface SidebarToggleButtonProps {
|
|||
* A toggle button for the fields sidebar
|
||||
* @param data-test-subj
|
||||
* @param isSidebarCollapsed
|
||||
* @param panelId
|
||||
* @param onChange
|
||||
* @constructor
|
||||
*/
|
||||
export const SidebarToggleButton: React.FC<SidebarToggleButtonProps> = ({
|
||||
'data-test-subj': dataTestSubj = 'unifiedFieldListSidebar__toggle',
|
||||
isSidebarCollapsed,
|
||||
panelId,
|
||||
buttonSize,
|
||||
onChange,
|
||||
}) => {
|
||||
|
@ -49,6 +52,8 @@ export const SidebarToggleButton: React.FC<SidebarToggleButtonProps> = ({
|
|||
}),
|
||||
iconType: 'transitionLeftIn',
|
||||
'data-test-subj': `${dataTestSubj}-expand`,
|
||||
'aria-expanded': false,
|
||||
'aria-controls': panelId,
|
||||
onClick: () => onChange(false),
|
||||
},
|
||||
]
|
||||
|
@ -59,6 +64,8 @@ export const SidebarToggleButton: React.FC<SidebarToggleButtonProps> = ({
|
|||
}),
|
||||
iconType: 'transitionLeftOut',
|
||||
'data-test-subj': `${dataTestSubj}-collapse`,
|
||||
'aria-expanded': true,
|
||||
'aria-controls': panelId,
|
||||
onClick: () => onChange(true),
|
||||
},
|
||||
]),
|
||||
|
|
|
@ -1,104 +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, act } from '@testing-library/react-hooks';
|
||||
import { useSidebarToggle } from './use_sidebar_toggle';
|
||||
import * as localStorageModule from 'react-use/lib/useLocalStorage';
|
||||
|
||||
jest.spyOn(localStorageModule, 'default');
|
||||
|
||||
describe('UnifiedFieldList useSidebarToggle', () => {
|
||||
const stateService = {
|
||||
creationOptions: {
|
||||
originatingApp: 'test',
|
||||
localStorageKeyPrefix: 'this',
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
(localStorageModule.default as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
it('should toggle correctly', async () => {
|
||||
const storeMock = jest.fn();
|
||||
(localStorageModule.default as jest.Mock).mockImplementation(() => {
|
||||
return [false, storeMock];
|
||||
});
|
||||
|
||||
const { result } = renderHook(useSidebarToggle, {
|
||||
initialProps: {
|
||||
stateService,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.current.isSidebarCollapsed).toBe(false);
|
||||
|
||||
act(() => {
|
||||
result.current.onToggleSidebar(true);
|
||||
});
|
||||
|
||||
expect(result.current.isSidebarCollapsed).toBe(true);
|
||||
expect(storeMock).toHaveBeenCalledWith(true);
|
||||
|
||||
act(() => {
|
||||
result.current.onToggleSidebar(false);
|
||||
});
|
||||
|
||||
expect(result.current.isSidebarCollapsed).toBe(false);
|
||||
expect(storeMock).toHaveBeenLastCalledWith(false);
|
||||
});
|
||||
|
||||
it('should restore collapsed state and expand from it', async () => {
|
||||
const storeMock = jest.fn();
|
||||
(localStorageModule.default as jest.Mock).mockImplementation(() => {
|
||||
return [true, storeMock];
|
||||
});
|
||||
|
||||
const { result } = renderHook(useSidebarToggle, {
|
||||
initialProps: {
|
||||
stateService,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.current.isSidebarCollapsed).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result.current.onToggleSidebar(false);
|
||||
});
|
||||
|
||||
expect(result.current.isSidebarCollapsed).toBe(false);
|
||||
expect(storeMock).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('should not persist if local storage key is not defined', async () => {
|
||||
const storeMock = jest.fn();
|
||||
(localStorageModule.default as jest.Mock).mockImplementation(() => {
|
||||
return [false, storeMock];
|
||||
});
|
||||
|
||||
const { result } = renderHook(useSidebarToggle, {
|
||||
initialProps: {
|
||||
stateService: {
|
||||
creationOptions: {
|
||||
originatingApp: 'test',
|
||||
localStorageKeyPrefix: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.current.isSidebarCollapsed).toBe(false);
|
||||
|
||||
act(() => {
|
||||
result.current.onToggleSidebar(true);
|
||||
});
|
||||
|
||||
expect(result.current.isSidebarCollapsed).toBe(true);
|
||||
expect(storeMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -1,64 +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 { useCallback, useState, useMemo } from 'react';
|
||||
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||
import type { UnifiedFieldListSidebarContainerStateService } from '../types';
|
||||
|
||||
/**
|
||||
* Hook params
|
||||
*/
|
||||
export interface UseSidebarToggleParams {
|
||||
/**
|
||||
* Service for managing the state
|
||||
*/
|
||||
stateService: UnifiedFieldListSidebarContainerStateService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook result type
|
||||
*/
|
||||
export interface UseSidebarToggleResult {
|
||||
isSidebarCollapsed: boolean;
|
||||
onToggleSidebar: (isSidebarCollapsed: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing sidebar toggle state
|
||||
* @param stateService
|
||||
*/
|
||||
export const useSidebarToggle = ({
|
||||
stateService,
|
||||
}: UseSidebarToggleParams): UseSidebarToggleResult => {
|
||||
const [initialIsSidebarCollapsed, storeIsSidebarCollapsed] = useLocalStorage<boolean>(
|
||||
`${stateService.creationOptions.localStorageKeyPrefix ?? 'unifiedFieldList'}:sidebarClosed`, // as legacy `discover:sidebarClosed` key
|
||||
false
|
||||
);
|
||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState<boolean>(
|
||||
initialIsSidebarCollapsed ?? false
|
||||
);
|
||||
|
||||
const onToggleSidebar = useCallback(
|
||||
(isCollapsed) => {
|
||||
setIsSidebarCollapsed(isCollapsed);
|
||||
if (stateService.creationOptions.localStorageKeyPrefix) {
|
||||
storeIsSidebarCollapsed(isCollapsed);
|
||||
}
|
||||
},
|
||||
[
|
||||
storeIsSidebarCollapsed,
|
||||
setIsSidebarCollapsed,
|
||||
stateService.creationOptions.localStorageKeyPrefix,
|
||||
]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({ isSidebarCollapsed, onToggleSidebar }),
|
||||
[isSidebarCollapsed, onToggleSidebar]
|
||||
);
|
||||
};
|
|
@ -32,6 +32,12 @@ export interface IconButton {
|
|||
title?: string;
|
||||
/** Test subject for button */
|
||||
'data-test-subj'?: string;
|
||||
/** To disable the action **/
|
||||
isDisabled?: boolean;
|
||||
/** A11y for button */
|
||||
'aria-expanded'?: boolean;
|
||||
/** A11y for button */
|
||||
'aria-controls'?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -44,6 +50,8 @@ export interface Props {
|
|||
buttons: IconButton[];
|
||||
/** Button size */
|
||||
buttonSize?: EuiButtonGroupProps['buttonSize'];
|
||||
/** Test subject for button group */
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
type Option = EuiButtonGroupOptionProps & Omit<IconButton, 'label'>;
|
||||
|
@ -51,7 +59,12 @@ type Option = EuiButtonGroupOptionProps & Omit<IconButton, 'label'>;
|
|||
/**
|
||||
* A group of buttons each performing an action, represented by an icon.
|
||||
*/
|
||||
export const IconButtonGroup = ({ buttons, legend, buttonSize = 'm' }: Props) => {
|
||||
export const IconButtonGroup = ({
|
||||
buttons,
|
||||
legend,
|
||||
buttonSize = 'm',
|
||||
'data-test-subj': dataTestSubj,
|
||||
}: Props) => {
|
||||
const euiTheme = useEuiTheme();
|
||||
const iconButtonGroupStyles = IconButtonGroupStyles(euiTheme);
|
||||
|
||||
|
@ -73,6 +86,7 @@ export const IconButtonGroup = ({ buttons, legend, buttonSize = 'm' }: Props) =>
|
|||
|
||||
return (
|
||||
<EuiButtonGroup
|
||||
data-test-subj={dataTestSubj}
|
||||
buttonSize={buttonSize}
|
||||
legend={legend}
|
||||
options={buttonGroupOptions}
|
||||
|
|
|
@ -45,6 +45,13 @@ describe('<ToolbarButton />', () => {
|
|||
component.find('button').simulate('click');
|
||||
expect(mockHandler).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('accepts an onBlur handler', () => {
|
||||
const mockHandler = jest.fn();
|
||||
const component = mountWithIntl(<ToolbarButton label="Create chart" onBlur={mockHandler} />);
|
||||
component.find('button').simulate('blur');
|
||||
expect(mockHandler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('iconButton', () => {
|
||||
|
|
|
@ -23,7 +23,7 @@ type ButtonRenderStyle = 'standard' | 'iconButton';
|
|||
interface ToolbarButtonCommonProps
|
||||
extends Pick<
|
||||
EuiButtonPropsForButton,
|
||||
'onClick' | 'iconType' | 'size' | 'data-test-subj' | 'isDisabled' | 'aria-label'
|
||||
'onClick' | 'onBlur' | 'iconType' | 'size' | 'data-test-subj' | 'isDisabled' | 'aria-label'
|
||||
> {
|
||||
/**
|
||||
* Render style of the toolbar button
|
||||
|
|
|
@ -9,7 +9,11 @@
|
|||
// focus states in Kibana.
|
||||
:focus {
|
||||
&:not([class^='eui']):not(.kbn-resetFocusState) {
|
||||
@include euiFocusRing;
|
||||
// The focus policy causes double focus rings to appear on EuiSelectableList
|
||||
// since the focusable element does not contain a class starting with "eui".
|
||||
&:not(.euiSelectableList__list > ul) {
|
||||
@include euiFocusRing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,8 +9,9 @@
|
|||
import React from 'react';
|
||||
import { BehaviorSubject, of } from 'rxjs';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { dataViewMock, esHitsMock } from '@kbn/discover-utils/src/__mocks__';
|
||||
import { savedSearchMock } from '../../../../__mocks__/saved_search';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { esHitsMock } from '@kbn/discover-utils/src/__mocks__';
|
||||
import { savedSearchMockWithTimeField } from '../../../../__mocks__/saved_search';
|
||||
import {
|
||||
AvailableFields$,
|
||||
DataDocuments$,
|
||||
|
@ -19,7 +20,7 @@ import {
|
|||
RecordRawType,
|
||||
} from '../../services/discover_data_state_container';
|
||||
import { discoverServiceMock } from '../../../../__mocks__/services';
|
||||
import { FetchStatus } from '../../../types';
|
||||
import { FetchStatus, SidebarToggleState } from '../../../types';
|
||||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { buildDataTableRecord } from '@kbn/discover-utils';
|
||||
|
@ -32,16 +33,19 @@ import { getSessionServiceMock } from '@kbn/data-plugin/public/search/session/mo
|
|||
import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock';
|
||||
import { DiscoverMainProvider } from '../../services/discover_state_provider';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { PanelsToggle } from '../../../../components/panels_toggle';
|
||||
|
||||
function getStateContainer(savedSearch?: SavedSearch) {
|
||||
const stateContainer = getDiscoverStateMock({ isTimeBased: true, savedSearch });
|
||||
const dataView = savedSearch?.searchSource?.getField('index') as DataView;
|
||||
|
||||
stateContainer.appState.update({
|
||||
index: dataView?.id,
|
||||
interval: 'auto',
|
||||
hideChart: false,
|
||||
});
|
||||
|
||||
stateContainer.internalState.transitions.setDataView(dataViewMock);
|
||||
stateContainer.internalState.transitions.setDataView(dataView);
|
||||
|
||||
return stateContainer;
|
||||
}
|
||||
|
@ -49,7 +53,7 @@ function getStateContainer(savedSearch?: SavedSearch) {
|
|||
const mountComponent = async ({
|
||||
isPlainRecord = false,
|
||||
storage,
|
||||
savedSearch = savedSearchMock,
|
||||
savedSearch = savedSearchMockWithTimeField,
|
||||
searchSessionId = '123',
|
||||
}: {
|
||||
isPlainRecord?: boolean;
|
||||
|
@ -58,6 +62,8 @@ const mountComponent = async ({
|
|||
savedSearch?: SavedSearch;
|
||||
searchSessionId?: string | null;
|
||||
} = {}) => {
|
||||
const dataView = savedSearch?.searchSource?.getField('index') as DataView;
|
||||
|
||||
let services = discoverServiceMock;
|
||||
services.data.query.timefilter.timefilter.getAbsoluteTime = () => {
|
||||
return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' };
|
||||
|
@ -85,7 +91,7 @@ const mountComponent = async ({
|
|||
|
||||
const documents$ = new BehaviorSubject({
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
result: esHitsMock.map((esHit) => buildDataTableRecord(esHit, dataViewMock)),
|
||||
result: esHitsMock.map((esHit) => buildDataTableRecord(esHit, dataView)),
|
||||
}) as DataDocuments$;
|
||||
|
||||
const availableFields$ = new BehaviorSubject({
|
||||
|
@ -115,13 +121,26 @@ const mountComponent = async ({
|
|||
|
||||
const props: DiscoverHistogramLayoutProps = {
|
||||
isPlainRecord,
|
||||
dataView: dataViewMock,
|
||||
dataView,
|
||||
stateContainer,
|
||||
onFieldEdited: jest.fn(),
|
||||
columns: [],
|
||||
viewMode: VIEW_MODE.DOCUMENT_LEVEL,
|
||||
onAddFilter: jest.fn(),
|
||||
container: null,
|
||||
panelsToggle: (
|
||||
<PanelsToggle
|
||||
stateContainer={stateContainer}
|
||||
sidebarToggleState$={
|
||||
new BehaviorSubject<SidebarToggleState>({
|
||||
isCollapsed: true,
|
||||
toggle: () => {},
|
||||
})
|
||||
}
|
||||
isChartAvailable={undefined}
|
||||
renderedFor="root"
|
||||
/>
|
||||
),
|
||||
};
|
||||
stateContainer.searchSessionManager = createSearchSessionMock(session).searchSessionManager;
|
||||
|
||||
|
@ -154,11 +173,19 @@ describe('Discover histogram layout component', () => {
|
|||
it('should not render null if there is a search session', async () => {
|
||||
const { component } = await mountComponent();
|
||||
expect(component.isEmptyRender()).toBe(false);
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
it('should not render null if there is no search session, but isPlainRecord is true', async () => {
|
||||
const { component } = await mountComponent({ isPlainRecord: true });
|
||||
expect(component.isEmptyRender()).toBe(false);
|
||||
});
|
||||
|
||||
it('should render PanelsToggle', async () => {
|
||||
const { component } = await mountComponent();
|
||||
expect(component.find(PanelsToggle).first().prop('isChartAvailable')).toBe(undefined);
|
||||
expect(component.find(PanelsToggle).first().prop('renderedFor')).toBe('histogram');
|
||||
expect(component.find(PanelsToggle).last().prop('isChartAvailable')).toBe(true);
|
||||
expect(component.find(PanelsToggle).last().prop('renderedFor')).toBe('tabs');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { UnifiedHistogramContainer } from '@kbn/unified-histogram-plugin/public';
|
||||
import { css } from '@emotion/react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
|
@ -27,6 +27,7 @@ export const DiscoverHistogramLayout = ({
|
|||
dataView,
|
||||
stateContainer,
|
||||
container,
|
||||
panelsToggle,
|
||||
...mainContentProps
|
||||
}: DiscoverHistogramLayoutProps) => {
|
||||
const { dataState } = stateContainer;
|
||||
|
@ -39,6 +40,14 @@ export const DiscoverHistogramLayout = ({
|
|||
isPlainRecord,
|
||||
});
|
||||
|
||||
const renderCustomChartToggleActions = useCallback(
|
||||
() =>
|
||||
React.isValidElement(panelsToggle)
|
||||
? React.cloneElement(panelsToggle, { renderedFor: 'histogram' })
|
||||
: panelsToggle,
|
||||
[panelsToggle]
|
||||
);
|
||||
|
||||
// Initialized when the first search has been requested or
|
||||
// when in text-based mode since search sessions are not supported
|
||||
if (!searchSessionId && !isPlainRecord) {
|
||||
|
@ -52,15 +61,14 @@ export const DiscoverHistogramLayout = ({
|
|||
requestAdapter={dataState.inspectorAdapters.requests}
|
||||
container={container}
|
||||
css={histogramLayoutCss}
|
||||
renderCustomChartToggleActions={renderCustomChartToggleActions}
|
||||
>
|
||||
<DiscoverMainContent
|
||||
{...mainContentProps}
|
||||
stateContainer={stateContainer}
|
||||
dataView={dataView}
|
||||
isPlainRecord={isPlainRecord}
|
||||
// The documents grid doesn't rerender when the chart visibility changes
|
||||
// which causes it to render blank space, so we need to force a rerender
|
||||
key={`docKey${hideChart}`}
|
||||
panelsToggle={panelsToggle}
|
||||
/>
|
||||
</UnifiedHistogramContainer>
|
||||
);
|
||||
|
|
|
@ -58,6 +58,10 @@ discover-app {
|
|||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.dscPageContent__panelsToggleWhenNoResults {
|
||||
padding: $euiSizeS;
|
||||
}
|
||||
|
||||
.dscTable {
|
||||
// needs for scroll container of lagacy table
|
||||
min-height: 0;
|
||||
|
|
|
@ -39,15 +39,13 @@ import { getSessionServiceMock } from '@kbn/data-plugin/public/search/session/mo
|
|||
import { DiscoverMainProvider } from '../../services/discover_state_provider';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { ErrorCallout } from '../../../../components/common/error_callout';
|
||||
import * as localStorageModule from 'react-use/lib/useLocalStorage';
|
||||
import { PanelsToggle } from '../../../../components/panels_toggle';
|
||||
|
||||
jest.mock('@elastic/eui', () => ({
|
||||
...jest.requireActual('@elastic/eui'),
|
||||
useResizeObserver: jest.fn(() => ({ width: 1000, height: 1000 })),
|
||||
}));
|
||||
|
||||
jest.spyOn(localStorageModule, 'default');
|
||||
|
||||
setHeaderActionMenuMounter(jest.fn());
|
||||
|
||||
async function mountComponent(
|
||||
|
@ -62,7 +60,7 @@ async function mountComponent(
|
|||
foundDocuments: true,
|
||||
}) as DataMain$
|
||||
) {
|
||||
const searchSourceMock = createSearchSourceMock({});
|
||||
const searchSourceMock = createSearchSourceMock({ index: dataView });
|
||||
const services = createDiscoverServicesMock();
|
||||
const time = { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' };
|
||||
services.data.query.timefilter.timefilter.getTime = () => time;
|
||||
|
@ -78,9 +76,10 @@ async function mountComponent(
|
|||
(searchSourceInstanceMock.fetch$ as jest.Mock).mockImplementation(
|
||||
jest.fn().mockReturnValue(of({ rawResponse: { hits: { total: 2 } } }))
|
||||
);
|
||||
(localStorageModule.default as jest.Mock).mockImplementation(
|
||||
jest.fn(() => [prevSidebarClosed, jest.fn()])
|
||||
);
|
||||
|
||||
if (typeof prevSidebarClosed === 'boolean') {
|
||||
localStorage.setItem('discover:sidebarClosed', String(prevSidebarClosed));
|
||||
}
|
||||
|
||||
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
|
||||
|
||||
|
@ -110,7 +109,7 @@ async function mountComponent(
|
|||
|
||||
session.getSession$.mockReturnValue(new BehaviorSubject('123'));
|
||||
|
||||
stateContainer.appState.update({ interval: 'auto', query });
|
||||
stateContainer.appState.update({ index: dataView.id, interval: 'auto', query });
|
||||
stateContainer.internalState.transitions.setDataView(dataView);
|
||||
|
||||
const props = {
|
||||
|
@ -150,17 +149,15 @@ describe('Discover component', () => {
|
|||
test('selected data view without time field displays no chart toggle', async () => {
|
||||
const container = document.createElement('div');
|
||||
await mountComponent(dataViewMock, undefined, { attachTo: container });
|
||||
expect(
|
||||
container.querySelector('[data-test-subj="unifiedHistogramChartOptionsToggle"]')
|
||||
).toBeNull();
|
||||
expect(container.querySelector('[data-test-subj="dscHideHistogramButton"]')).toBeNull();
|
||||
expect(container.querySelector('[data-test-subj="dscShowHistogramButton"]')).toBeNull();
|
||||
}, 10000);
|
||||
|
||||
test('selected data view with time field displays chart toggle', async () => {
|
||||
const container = document.createElement('div');
|
||||
await mountComponent(dataViewWithTimefieldMock, undefined, { attachTo: container });
|
||||
expect(
|
||||
container.querySelector('[data-test-subj="unifiedHistogramChartOptionsToggle"]')
|
||||
).not.toBeNull();
|
||||
expect(container.querySelector('[data-test-subj="dscHideHistogramButton"]')).not.toBeNull();
|
||||
expect(container.querySelector('[data-test-subj="dscShowHistogramButton"]')).toBeNull();
|
||||
}, 10000);
|
||||
|
||||
describe('sidebar', () => {
|
||||
|
@ -195,5 +192,7 @@ describe('Discover component', () => {
|
|||
}) as DataMain$
|
||||
);
|
||||
expect(component.find(ErrorCallout)).toHaveLength(1);
|
||||
expect(component.find(PanelsToggle).prop('isChartAvailable')).toBe(false);
|
||||
expect(component.find(PanelsToggle).prop('renderedFor')).toBe('prompt');
|
||||
}, 10000);
|
||||
});
|
||||
|
|
|
@ -6,17 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
import './discover_layout.scss';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHideFor,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPanel,
|
||||
useEuiBackgroundColor,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState, ReactElement } from 'react';
|
||||
import { EuiPage, EuiPageBody, EuiPanel, useEuiBackgroundColor } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
|
@ -31,7 +22,7 @@ import {
|
|||
} from '@kbn/discover-utils';
|
||||
import { popularizeField, useColumns } from '@kbn/unified-data-table';
|
||||
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
|
||||
import type { UnifiedFieldListSidebarContainerApi } from '@kbn/unified-field-list';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { useSavedSearchInitial } from '../../services/discover_state_provider';
|
||||
import { DiscoverStateContainer } from '../../services/discover_state';
|
||||
import { VIEW_MODE } from '../../../../../common/constants';
|
||||
|
@ -45,7 +36,7 @@ import { DiscoverTopNav } from '../top_nav/discover_topnav';
|
|||
import { getResultState } from '../../utils/get_result_state';
|
||||
import { DiscoverUninitialized } from '../uninitialized/uninitialized';
|
||||
import { DataMainMsg, RecordRawType } from '../../services/discover_data_state_container';
|
||||
import { FetchStatus } from '../../../types';
|
||||
import { FetchStatus, SidebarToggleState } from '../../../types';
|
||||
import { useDataState } from '../../hooks/use_data_state';
|
||||
import { getRawRecordType } from '../../utils/get_raw_record_type';
|
||||
import { SavedSearchURLConflictCallout } from '../../../../components/saved_search_url_conflict_callout/saved_search_url_conflict_callout';
|
||||
|
@ -54,6 +45,7 @@ import { ErrorCallout } from '../../../../components/common/error_callout';
|
|||
import { addLog } from '../../../../utils/add_log';
|
||||
import { DiscoverResizableLayout } from './discover_resizable_layout';
|
||||
import { ESQLTechPreviewCallout } from './esql_tech_preview_callout';
|
||||
import { PanelsToggle, PanelsToggleProps } from '../../../../components/panels_toggle';
|
||||
|
||||
const SidebarMemoized = React.memo(DiscoverSidebarResponsive);
|
||||
const TopNavMemoized = React.memo(DiscoverTopNav);
|
||||
|
@ -75,7 +67,6 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
|
|||
docLinks,
|
||||
serverless,
|
||||
} = useDiscoverServices();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const pageBackgroundColor = useEuiBackgroundColor('plain');
|
||||
const globalQueryState = data.query.getState();
|
||||
const { main$ } = stateContainer.dataState.data$;
|
||||
|
@ -194,6 +185,21 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
|
|||
return () => onAddColumn(draggingFieldName);
|
||||
}, [onAddColumn, draggingFieldName, currentColumns]);
|
||||
|
||||
const [sidebarToggleState$] = useState<BehaviorSubject<SidebarToggleState>>(
|
||||
() => new BehaviorSubject<SidebarToggleState>({ isCollapsed: false, toggle: () => {} })
|
||||
);
|
||||
|
||||
const panelsToggle: ReactElement<PanelsToggleProps> = useMemo(() => {
|
||||
return (
|
||||
<PanelsToggle
|
||||
stateContainer={stateContainer}
|
||||
sidebarToggleState$={sidebarToggleState$}
|
||||
renderedFor="root"
|
||||
isChartAvailable={undefined}
|
||||
/>
|
||||
);
|
||||
}, [stateContainer, sidebarToggleState$]);
|
||||
|
||||
const mainDisplay = useMemo(() => {
|
||||
if (resultState === 'uninitialized') {
|
||||
addLog('[DiscoverLayout] uninitialized triggers data fetching');
|
||||
|
@ -214,6 +220,7 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
|
|||
onFieldEdited={onFieldEdited}
|
||||
container={mainContainer}
|
||||
onDropFieldToTable={onDropFieldToTable}
|
||||
panelsToggle={panelsToggle}
|
||||
/>
|
||||
{resultState === 'loading' && <LoadingSpinner />}
|
||||
</>
|
||||
|
@ -230,11 +237,9 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
|
|||
resultState,
|
||||
stateContainer,
|
||||
viewMode,
|
||||
panelsToggle,
|
||||
]);
|
||||
|
||||
const [unifiedFieldListSidebarContainerApi, setUnifiedFieldListSidebarContainerApi] =
|
||||
useState<UnifiedFieldListSidebarContainerApi | null>(null);
|
||||
|
||||
return (
|
||||
<EuiPage
|
||||
className={classNames('dscPage', { 'dscPage--serverless': serverless })}
|
||||
|
@ -282,64 +287,56 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
|
|||
/>
|
||||
<DiscoverResizableLayout
|
||||
container={sidebarContainer}
|
||||
unifiedFieldListSidebarContainerApi={unifiedFieldListSidebarContainerApi}
|
||||
sidebarToggleState$={sidebarToggleState$}
|
||||
sidebarPanel={
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
css={css`
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<SidebarMemoized
|
||||
documents$={stateContainer.dataState.data$.documents$}
|
||||
onAddField={onAddColumn}
|
||||
columns={currentColumns}
|
||||
onAddFilter={!isPlainRecord ? onAddFilter : undefined}
|
||||
onRemoveField={onRemoveColumn}
|
||||
onChangeDataView={stateContainer.actions.onChangeDataView}
|
||||
selectedDataView={dataView}
|
||||
trackUiMetric={trackUiMetric}
|
||||
onFieldEdited={onFieldEdited}
|
||||
onDataViewCreated={stateContainer.actions.onDataViewCreated}
|
||||
availableFields$={stateContainer.dataState.data$.availableFields$}
|
||||
unifiedFieldListSidebarContainerApi={unifiedFieldListSidebarContainerApi}
|
||||
setUnifiedFieldListSidebarContainerApi={setUnifiedFieldListSidebarContainerApi}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiHideFor sizes={['xs', 's']}>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
css={css`
|
||||
border-right: ${euiTheme.border.thin};
|
||||
`}
|
||||
/>
|
||||
</EuiHideFor>
|
||||
</EuiFlexGroup>
|
||||
<SidebarMemoized
|
||||
documents$={stateContainer.dataState.data$.documents$}
|
||||
onAddField={onAddColumn}
|
||||
columns={currentColumns}
|
||||
onAddFilter={!isPlainRecord ? onAddFilter : undefined}
|
||||
onRemoveField={onRemoveColumn}
|
||||
onChangeDataView={stateContainer.actions.onChangeDataView}
|
||||
selectedDataView={dataView}
|
||||
trackUiMetric={trackUiMetric}
|
||||
onFieldEdited={onFieldEdited}
|
||||
onDataViewCreated={stateContainer.actions.onDataViewCreated}
|
||||
availableFields$={stateContainer.dataState.data$.availableFields$}
|
||||
sidebarToggleState$={sidebarToggleState$}
|
||||
/>
|
||||
}
|
||||
mainPanel={
|
||||
<div className="dscPageContent__wrapper">
|
||||
{resultState === 'none' ? (
|
||||
dataState.error ? (
|
||||
<ErrorCallout
|
||||
title={i18n.translate(
|
||||
'discover.noResults.searchExamples.noResultsErrorTitle',
|
||||
{
|
||||
defaultMessage: 'Unable to retrieve search results',
|
||||
}
|
||||
)}
|
||||
error={dataState.error}
|
||||
/>
|
||||
) : (
|
||||
<DiscoverNoResults
|
||||
stateContainer={stateContainer}
|
||||
isTimeBased={isTimeBased}
|
||||
query={globalQueryState.query}
|
||||
filters={globalQueryState.filters}
|
||||
dataView={dataView}
|
||||
onDisableFilters={onDisableFilters}
|
||||
/>
|
||||
)
|
||||
<>
|
||||
{React.isValidElement(panelsToggle) ? (
|
||||
<div className="dscPageContent__panelsToggleWhenNoResults">
|
||||
{React.cloneElement(panelsToggle, {
|
||||
renderedFor: 'prompt',
|
||||
isChartAvailable: false,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{dataState.error ? (
|
||||
<ErrorCallout
|
||||
title={i18n.translate(
|
||||
'discover.noResults.searchExamples.noResultsErrorTitle',
|
||||
{
|
||||
defaultMessage: 'Unable to retrieve search results',
|
||||
}
|
||||
)}
|
||||
error={dataState.error}
|
||||
/>
|
||||
) : (
|
||||
<DiscoverNoResults
|
||||
stateContainer={stateContainer}
|
||||
isTimeBased={isTimeBased}
|
||||
query={globalQueryState.query}
|
||||
filters={globalQueryState.filters}
|
||||
dataView={dataView}
|
||||
onDisableFilters={onDisableFilters}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<EuiPanel
|
||||
role="main"
|
||||
|
|
|
@ -8,8 +8,10 @@
|
|||
|
||||
import React from 'react';
|
||||
import { BehaviorSubject, of } from 'rxjs';
|
||||
import { EuiHorizontalRule } from '@elastic/eui';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { DataView } from '@kbn/data-plugin/common';
|
||||
import { dataViewMock, esHitsMock } from '@kbn/discover-utils/src/__mocks__';
|
||||
import {
|
||||
AvailableFields$,
|
||||
|
@ -19,7 +21,7 @@ import {
|
|||
RecordRawType,
|
||||
} from '../../services/discover_data_state_container';
|
||||
import { createDiscoverServicesMock } from '../../../../__mocks__/services';
|
||||
import { FetchStatus } from '../../../types';
|
||||
import { FetchStatus, SidebarToggleState } from '../../../types';
|
||||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { buildDataTableRecord } from '@kbn/discover-utils';
|
||||
|
@ -31,16 +33,19 @@ import { DiscoverDocuments } from './discover_documents';
|
|||
import { FieldStatisticsTab } from '../field_stats_table';
|
||||
import { DiscoverMainProvider } from '../../services/discover_state_provider';
|
||||
import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock';
|
||||
import { PanelsToggle } from '../../../../components/panels_toggle';
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
|
||||
const mountComponent = async ({
|
||||
hideChart = false,
|
||||
isPlainRecord = false,
|
||||
isChartAvailable,
|
||||
viewMode = VIEW_MODE.DOCUMENT_LEVEL,
|
||||
storage,
|
||||
}: {
|
||||
hideChart?: boolean;
|
||||
isPlainRecord?: boolean;
|
||||
isChartAvailable?: boolean;
|
||||
viewMode?: VIEW_MODE;
|
||||
storage?: Storage;
|
||||
savedSearch?: SavedSearch;
|
||||
|
@ -80,15 +85,19 @@ const mountComponent = async ({
|
|||
result: Number(esHitsMock.length),
|
||||
}) as DataTotalHits$;
|
||||
|
||||
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
|
||||
const savedSearchData$ = {
|
||||
main$,
|
||||
documents$,
|
||||
totalHits$,
|
||||
availableFields$,
|
||||
};
|
||||
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
|
||||
stateContainer.dataState.data$ = savedSearchData$;
|
||||
const dataView = stateContainer.savedSearchState
|
||||
.getState()
|
||||
.searchSource.getField('index') as DataView;
|
||||
stateContainer.appState.update({
|
||||
index: dataView?.id!,
|
||||
interval: 'auto',
|
||||
hideChart,
|
||||
columns: [],
|
||||
|
@ -96,12 +105,26 @@ const mountComponent = async ({
|
|||
|
||||
const props: DiscoverMainContentProps = {
|
||||
isPlainRecord,
|
||||
dataView: dataViewMock,
|
||||
dataView,
|
||||
stateContainer,
|
||||
onFieldEdited: jest.fn(),
|
||||
columns: [],
|
||||
viewMode,
|
||||
onAddFilter: jest.fn(),
|
||||
isChartAvailable,
|
||||
panelsToggle: (
|
||||
<PanelsToggle
|
||||
stateContainer={stateContainer}
|
||||
sidebarToggleState$={
|
||||
new BehaviorSubject<SidebarToggleState>({
|
||||
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', () => {
|
||||
|
|
|
@ -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<void>;
|
||||
onDropFieldToTable?: () => void;
|
||||
columns: string[];
|
||||
panelsToggle: ReactElement<PanelsToggleProps>;
|
||||
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 ? (
|
||||
<DocumentViewModeToggle viewMode={viewMode} setDiscoverViewMode={setDiscoverViewMode} />
|
||||
) : undefined;
|
||||
}, [viewMode, setDiscoverViewMode, isPlainRecord]);
|
||||
return (
|
||||
<DocumentViewModeToggle
|
||||
viewMode={viewMode}
|
||||
isTextBasedQuery={isPlainRecord}
|
||||
stateContainer={stateContainer}
|
||||
setDiscoverViewMode={setDiscoverViewMode}
|
||||
prepend={
|
||||
React.isValidElement(panelsToggle)
|
||||
? React.cloneElement(panelsToggle, { renderedFor: 'tabs', isChartAvailable })
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
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 && <EuiHorizontalRule margin="none" />}
|
||||
{showChart && isChartAvailable && <EuiHorizontalRule margin="none" />}
|
||||
{viewMode === VIEW_MODE.DOCUMENT_LEVEL ? (
|
||||
<DiscoverDocuments
|
||||
viewModeToggle={viewModeToggle}
|
||||
|
|
|
@ -12,12 +12,12 @@ import {
|
|||
ResizableLayoutMode,
|
||||
} from '@kbn/resizable-layout';
|
||||
import { findTestSubject } from '@kbn/test-jest-helpers';
|
||||
import type { UnifiedFieldListSidebarContainerApi } from '@kbn/unified-field-list';
|
||||
import { mount } from 'enzyme';
|
||||
import { isEqual as mockIsEqual } from 'lodash';
|
||||
import React from 'react';
|
||||
import { of } from 'rxjs';
|
||||
import { DiscoverResizableLayout, SIDEBAR_WIDTH_KEY } from './discover_resizable_layout';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { SidebarToggleState } from '../../../types';
|
||||
|
||||
const mockSidebarKey = SIDEBAR_WIDTH_KEY;
|
||||
let mockSidebarWidth: number | undefined;
|
||||
|
@ -56,7 +56,12 @@ describe('DiscoverResizableLayout', () => {
|
|||
const wrapper = mount(
|
||||
<DiscoverResizableLayout
|
||||
container={null}
|
||||
unifiedFieldListSidebarContainerApi={null}
|
||||
sidebarToggleState$={
|
||||
new BehaviorSubject<SidebarToggleState>({
|
||||
isCollapsed: true,
|
||||
toggle: () => {},
|
||||
})
|
||||
}
|
||||
sidebarPanel={<div data-test-subj="sidebarPanel" />}
|
||||
mainPanel={<div data-test-subj="mainPanel" />}
|
||||
/>
|
||||
|
@ -69,7 +74,12 @@ describe('DiscoverResizableLayout', () => {
|
|||
const wrapper = mount(
|
||||
<DiscoverResizableLayout
|
||||
container={null}
|
||||
unifiedFieldListSidebarContainerApi={null}
|
||||
sidebarToggleState$={
|
||||
new BehaviorSubject<SidebarToggleState>({
|
||||
isCollapsed: true,
|
||||
toggle: () => {},
|
||||
})
|
||||
}
|
||||
sidebarPanel={<div data-test-subj="sidebarPanel" />}
|
||||
mainPanel={<div data-test-subj="mainPanel" />}
|
||||
/>
|
||||
|
@ -82,7 +92,12 @@ describe('DiscoverResizableLayout', () => {
|
|||
const wrapper = mount(
|
||||
<DiscoverResizableLayout
|
||||
container={null}
|
||||
unifiedFieldListSidebarContainerApi={null}
|
||||
sidebarToggleState$={
|
||||
new BehaviorSubject<SidebarToggleState>({
|
||||
isCollapsed: true,
|
||||
toggle: () => {},
|
||||
})
|
||||
}
|
||||
sidebarPanel={<div data-test-subj="sidebarPanel" />}
|
||||
mainPanel={<div data-test-subj="mainPanel" />}
|
||||
/>
|
||||
|
@ -95,8 +110,11 @@ describe('DiscoverResizableLayout', () => {
|
|||
const wrapper = mount(
|
||||
<DiscoverResizableLayout
|
||||
container={null}
|
||||
unifiedFieldListSidebarContainerApi={
|
||||
{ isSidebarCollapsed$: of(false) } as UnifiedFieldListSidebarContainerApi
|
||||
sidebarToggleState$={
|
||||
new BehaviorSubject<SidebarToggleState>({
|
||||
isCollapsed: false,
|
||||
toggle: () => {},
|
||||
})
|
||||
}
|
||||
sidebarPanel={<div data-test-subj="sidebarPanel" />}
|
||||
mainPanel={<div data-test-subj="mainPanel" />}
|
||||
|
@ -110,8 +128,11 @@ describe('DiscoverResizableLayout', () => {
|
|||
const wrapper = mount(
|
||||
<DiscoverResizableLayout
|
||||
container={null}
|
||||
unifiedFieldListSidebarContainerApi={
|
||||
{ isSidebarCollapsed$: of(false) } as UnifiedFieldListSidebarContainerApi
|
||||
sidebarToggleState$={
|
||||
new BehaviorSubject<SidebarToggleState>({
|
||||
isCollapsed: false,
|
||||
toggle: () => {},
|
||||
})
|
||||
}
|
||||
sidebarPanel={<div data-test-subj="sidebarPanel" />}
|
||||
mainPanel={<div data-test-subj="mainPanel" />}
|
||||
|
@ -125,8 +146,11 @@ describe('DiscoverResizableLayout', () => {
|
|||
const wrapper = mount(
|
||||
<DiscoverResizableLayout
|
||||
container={null}
|
||||
unifiedFieldListSidebarContainerApi={
|
||||
{ isSidebarCollapsed$: of(true) } as UnifiedFieldListSidebarContainerApi
|
||||
sidebarToggleState$={
|
||||
new BehaviorSubject<SidebarToggleState>({
|
||||
isCollapsed: true,
|
||||
toggle: () => {},
|
||||
})
|
||||
}
|
||||
sidebarPanel={<div data-test-subj="sidebarPanel" />}
|
||||
mainPanel={<div data-test-subj="mainPanel" />}
|
||||
|
@ -140,8 +164,11 @@ describe('DiscoverResizableLayout', () => {
|
|||
const wrapper = mount(
|
||||
<DiscoverResizableLayout
|
||||
container={null}
|
||||
unifiedFieldListSidebarContainerApi={
|
||||
{ isSidebarCollapsed$: of(false) } as UnifiedFieldListSidebarContainerApi
|
||||
sidebarToggleState$={
|
||||
new BehaviorSubject<SidebarToggleState>({
|
||||
isCollapsed: false,
|
||||
toggle: () => {},
|
||||
})
|
||||
}
|
||||
sidebarPanel={<div data-test-subj="sidebarPanel" />}
|
||||
mainPanel={<div data-test-subj="mainPanel" />}
|
||||
|
@ -157,8 +184,11 @@ describe('DiscoverResizableLayout', () => {
|
|||
const wrapper = mount(
|
||||
<DiscoverResizableLayout
|
||||
container={null}
|
||||
unifiedFieldListSidebarContainerApi={
|
||||
{ isSidebarCollapsed$: of(false) } as UnifiedFieldListSidebarContainerApi
|
||||
sidebarToggleState$={
|
||||
new BehaviorSubject<SidebarToggleState>({
|
||||
isCollapsed: false,
|
||||
toggle: () => {},
|
||||
})
|
||||
}
|
||||
sidebarPanel={<div data-test-subj="sidebarPanel" />}
|
||||
mainPanel={<div data-test-subj="mainPanel" />}
|
||||
|
|
|
@ -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<SidebarToggleState>;
|
||||
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 =
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<SidebarToggleState>({
|
||||
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<UnifiedFieldListSidebarContainerApi | null>(null);
|
||||
return (
|
||||
<DiscoverSidebarResponsive
|
||||
{...props}
|
||||
unifiedFieldListSidebarContainerApi={api}
|
||||
setUnifiedFieldListSidebarContainerApi={setApi}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
comp = mountWithIntl(
|
||||
<KibanaContextProvider services={mockedServices}>
|
||||
<DiscoverAppStateProvider value={appState}>
|
||||
<SidebarWrapper />
|
||||
<DiscoverSidebarResponsive {...props} />
|
||||
</DiscoverAppStateProvider>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
|
|
@ -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<SidebarToggleState>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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<UnifiedFieldListSidebarContainerApi | null>(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 (
|
||||
<UnifiedFieldListSidebarContainer
|
||||
ref={initializeUnifiedFieldListSidebarContainerApi}
|
||||
variant={fieldListVariant}
|
||||
getCreationOptions={getCreationOptions}
|
||||
services={fieldListSidebarServices}
|
||||
dataView={selectedDataView}
|
||||
trackUiMetric={trackUiMetric}
|
||||
allFields={sidebarState.allFields}
|
||||
showFieldList={showFieldList}
|
||||
workspaceSelectedFieldNames={columns}
|
||||
fullWidth
|
||||
onAddFieldToWorkspace={onAddFieldToWorkspace}
|
||||
onRemoveFieldFromWorkspace={onRemoveFieldFromWorkspace}
|
||||
onAddFilter={onAddFilter}
|
||||
onFieldEdited={onFieldEdited}
|
||||
prependInFlyout={prependDataViewPickerForMobile}
|
||||
/>
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
css={css`
|
||||
height: 100%;
|
||||
display: ${isSidebarCollapsed ? 'none' : 'flex'};
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
{selectedDataView ? (
|
||||
<UnifiedFieldListSidebarContainer
|
||||
ref={initializeUnifiedFieldListSidebarContainerApi}
|
||||
variant={fieldListVariant}
|
||||
getCreationOptions={getCreationOptions}
|
||||
services={fieldListSidebarServices}
|
||||
dataView={selectedDataView}
|
||||
trackUiMetric={trackUiMetric}
|
||||
allFields={sidebarState.allFields}
|
||||
showFieldList={showFieldList}
|
||||
workspaceSelectedFieldNames={columns}
|
||||
fullWidth
|
||||
onAddFieldToWorkspace={onAddFieldToWorkspace}
|
||||
onRemoveFieldFromWorkspace={onRemoveFieldFromWorkspace}
|
||||
onAddFilter={onAddFilter}
|
||||
onFieldEdited={onFieldEdited}
|
||||
prependInFlyout={prependDataViewPickerForMobile}
|
||||
/>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
<EuiHideFor sizes={['xs', 's']}>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
css={css`
|
||||
border-right: ${euiTheme.border.thin};
|
||||
`}
|
||||
/>
|
||||
</EuiHideFor>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -38,3 +38,8 @@ export interface RecordsFetchResponse {
|
|||
textBasedHeaderWarning?: string;
|
||||
interceptedWarnings?: SearchResponseWarning[];
|
||||
}
|
||||
|
||||
export interface SidebarToggleState {
|
||||
isCollapsed: boolean;
|
||||
toggle: undefined | ((isCollapsed: boolean) => void);
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
<HitsCounter mode={HitsCounterMode.appended} stateContainer={stateContainer} />
|
||||
);
|
||||
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(
|
||||
<HitsCounter mode={HitsCounterMode.standalone} stateContainer={stateContainer} />
|
||||
);
|
||||
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(
|
||||
<HitsCounter mode={HitsCounterMode.appended} stateContainer={stateContainer} />
|
||||
);
|
||||
expect(findTestSubject(component1, 'discoverQueryHits').text()).toBe('1,899');
|
||||
expect(findTestSubject(component1, 'discoverQueryTotalHits').text()).toBe('1,899');
|
||||
|
||||
const component2 = mountWithIntl(
|
||||
<HitsCounter mode={HitsCounterMode.standalone} stateContainer={stateContainer} />
|
||||
);
|
||||
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(
|
||||
<HitsCounter mode={HitsCounterMode.standalone} stateContainer={stateContainer} />
|
||||
);
|
||||
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(
|
||||
<HitsCounter mode={HitsCounterMode.standalone} stateContainer={stateContainer} />
|
||||
);
|
||||
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(
|
||||
<HitsCounter mode={HitsCounterMode.standalone} stateContainer={stateContainer} />
|
||||
);
|
||||
expect(component.isEmptyRender()).toBe(true);
|
||||
});
|
||||
});
|
|
@ -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<HitsCounterProps> = ({ 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 = (
|
||||
<span
|
||||
data-test-subj={
|
||||
hitsStatus === FetchStatus.PARTIAL ? 'discoverQueryHitsPartial' : 'discoverQueryHits'
|
||||
}
|
||||
>
|
||||
<FormattedNumber value={hitsTotal ?? 0} />
|
||||
</span>
|
||||
);
|
||||
|
||||
const hitsCounterCss = css`
|
||||
display: inline-flex;
|
||||
`;
|
||||
const hitsCounterTextCss = css`
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const element = (
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
className="eui-textTruncate eui-textNoWrap"
|
||||
css={hitsCounterCss}
|
||||
data-test-subj="discoverQueryTotalHits"
|
||||
>
|
||||
<EuiFlexItem grow={false} aria-live="polite" css={hitsCounterTextCss}>
|
||||
<EuiText className="eui-textTruncate" size="s">
|
||||
<strong>
|
||||
{hitsStatus === FetchStatus.PARTIAL &&
|
||||
(mode === HitsCounterMode.standalone ? (
|
||||
<FormattedMessage
|
||||
id="discover.hitsCounter.partialHitsPluralTitle"
|
||||
defaultMessage="≥{formattedHits} {hits, plural, one {result} other {results}}"
|
||||
values={{ hits: hitsTotal, formattedHits }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="discover.hitsCounter.partialHits"
|
||||
defaultMessage="≥{formattedHits}"
|
||||
values={{ formattedHits }}
|
||||
/>
|
||||
))}
|
||||
{hitsStatus !== FetchStatus.PARTIAL &&
|
||||
(mode === HitsCounterMode.standalone ? (
|
||||
<FormattedMessage
|
||||
id="discover.hitsCounter.hitsPluralTitle"
|
||||
defaultMessage="{formattedHits} {hits, plural, one {result} other {results}}"
|
||||
values={{ hits: hitsTotal, formattedHits }}
|
||||
/>
|
||||
) : (
|
||||
formattedHits
|
||||
))}
|
||||
</strong>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{hitsStatus === FetchStatus.PARTIAL && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner
|
||||
size="m"
|
||||
aria-label={i18n.translate('discover.hitsCounter.hitCountSpinnerAriaLabel', {
|
||||
defaultMessage: 'Final hit count still loading',
|
||||
})}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
return mode === HitsCounterMode.appended ? (
|
||||
<>
|
||||
{' ('}
|
||||
{element}
|
||||
{')'}
|
||||
</>
|
||||
) : (
|
||||
element
|
||||
);
|
||||
};
|
|
@ -6,4 +6,4 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { HitsCounter } from './hits_counter';
|
||||
export { HitsCounter, HitsCounterMode } from './hits_counter';
|
|
@ -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';
|
|
@ -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<PanelsToggleProps, 'stateContainer'> & { hideChart: boolean }) => {
|
||||
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
|
||||
const appStateContainer = stateContainer.appState;
|
||||
appStateContainer.set({
|
||||
hideChart,
|
||||
});
|
||||
|
||||
return mountWithIntl(
|
||||
<DiscoverAppStateProvider value={appStateContainer}>
|
||||
<PanelsToggle
|
||||
stateContainer={stateContainer}
|
||||
sidebarToggleState$={sidebarToggleState$}
|
||||
isChartAvailable={isChartAvailable}
|
||||
renderedFor={renderedFor}
|
||||
/>
|
||||
</DiscoverAppStateProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('inside histogram toolbar', function () {
|
||||
it('should render correctly when sidebar is visible and histogram is visible', () => {
|
||||
const sidebarToggleState$ = new BehaviorSubject<SidebarToggleState>({
|
||||
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<SidebarToggleState>({
|
||||
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<SidebarToggleState>({
|
||||
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<SidebarToggleState>({
|
||||
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<SidebarToggleState>({
|
||||
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<SidebarToggleState>({
|
||||
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<SidebarToggleState>({
|
||||
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<SidebarToggleState>({
|
||||
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<SidebarToggleState>({
|
||||
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<SidebarToggleState>({
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<SidebarToggleState>;
|
||||
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<PanelsToggleProps> = ({
|
||||
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 (
|
||||
<IconButtonGroup
|
||||
data-test-subj={`dscPanelsToggle${isInsideHistogram ? 'InHistogram' : 'InPage'}`}
|
||||
legend={i18n.translate('discover.panelsToggle.panelsVisibilityLegend', {
|
||||
defaultMessage: 'Panels visibility',
|
||||
})}
|
||||
buttonSize="s"
|
||||
buttons={buttons}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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(
|
||||
<KibanaContextProvider services={serivces}>
|
||||
<DocumentViewModeToggle viewMode={viewMode} setDiscoverViewMode={setDiscoverViewMode} />
|
||||
<DocumentViewModeToggle
|
||||
viewMode={viewMode}
|
||||
isTextBasedQuery={isTextBasedQuery}
|
||||
stateContainer={stateContainer}
|
||||
setDiscoverViewMode={setDiscoverViewMode}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
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', () => {
|
||||
|
|
|
@ -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 (
|
||||
<EuiTabs size="m" css={tabsCss} data-test-subj="dscViewModeToggle" bottomBorder={false}>
|
||||
<EuiTab
|
||||
isSelected={viewMode === VIEW_MODE.DOCUMENT_LEVEL}
|
||||
onClick={() => setDiscoverViewMode(VIEW_MODE.DOCUMENT_LEVEL)}
|
||||
data-test-subj="dscViewModeDocumentButton"
|
||||
>
|
||||
<FormattedMessage id="discover.viewModes.document.label" defaultMessage="Documents" />
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
isSelected={viewMode === VIEW_MODE.AGGREGATED_LEVEL}
|
||||
onClick={() => setDiscoverViewMode(VIEW_MODE.AGGREGATED_LEVEL)}
|
||||
data-test-subj="dscViewModeFieldStatsButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="discover.viewModes.fieldStatistics.label"
|
||||
defaultMessage="Field statistics"
|
||||
/>
|
||||
</EuiTab>
|
||||
</EuiTabs>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
responsive={false}
|
||||
css={containerCss}
|
||||
>
|
||||
{prepend && (
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
css={css`
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
`}
|
||||
>
|
||||
{prepend}
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
{isTextBasedQuery || !showViewModeToggle ? (
|
||||
<HitsCounter mode={HitsCounterMode.standalone} stateContainer={stateContainer} />
|
||||
) : (
|
||||
<EuiTabs size="m" css={tabsCss} data-test-subj="dscViewModeToggle" bottomBorder={false}>
|
||||
<EuiTab
|
||||
isSelected={viewMode === VIEW_MODE.DOCUMENT_LEVEL}
|
||||
onClick={() => setDiscoverViewMode(VIEW_MODE.DOCUMENT_LEVEL)}
|
||||
data-test-subj="dscViewModeDocumentButton"
|
||||
>
|
||||
<FormattedMessage id="discover.viewModes.document.label" defaultMessage="Documents" />
|
||||
<HitsCounter mode={HitsCounterMode.appended} stateContainer={stateContainer} />
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
isSelected={viewMode === VIEW_MODE.AGGREGATED_LEVEL}
|
||||
onClick={() => setDiscoverViewMode(VIEW_MODE.AGGREGATED_LEVEL)}
|
||||
data-test-subj="dscViewModeFieldStatsButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="discover.viewModes.fieldStatistics.label"
|
||||
defaultMessage="Field statistics"
|
||||
/>
|
||||
</EuiTab>
|
||||
</EuiTabs>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
],
|
||||
|
|
|
@ -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={<MyButton />}
|
||||
>
|
||||
<MyLayout />
|
||||
</UnifiedHistogramContainer>
|
||||
|
@ -165,7 +162,6 @@ return (
|
|||
searchSessionId={searchSessionId}
|
||||
requestAdapter={requestAdapter}
|
||||
resizeRef={resizeRef}
|
||||
appendHitsCounter={<MyButton />}
|
||||
>
|
||||
<MyLayout />
|
||||
</UnifiedHistogramContainer>
|
||||
|
|
|
@ -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(
|
||||
<BreakdownFieldSelector
|
||||
dataView={dataViewWithTimefieldMock}
|
||||
breakdown={breakdown}
|
||||
onBreakdownFieldChange={onBreakdownFieldChange}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<BreakdownFieldSelector
|
||||
dataView={dataViewWithTimefieldMock}
|
||||
breakdown={breakdown}
|
||||
onBreakdownFieldChange={onBreakdownFieldChange}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<BreakdownFieldSelector
|
||||
dataView={dataViewWithTimefieldMock}
|
||||
breakdown={breakdown}
|
||||
onBreakdownFieldChange={onBreakdownFieldChange}
|
||||
/>
|
||||
);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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: (
|
||||
<span
|
||||
css={css`
|
||||
.euiToken {
|
||||
vertical-align: middle;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<FieldIcon {...getFieldIconProps(field)} />
|
||||
</span>
|
||||
),
|
||||
}))
|
||||
.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)
|
||||
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 (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={fieldPopoverDisabled ? undefined : breakdown.field?.displayName}
|
||||
anchorProps={{ css: breakdownCss }}
|
||||
>
|
||||
<EuiComboBox
|
||||
data-test-subj="unifiedHistogramBreakdownFieldSelector"
|
||||
prepend={i18n.translate('unifiedHistogram.breakdownFieldSelectorLabel', {
|
||||
defaultMessage: 'Break down by',
|
||||
})}
|
||||
placeholder={i18n.translate('unifiedHistogram.breakdownFieldSelectorPlaceholder', {
|
||||
defaultMessage: 'Select field',
|
||||
})}
|
||||
aria-label={i18n.translate('unifiedHistogram.breakdownFieldSelectorAriaLabel', {
|
||||
defaultMessage: 'Break down by',
|
||||
})}
|
||||
inputPopoverProps={{ panelMinWidth, anchorPosition: 'downRight' }}
|
||||
singleSelection={SINGLE_SELECTION}
|
||||
options={fieldOptions}
|
||||
selectedOptions={selectedFields}
|
||||
onChange={onFieldChange}
|
||||
truncationProps={TRUNCATION_PROPS}
|
||||
compressed
|
||||
fullWidth={true}
|
||||
onFocus={disableFieldPopover}
|
||||
onBlur={enableFieldPopover}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
<ToolbarSelector
|
||||
data-test-subj="unifiedHistogramBreakdownSelector"
|
||||
data-selected-value={breakdown?.field?.name}
|
||||
searchable
|
||||
buttonLabel={
|
||||
breakdown?.field?.displayName
|
||||
? i18n.translate('unifiedHistogram.breakdownFieldSelector.breakdownByButtonLabel', {
|
||||
defaultMessage: 'Breakdown by {fieldName}',
|
||||
values: {
|
||||
fieldName: breakdown?.field?.displayName,
|
||||
},
|
||||
})
|
||||
: i18n.translate('unifiedHistogram.breakdownFieldSelector.noBreakdownButtonLabel', {
|
||||
defaultMessage: 'No breakdown',
|
||||
})
|
||||
}
|
||||
popoverTitle={i18n.translate(
|
||||
'unifiedHistogram.breakdownFieldSelector.breakdownFieldPopoverTitle',
|
||||
{
|
||||
defaultMessage: 'Select breakdown field',
|
||||
}
|
||||
)}
|
||||
options={fieldOptions}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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: <span data-test-subj="custom-toggle" />,
|
||||
});
|
||||
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: <span />, 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 = <div data-test-subj="appendHistogram" />;
|
||||
const component = await mountComponent({ appendHistogram });
|
||||
|
|
|
@ -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<UnifiedHistogramInputMessage>(),
|
||||
|
@ -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 <div {...a11yCommonProps} data-test-subj="unifiedHistogramChartPanelHidden" />;
|
||||
}
|
||||
|
||||
const LensSaveModalComponent = services.lens.SaveModalComponent;
|
||||
const canSaveVisualization =
|
||||
chartVisible && currentSuggestion && services.capabilities.dashboard?.showWriteControls;
|
||||
|
||||
const renderEditButton = useMemo(
|
||||
() => (
|
||||
<EuiButtonIcon
|
||||
size="xs"
|
||||
iconType="pencil"
|
||||
onClick={() => setIsFlyoutVisible(true)}
|
||||
data-test-subj="unifiedHistogramEditFlyoutVisualization"
|
||||
aria-label={i18n.translate('unifiedHistogram.editVisualizationButton', {
|
||||
defaultMessage: 'Edit visualization',
|
||||
})}
|
||||
disabled={isFlyoutVisible}
|
||||
/>
|
||||
),
|
||||
[isFlyoutVisible]
|
||||
);
|
||||
|
||||
const canEditVisualizationOnTheFly = currentSuggestion && chartVisible;
|
||||
|
||||
const actions: IconButtonGroupProps['buttons'] = [];
|
||||
|
||||
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 (
|
||||
<EuiFlexGroup
|
||||
{...a11yCommonProps}
|
||||
className={className}
|
||||
direction="column"
|
||||
alignItems="stretch"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false} css={resultCountCss}>
|
||||
<EuiFlexItem grow={false} css={chartToolbarCss}>
|
||||
<EuiFlexGroup
|
||||
justifyContent="spaceBetween"
|
||||
alignItems="center"
|
||||
gutterSize="none"
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
css={resultCountInnerCss}
|
||||
alignItems="center"
|
||||
justifyContent="spaceBetween"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
className="eui-textTruncate eui-textNoWrap"
|
||||
css={resultCountTitleCss}
|
||||
>
|
||||
{hits && <HitsCounter hits={hits} append={appendHitsCounter} />}
|
||||
</EuiFlexItem>
|
||||
{chart && (
|
||||
<EuiFlexItem css={resultCountToggleCss}>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
justifyContent="flexEnd"
|
||||
css={breakdownFieldSelectorGroupCss}
|
||||
>
|
||||
{chartVisible && breakdown && (
|
||||
<EuiFlexItem css={breakdownFieldSelectorItemCss}>
|
||||
<EuiFlexItem grow={false} css={{ minWidth: 0 }}>
|
||||
<EuiFlexGroup direction="row" gutterSize="s" responsive={false} alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
{renderCustomChartToggleActions ? (
|
||||
renderCustomChartToggleActions()
|
||||
) : (
|
||||
<IconButtonGroup
|
||||
legend={i18n.translate('unifiedHistogram.hideChartButtongroupLegend', {
|
||||
defaultMessage: 'Chart visibility',
|
||||
})}
|
||||
buttonSize="s"
|
||||
buttons={[
|
||||
{
|
||||
label: chartVisible
|
||||
? i18n.translate('unifiedHistogram.hideChartButton', {
|
||||
defaultMessage: 'Hide chart',
|
||||
})
|
||||
: i18n.translate('unifiedHistogram.showChartButton', {
|
||||
defaultMessage: 'Show chart',
|
||||
}),
|
||||
iconType: chartVisible ? 'transitionTopOut' : 'transitionTopIn',
|
||||
'data-test-subj': 'unifiedHistogramToggleChartButton',
|
||||
onClick: toggleHideChart,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
{chartVisible && !isPlainRecord && !!onTimeIntervalChange && (
|
||||
<EuiFlexItem grow={false} css={{ minWidth: 0 }}>
|
||||
<TimeIntervalSelector chart={chart} onTimeIntervalChange={onTimeIntervalChange} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false} css={{ minWidth: 0 }}>
|
||||
<div>
|
||||
{chartVisible && breakdown && (
|
||||
<BreakdownFieldSelector
|
||||
dataView={dataView}
|
||||
breakdown={breakdown}
|
||||
onBreakdownFieldChange={onBreakdownFieldChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{chartVisible && currentSuggestion && allSuggestions && allSuggestions?.length > 1 && (
|
||||
<EuiFlexItem css={suggestionsSelectorItemCss}>
|
||||
<SuggestionSelector
|
||||
suggestions={allSuggestions}
|
||||
activeSuggestion={currentSuggestion}
|
||||
onSuggestionChange={onSuggestionSelectorChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{canSaveVisualization && (
|
||||
<>
|
||||
<EuiFlexItem grow={false} css={chartToolButtonCss}>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('unifiedHistogram.saveVisualizationButton', {
|
||||
defaultMessage: 'Save visualization',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
size="xs"
|
||||
iconType="save"
|
||||
onClick={() => setIsSaveModalVisible(true)}
|
||||
data-test-subj="unifiedHistogramSaveVisualization"
|
||||
aria-label={i18n.translate('unifiedHistogram.saveVisualizationButton', {
|
||||
defaultMessage: 'Save visualization',
|
||||
})}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
{canEditVisualizationOnTheFly && (
|
||||
<EuiFlexItem grow={false} css={chartToolButtonCss}>
|
||||
{!isFlyoutVisible ? (
|
||||
<EuiToolTip
|
||||
content={i18n.translate('unifiedHistogram.editVisualizationButton', {
|
||||
defaultMessage: 'Edit visualization',
|
||||
})}
|
||||
>
|
||||
{renderEditButton}
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
renderEditButton
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{onEditVisualization && (
|
||||
<EuiFlexItem grow={false} css={chartToolButtonCss}>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('unifiedHistogram.editVisualizationButton', {
|
||||
defaultMessage: 'Edit visualization',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
size="xs"
|
||||
iconType="lensApp"
|
||||
onClick={onEditVisualization}
|
||||
data-test-subj="unifiedHistogramEditVisualization"
|
||||
aria-label={i18n.translate('unifiedHistogram.editVisualizationButton', {
|
||||
defaultMessage: 'Edit visualization',
|
||||
})}
|
||||
)}
|
||||
{chartVisible &&
|
||||
currentSuggestion &&
|
||||
allSuggestions &&
|
||||
allSuggestions?.length > 1 && (
|
||||
<SuggestionSelector
|
||||
suggestions={allSuggestions}
|
||||
activeSuggestion={currentSuggestion}
|
||||
onSuggestionChange={onSuggestionSelectorChange}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false} css={chartToolButtonCss}>
|
||||
<EuiPopover
|
||||
id="unifiedHistogramChartOptions"
|
||||
button={
|
||||
<EuiToolTip
|
||||
content={i18n.translate('unifiedHistogram.chartOptionsButton', {
|
||||
defaultMessage: 'Chart options',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
size="xs"
|
||||
iconType="gear"
|
||||
onClick={toggleChartOptions}
|
||||
data-test-subj="unifiedHistogramChartOptionsToggle"
|
||||
aria-label={i18n.translate('unifiedHistogram.chartOptionsButton', {
|
||||
defaultMessage: 'Chart options',
|
||||
})}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
}
|
||||
isOpen={showChartOptionsPopover}
|
||||
closePopover={closeChartOptions}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiContextMenu initialPanelId={0} panels={panels} />
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{chartVisible && actions.length > 0 && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<IconButtonGroup
|
||||
legend={i18n.translate('unifiedHistogram.chartActionsGroupLegend', {
|
||||
defaultMessage: 'Chart actions',
|
||||
})}
|
||||
buttonSize="s"
|
||||
buttons={actions}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
@ -427,6 +362,7 @@ export function Chart({
|
|||
defaultMessage: 'Histogram of found documents',
|
||||
})}
|
||||
css={histogramCss}
|
||||
data-test-subj="unifiedHistogramRendered"
|
||||
>
|
||||
{isChartLoading && (
|
||||
<EuiProgress
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 DataView, DataViewType } from '@kbn/data-views-plugin/common';
|
||||
import { UnifiedHistogramChartContext } from '../types';
|
||||
|
||||
export function checkChartAvailability({
|
||||
chart,
|
||||
dataView,
|
||||
isPlainRecord,
|
||||
}: {
|
||||
chart?: UnifiedHistogramChartContext;
|
||||
dataView: DataView;
|
||||
isPlainRecord?: boolean;
|
||||
}): boolean {
|
||||
return Boolean(
|
||||
chart &&
|
||||
dataView.id &&
|
||||
dataView.type !== DataViewType.ROLLUP &&
|
||||
(isPlainRecord || (!isPlainRecord && dataView.isTimeBased()))
|
||||
);
|
||||
}
|
|
@ -70,8 +70,12 @@ const computeTotalHits = (
|
|||
return Object.values(adapterTables ?? {})?.[0]?.rows?.length;
|
||||
} else if (isPlainRecord && !hasLensSuggestions) {
|
||||
// ES|QL histogram case
|
||||
const rows = Object.values(adapterTables ?? {})?.[0]?.rows;
|
||||
if (!rows) {
|
||||
return undefined;
|
||||
}
|
||||
let rowsCount = 0;
|
||||
Object.values(adapterTables ?? {})?.[0]?.rows.forEach((r) => {
|
||||
rows.forEach((r) => {
|
||||
rowsCount += r.rows;
|
||||
});
|
||||
return rowsCount;
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -7,3 +7,4 @@
|
|||
*/
|
||||
|
||||
export { Chart } from './chart';
|
||||
export { checkChartAvailability } from './check_chart_availability';
|
||||
|
|
|
@ -75,6 +75,8 @@ export const SuggestionSelector = ({
|
|||
position="top"
|
||||
content={suggestionsPopoverDisabled ? undefined : activeSuggestion?.title}
|
||||
anchorProps={{ css: suggestionComboCss }}
|
||||
display="block"
|
||||
delay="long"
|
||||
>
|
||||
<EuiComboBox
|
||||
data-test-subj="unifiedHistogramSuggestionSelector"
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* 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 { render, act, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { TimeIntervalSelector } from './time_interval_selector';
|
||||
|
||||
describe('TimeIntervalSelector', () => {
|
||||
it('should render correctly', () => {
|
||||
const onTimeIntervalChange = jest.fn();
|
||||
|
||||
render(
|
||||
<TimeIntervalSelector
|
||||
chart={{
|
||||
timeInterval: 'auto',
|
||||
}}
|
||||
onTimeIntervalChange={onTimeIntervalChange}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<TimeIntervalSelector
|
||||
chart={{
|
||||
timeInterval: 'y',
|
||||
}}
|
||||
onTimeIntervalChange={onTimeIntervalChange}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<TimeIntervalSelector
|
||||
chart={{
|
||||
timeInterval: 'auto',
|
||||
}}
|
||||
onTimeIntervalChange={onTimeIntervalChange}
|
||||
/>
|
||||
);
|
||||
|
||||
act(() => {
|
||||
screen.getByTestId('unifiedHistogramTimeIntervalSelectorButton').click();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
screen.getByTitle('Week').click();
|
||||
});
|
||||
|
||||
expect(onTimeIntervalChange).toHaveBeenCalledWith('w');
|
||||
});
|
||||
});
|
|
@ -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<TimeIntervalSelectorProps> = ({
|
||||
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 (
|
||||
<ToolbarSelector
|
||||
data-test-subj="unifiedHistogramTimeIntervalSelector"
|
||||
data-selected-value={chart.timeInterval}
|
||||
searchable={false}
|
||||
buttonLabel={
|
||||
chart.timeInterval !== 'auto'
|
||||
? i18n.translate('unifiedHistogram.timeIntervalSelector.buttonLabel', {
|
||||
defaultMessage: `Interval: {timeInterval}`,
|
||||
values: {
|
||||
timeInterval: intervalDisplay.toLowerCase(),
|
||||
},
|
||||
})
|
||||
: i18n.translate('unifiedHistogram.timeIntervalSelector.autoIntervalButtonLabel', {
|
||||
defaultMessage: 'Auto interval',
|
||||
})
|
||||
}
|
||||
popoverTitle={i18n.translate(
|
||||
'unifiedHistogram.timeIntervalSelector.timeIntervalPopoverTitle',
|
||||
{
|
||||
defaultMessage: 'Select time interval',
|
||||
}
|
||||
)}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
};
|
178
src/plugins/unified_histogram/public/chart/toolbar_selector.tsx
Normal file
178
src/plugins/unified_histogram/public/chart/toolbar_selector.tsx
Normal file
|
@ -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<ToolbarSelectorProps> = ({
|
||||
'data-test-subj': dataTestSubj,
|
||||
'data-selected-value': dataSelectedValue,
|
||||
buttonLabel,
|
||||
popoverTitle,
|
||||
options,
|
||||
searchable,
|
||||
onChange,
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [searchTerm, setSearchTerm] = useState<string>();
|
||||
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 (
|
||||
<EuiPopover
|
||||
id={dataTestSubj}
|
||||
ownFocus
|
||||
initialFocus={
|
||||
searchable ? `#${dataTestSubj}SelectableInput` : `#${dataTestSubj}Selectable_listbox`
|
||||
}
|
||||
panelProps={{
|
||||
css: searchable
|
||||
? css`
|
||||
min-width: ${panelMinWidth}px;
|
||||
`
|
||||
: css`
|
||||
width: ${panelMinWidth}px;
|
||||
`,
|
||||
}}
|
||||
panelPaddingSize="none"
|
||||
button={
|
||||
<EuiToolTip content={labelPopoverDisabled ? undefined : buttonLabel} delay="long">
|
||||
<ToolbarButton
|
||||
size="s"
|
||||
css={css`
|
||||
font-weight: ${euiTheme.font.weight.medium};
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: ${euiTheme.base * 20}px;
|
||||
`}
|
||||
data-test-subj={`${dataTestSubj}Button`}
|
||||
data-selected-value={dataSelectedValue}
|
||||
aria-label={popoverTitle}
|
||||
label={buttonLabel}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
onBlur={enableLabelPopover}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
closePopover={() => setIsOpen(false)}
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiPopoverTitle paddingSize="s">{popoverTitle}</EuiPopoverTitle>
|
||||
<EuiSelectable
|
||||
id={`${dataTestSubj}Selectable`}
|
||||
singleSelection
|
||||
aria-label={popoverTitle}
|
||||
data-test-subj={`${dataTestSubj}Selectable`}
|
||||
options={options}
|
||||
onChange={onSelectionChange}
|
||||
listProps={{
|
||||
truncationProps: { truncation: 'middle' },
|
||||
isVirtualized: searchable,
|
||||
}}
|
||||
{...(searchable
|
||||
? {
|
||||
searchable,
|
||||
searchProps,
|
||||
noMatchesMessage: (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="unifiedHistogram.toolbarSelectorPopover.noResults"
|
||||
defaultMessage="No results found for {term}"
|
||||
values={{
|
||||
term: <strong>{searchTerm}</strong>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
),
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
{(list, search) => (
|
||||
<>
|
||||
{search && (
|
||||
<EuiPanel paddingSize="s" hasShadow={false} css={{ paddingBottom: 0 }}>
|
||||
{search}
|
||||
</EuiPanel>
|
||||
)}
|
||||
{list}
|
||||
</>
|
||||
)}
|
||||
</EuiSelectable>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -54,7 +54,7 @@ export type UnifiedHistogramContainerProps = {
|
|||
| 'relativeTimeRange'
|
||||
| 'columns'
|
||||
| 'container'
|
||||
| 'appendHitsCounter'
|
||||
| 'renderCustomChartToggleActions'
|
||||
| 'children'
|
||||
| 'onBrushEnd'
|
||||
| 'onFilter'
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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<HitsCounterProps>;
|
||||
|
||||
beforeAll(() => {
|
||||
props = {
|
||||
hits: {
|
||||
status: UnifiedHistogramFetchStatus.complete,
|
||||
total: 2,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('expect to render the number of hits', function () {
|
||||
component = mountWithIntl(<HitsCounter {...props} />);
|
||||
const hits = findTestSubject(component, 'unifiedHistogramQueryHits');
|
||||
expect(hits.text()).toBe('2');
|
||||
});
|
||||
|
||||
it('expect to render 1,899 hits if 1899 hits given', function () {
|
||||
component = mountWithIntl(
|
||||
<HitsCounter
|
||||
{...props}
|
||||
hits={{ status: UnifiedHistogramFetchStatus.complete, total: 1899 }}
|
||||
/>
|
||||
);
|
||||
const hits = findTestSubject(component, 'unifiedHistogramQueryHits');
|
||||
expect(hits.text()).toBe('1,899');
|
||||
});
|
||||
|
||||
it('should render the element passed to the append prop', () => {
|
||||
const appendHitsCounter = <div data-test-subj="appendHitsCounter">appendHitsCounter</div>;
|
||||
component = mountWithIntl(<HitsCounter {...props} append={appendHitsCounter} />);
|
||||
expect(findTestSubject(component, 'appendHitsCounter').length).toBe(1);
|
||||
});
|
||||
|
||||
it('should render a EuiLoadingSpinner when status is partial', () => {
|
||||
component = mountWithIntl(
|
||||
<HitsCounter {...props} hits={{ status: UnifiedHistogramFetchStatus.partial, total: 2 }} />
|
||||
);
|
||||
expect(component.find(EuiLoadingSpinner).length).toBe(1);
|
||||
});
|
||||
|
||||
it('should render unifiedHistogramQueryHitsPartial when status is partial', () => {
|
||||
component = mountWithIntl(
|
||||
<HitsCounter {...props} hits={{ status: UnifiedHistogramFetchStatus.partial, total: 2 }} />
|
||||
);
|
||||
expect(component.find('[data-test-subj="unifiedHistogramQueryHitsPartial"]').length).toBe(1);
|
||||
});
|
||||
|
||||
it('should render unifiedHistogramQueryHits when status is complete', () => {
|
||||
component = mountWithIntl(<HitsCounter {...props} />);
|
||||
expect(component.find('[data-test-subj="unifiedHistogramQueryHits"]').length).toBe(1);
|
||||
});
|
||||
});
|
|
@ -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 = (
|
||||
<strong
|
||||
data-test-subj={
|
||||
hits.status === 'partial' ? 'unifiedHistogramQueryHitsPartial' : 'unifiedHistogramQueryHits'
|
||||
}
|
||||
>
|
||||
<FormattedNumber value={hits.total ?? 0} />
|
||||
</strong>
|
||||
);
|
||||
|
||||
const hitsCounterCss = css`
|
||||
flex-grow: 0;
|
||||
`;
|
||||
const hitsCounterTextCss = css`
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
css={hitsCounterCss}
|
||||
>
|
||||
<EuiFlexItem grow={false} aria-live="polite" css={hitsCounterTextCss}>
|
||||
<EuiText className="eui-textTruncate">
|
||||
{hits.status === 'partial' && (
|
||||
<FormattedMessage
|
||||
id="unifiedHistogram.partialHits"
|
||||
defaultMessage="≥{formattedHits} {hits, plural, one {hit} other {hits}}"
|
||||
values={{ hits: hits.total, formattedHits }}
|
||||
/>
|
||||
)}
|
||||
{hits.status !== 'partial' && (
|
||||
<FormattedMessage
|
||||
id="unifiedHistogram.hitsPluralTitle"
|
||||
defaultMessage="{formattedHits} {hits, plural, one {hit} other {hits}}"
|
||||
values={{ hits: hits.total, formattedHits }}
|
||||
/>
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{hits.status === 'partial' && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner
|
||||
size="m"
|
||||
aria-label={i18n.translate('unifiedHistogram.hitCountSpinnerAriaLabel', {
|
||||
defaultMessage: 'Final hit count still loading',
|
||||
})}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{append}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 = <EuiSpacer size="s" />;
|
||||
|
||||
export interface UnifiedHistogramLayoutProps extends PropsWithChildren<unknown> {
|
||||
/**
|
||||
* Optional class name to add to the layout container
|
||||
|
@ -107,9 +111,9 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren<unknown>
|
|||
*/
|
||||
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 (
|
||||
<>
|
||||
<InPortal node={topPanelNode}>
|
||||
<Chart
|
||||
<ChartMemoized
|
||||
isChartAvailable={isChartAvailable}
|
||||
className={chartClassName}
|
||||
services={services}
|
||||
dataView={dataView}
|
||||
|
@ -289,13 +289,12 @@ export const UnifiedHistogramLayout = ({
|
|||
isPlainRecord={isPlainRecord}
|
||||
chart={chart}
|
||||
breakdown={breakdown}
|
||||
appendHitsCounter={appendHitsCounter}
|
||||
appendHistogram={<EuiSpacer size="s" />}
|
||||
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}
|
||||
/>
|
||||
</InPortal>
|
||||
<InPortal node={mainPanelNode}>{children}</InPortal>
|
||||
<InPortal node={mainPanelNode}>
|
||||
{React.isValidElement(children)
|
||||
? React.cloneElement(children, { isChartAvailable })
|
||||
: children}
|
||||
</InPortal>
|
||||
<ResizableLayout
|
||||
className={className}
|
||||
mode={panelsMode}
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
"@kbn/inspector-plugin",
|
||||
"@kbn/expressions-plugin",
|
||||
"@kbn/test-jest-helpers",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/i18n",
|
||||
"@kbn/es-query",
|
||||
"@kbn/embeddable-plugin",
|
||||
|
@ -26,7 +25,10 @@
|
|||
"@kbn/visualizations-plugin",
|
||||
"@kbn/discover-utils",
|
||||
"@kbn/resizable-layout",
|
||||
"@kbn/shared-ux-button-toolbar",
|
||||
"@kbn/calculate-width-from-char-count",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/field-utils",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -159,24 +159,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await a11y.testAppSnapshot();
|
||||
});
|
||||
|
||||
it('a11y test for chart options panel', async () => {
|
||||
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 () => {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
261
test/functional/apps/discover/group3/_panels_toggle.ts
Normal file
261
test/functional/apps/discover/group3/_panels_toggle.ts
Normal file
|
@ -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' });
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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),
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}削除",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue