[Discover] Add resize support to the Discover field list sidebar (#167066)

## Summary

This PR adds resize support to the Discover field list sidebar, which is
persisted to a user's local storage similar to the resizable chart
height.

Additionally it migrates the resizable layout code from Unified
Histogram to a new package called `kbn-resizable-layout` so it can be
shared between Discover and Unified Histogram, as well as act as a new
platform component that other teams can consume to create their own
resizable layouts.


![resize](71b9a0ae-1795-43c8-acb0-e75fe46e2a8a)

Resolves #9531.

### Checklist

- [ ] ~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)~
- [ ]
~[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials~
- [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] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] ~Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))~
- [ ] ~If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)~
- [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)

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Davis McPhee 2023-09-27 21:52:25 -03:00 committed by GitHub
parent 272219ca49
commit 3e1865513d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 1822 additions and 789 deletions

2
.github/CODEOWNERS vendored
View file

@ -581,6 +581,8 @@ packages/kbn-repo-source-classifier-cli @elastic/kibana-operations
packages/kbn-reporting/common @elastic/appex-sharedux
x-pack/examples/reporting_example @elastic/appex-sharedux
x-pack/plugins/reporting @elastic/appex-sharedux
packages/kbn-resizable-layout @elastic/kibana-data-discovery
examples/resizable_layout_examples @elastic/kibana-data-discovery
x-pack/test/plugin_functional/plugins/resolver_test @elastic/security-solution
examples/response_stream @elastic/ml-ui
packages/kbn-rison @elastic/kibana-operations

View file

@ -0,0 +1,11 @@
{
"type": "plugin",
"id": "@kbn/resizable-layout-examples-plugin",
"owner": "@elastic/kibana-data-discovery",
"plugin": {
"id": "resizableLayoutExamples",
"server": false,
"browser": true,
"requiredPlugins": ["developerExamples"]
}
}

View file

@ -0,0 +1,161 @@
/*
* 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 { CoreThemeProvider } from '@kbn/core-theme-browser-internal';
import type { AppMountParameters } from '@kbn/core/public';
import { I18nProvider } from '@kbn/i18n-react';
import React, { ReactNode, useState } from 'react';
import ReactDOM from 'react-dom';
import { useIsWithinBreakpoints } from '@elastic/eui';
import { css } from '@emotion/react';
import {
ResizableLayout,
ResizableLayoutDirection,
ResizableLayoutMode,
} from '@kbn/resizable-layout';
import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
const ResizableSection = ({
direction,
initialFixedPanelSize,
minFixedPanelSize,
minFlexPanelSize,
fixedPanelColor,
flexPanelColor,
fixedPanelContent,
flexPanelContent,
}: {
direction: ResizableLayoutDirection;
initialFixedPanelSize: number;
minFixedPanelSize: number;
minFlexPanelSize: number;
fixedPanelColor: string;
flexPanelColor: string;
fixedPanelContent: ReactNode;
flexPanelContent: ReactNode;
}) => {
const [fixedPanelSize, setFixedPanelSize] = useState(initialFixedPanelSize);
const [container, setContainer] = useState<HTMLDivElement | null>(null);
const [fixedPanelNode] = useState(() =>
createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } })
);
const [flexPanelNode] = useState(() =>
createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } })
);
const isMobile = useIsWithinBreakpoints(['xs', 's']);
const layoutMode = isMobile ? ResizableLayoutMode.Static : ResizableLayoutMode.Resizable;
const layoutDirection = isMobile ? ResizableLayoutDirection.Vertical : direction;
const fullWidthAndHeightCss = css`
position: relative;
width: 100%;
height: 100%;
`;
const panelBaseCss = css`
${fullWidthAndHeightCss}
padding: 20px;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
`;
const fixedPanelCss = css`
${panelBaseCss}
background-color: ${fixedPanelColor};
`;
const flexPanelCss = css`
${panelBaseCss}
background-color: ${flexPanelColor};
`;
return (
<div ref={setContainer} css={fullWidthAndHeightCss}>
<InPortal node={fixedPanelNode}>
<div css={fixedPanelCss}>{fixedPanelContent}</div>
</InPortal>
<InPortal node={flexPanelNode}>
<div css={flexPanelCss}>{flexPanelContent}</div>
</InPortal>
<ResizableLayout
mode={layoutMode}
direction={layoutDirection}
container={container}
fixedPanelSize={fixedPanelSize}
minFixedPanelSize={minFixedPanelSize}
minFlexPanelSize={minFlexPanelSize}
fixedPanel={<OutPortal node={fixedPanelNode} />}
flexPanel={<OutPortal node={flexPanelNode} />}
onFixedPanelSizeChange={setFixedPanelSize}
/>
</div>
);
};
export const renderApp = ({ element, theme$ }: AppMountParameters) => {
ReactDOM.render(
<I18nProvider>
<CoreThemeProvider theme$={theme$}>
<div
css={css`
height: calc(100vh - var(--euiFixedHeadersOffset, 0));
`}
>
<ResizableSection
direction={ResizableLayoutDirection.Horizontal}
initialFixedPanelSize={500}
minFixedPanelSize={300}
minFlexPanelSize={500}
fixedPanelColor="#16E0BD"
flexPanelColor="#89A6FB"
fixedPanelContent={
<ResizableSection
direction={ResizableLayoutDirection.Vertical}
initialFixedPanelSize={200}
minFixedPanelSize={100}
minFlexPanelSize={200}
fixedPanelColor="#E3655B"
flexPanelColor="#FDCA40"
fixedPanelContent="Sidebar Header"
flexPanelContent="Sidebar Body"
/>
}
flexPanelContent={
<ResizableSection
direction={ResizableLayoutDirection.Vertical}
initialFixedPanelSize={300}
minFixedPanelSize={200}
minFlexPanelSize={300}
fixedPanelColor="#FFA0AC"
flexPanelColor="#F6F740"
fixedPanelContent="Main Body Header"
flexPanelContent={
<ResizableSection
direction={ResizableLayoutDirection.Horizontal}
initialFixedPanelSize={400}
minFixedPanelSize={200}
minFlexPanelSize={200}
fixedPanelColor="#78C3FB"
flexPanelColor="#EF709D"
fixedPanelContent="Main Body Left"
flexPanelContent="Main Body Right"
/>
}
/>
}
/>
</div>
</CoreThemeProvider>
</I18nProvider>,
element
);
return () => {
ReactDOM.unmountComponentAtNode(element);
};
};

View file

@ -0,0 +1,13 @@
/*
* 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 { ResizableLayoutExamplesPlugin } from './plugin';
export function plugin() {
return new ResizableLayoutExamplesPlugin();
}

View file

@ -0,0 +1,44 @@
/*
* 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 { AppMountParameters, AppNavLinkStatus, CoreSetup, Plugin } from '@kbn/core/public';
import type { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';
import image from './resizable_layout_examples.png';
export interface ResizableLayoutExamplesSetupPlugins {
developerExamples: DeveloperExamplesSetup;
}
const PLUGIN_ID = 'resizableLayoutExamples';
const PLUGIN_NAME = 'Resizable Layout Examples';
export class ResizableLayoutExamplesPlugin implements Plugin {
setup(core: CoreSetup, plugins: ResizableLayoutExamplesSetupPlugins) {
core.application.register({
id: PLUGIN_ID,
title: PLUGIN_NAME,
navLinkStatus: AppNavLinkStatus.hidden,
mount: async (params: AppMountParameters) => {
// Load application bundle
const { renderApp } = await import('./application');
// Render the application
return renderApp(params);
},
});
plugins.developerExamples.register({
appId: PLUGIN_ID,
title: PLUGIN_NAME,
description:
'A component for creating resizable layouts containing a fixed width panel and a flexible panel, with support for horizontal and vertical layouts.',
image,
});
}
start() {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types"
},
"include": ["common/**/*", "public/**/*", "server/**/*", "../../typings/**/*"],
"kbn_references": [
"@kbn/resizable-layout",
"@kbn/core-theme-browser-internal",
"@kbn/core",
"@kbn/i18n-react",
"@kbn/developer-examples-plugin",
],
"exclude": ["target/**/*"]
}

View file

@ -588,6 +588,8 @@
"@kbn/reporting-common": "link:packages/kbn-reporting/common",
"@kbn/reporting-example-plugin": "link:x-pack/examples/reporting_example",
"@kbn/reporting-plugin": "link:x-pack/plugins/reporting",
"@kbn/resizable-layout": "link:packages/kbn-resizable-layout",
"@kbn/resizable-layout-examples-plugin": "link:examples/resizable_layout_examples",
"@kbn/resolver-test-plugin": "link:x-pack/test/plugin_functional/plugins/resolver_test",
"@kbn/response-stream-plugin": "link:examples/response_stream",
"@kbn/rison": "link:packages/kbn-rison",

View file

@ -0,0 +1,85 @@
# @kbn/resizable-layout
A component for creating resizable layouts containing a fixed width panel and a flexible panel, with support for horizontal and vertical layouts.
## Example
> [!NOTE]
> For advanced usage see [the example plugin](/examples/resizable_layout_examples/public/application.tsx).
```tsx
import { useIsWithinBreakpoints } from '@elastic/eui';
import { css } from '@emotion/react';
import {
ResizableLayout,
ResizableLayoutDirection,
ResizableLayoutMode,
} from '@kbn/resizable-layout';
import React, { useRef, useState } from 'react';
// Using react-reverse-portal is recommended for complex/heavy layouts to prevent
// re-mounting panel components when the layout switches from resizable to static
import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
export const ResizablePage = () => {
const [fixedPanelSize, setFixedPanelSize] = useState(500);
const [container, setContainer] = useState<HTMLDivElement | null>(null);
const [fixedPanelNode] = useState(() =>
createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } })
);
const [flexPanelNode] = useState(() =>
createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } })
);
const isMobile = useIsWithinBreakpoints(['xs']);
const layoutMode = isMobile ? ResizableLayoutMode.Static : ResizableLayoutMode.Resizable;
const layoutDirection = isMobile
? ResizableLayoutDirection.Vertical
: ResizableLayoutDirection.Horizontal;
const fullWidthAndHeightCss = css`
position: relative;
width: 100%;
height: 100%;
`;
const panelBaseCss = css`
${fullWidthAndHeightCss}
padding: 20px;
`;
const fixedPanelCss = css`
${panelBaseCss}
background-color: rgb(255 0 0 / 30%);
`;
const flexPanelCss = css`
${panelBaseCss}
background-color: rgb(0 0 255 / 30%);
`;
return (
<div ref={setContainer} css={fullWidthAndHeightCss}>
<InPortal node={fixedPanelNode}>
<div css={fixedPanelCss}>
This is the fixed width panel. It will remain the same size when resizing the window until
the flexible panel reaches its minimum size.
</div>
</InPortal>
<InPortal node={flexPanelNode}>
<div css={flexPanelCss}>
This is the flexible width panel. It will resize as the window resizes until it reaches
its minimum size.
</div>
</InPortal>
<ResizableLayout
mode={layoutMode}
direction={layoutDirection}
container={container}
fixedPanelSize={fixedPanelSize}
minFixedPanelSize={300}
minFlexPanelSize={500}
fixedPanel={<OutPortal node={fixedPanelNode} />}
flexPanel={<OutPortal node={flexPanelNode} />}
onFixedPanelSizeChange={setFixedPanelSize}
/>
</div>
);
};
```

View file

@ -0,0 +1,14 @@
/*
* 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 { withSuspense } from '@kbn/shared-ux-utility';
import { lazy } from 'react';
export { ResizableLayoutMode, ResizableLayoutDirection } from './types';
export type { ResizableLayoutProps } from './src/resizable_layout';
export const ResizableLayout = withSuspense(lazy(() => import('./src/resizable_layout')));

View file

@ -6,4 +6,8 @@
* Side Public License, v 1.
*/
export { Panels, PANELS_MODE } from './panels';
module.exports = {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-resizable-layout'],
};

View file

@ -0,0 +1,6 @@
{
"type": "shared-common",
"id": "@kbn/resizable-layout",
"description": "A component for creating resizable layouts containing a fixed width panel and a flexible panel, with support for horizontal and vertical layouts.",
"owner": "@elastic/kibana-data-discovery"
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/resizable-layout",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,246 @@
/*
* 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 { ReactWrapper } from 'enzyme';
import { mount } from 'enzyme';
import { ReactElement, useState } from 'react';
import React from 'react';
import { PanelsResizable } from './panels_resizable';
import { act } from 'react-dom/test-utils';
const containerHeight = 1000;
const containerWidth = 500;
const fixedPanelId = 'fixedPanel';
jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'),
useResizeObserver: jest.fn(),
useGeneratedHtmlId: jest.fn(() => fixedPanelId),
}));
import * as eui from '@elastic/eui';
import { waitFor } from '@testing-library/dom';
import { ResizableLayoutDirection } from '../types';
describe('Panels resizable', () => {
const mountComponent = ({
className = '',
direction = ResizableLayoutDirection.Vertical,
container = null,
initialFixedPanelSize = 0,
minFixedPanelSize = 0,
minFlexPanelSize = 0,
fixedPanel = <></>,
flexPanel = <></>,
attachTo,
onFixedPanelSizeChange = jest.fn(),
}: {
className?: string;
direction?: ResizableLayoutDirection;
container?: HTMLElement | null;
initialFixedPanelSize?: number;
minFixedPanelSize?: number;
minFlexPanelSize?: number;
fixedPanel?: ReactElement;
flexPanel?: ReactElement;
attachTo?: HTMLElement;
onFixedPanelSizeChange?: (fixedPanelSize: number) => void;
}) => {
const PanelsWrapper = ({ fixedPanelSize }: { fixedPanelSize?: number }) => {
const [panelSizes, setPanelSizes] = useState({
fixedPanelSizePct: 50,
flexPanelSizePct: 50,
});
return (
<PanelsResizable
className={className}
direction={direction}
container={container}
fixedPanelSize={fixedPanelSize ?? initialFixedPanelSize}
minFixedPanelSize={minFixedPanelSize}
minFlexPanelSize={minFlexPanelSize}
panelSizes={panelSizes}
fixedPanel={fixedPanel}
flexPanel={flexPanel}
onFixedPanelSizeChange={onFixedPanelSizeChange}
setPanelSizes={setPanelSizes}
/>
);
};
return mount(<PanelsWrapper />, attachTo ? { attachTo } : undefined);
};
const expectCorrectPanelSizes = (
component: ReactWrapper,
currentContainerSize: number,
fixedPanelSize: number
) => {
const fixedPanelSizePct = (fixedPanelSize / currentContainerSize) * 100;
expect(
component.find('[data-test-subj="resizableLayoutResizablePanelFixed"]').at(0).prop('size')
).toBe(fixedPanelSizePct);
expect(
component.find('[data-test-subj="resizableLayoutResizablePanelFlex"]').at(0).prop('size')
).toBe(100 - fixedPanelSizePct);
};
const forceRender = (component: ReactWrapper) => {
component.setProps({}).update();
};
beforeEach(() => {
jest
.spyOn(eui, 'useResizeObserver')
.mockReturnValue({ height: containerHeight, width: containerWidth });
});
it('should render both panels', () => {
const fixedPanel = <div data-test-subj="fixedPanel" />;
const flexPanel = <div data-test-subj="flexPanel" />;
const component = mountComponent({ fixedPanel, flexPanel });
expect(component.contains(fixedPanel)).toBe(true);
expect(component.contains(flexPanel)).toBe(true);
});
it('should set the initial sizes of both panels', () => {
const initialFixedPanelSize = 200;
const component = mountComponent({ initialFixedPanelSize });
expectCorrectPanelSizes(component, containerHeight, initialFixedPanelSize);
});
it('should set the correct sizes of both panels when the panels are resized', () => {
const initialFixedPanelSize = 200;
const onFixedPanelSizeChange = jest.fn((fixedPanelSize) => {
component.setProps({ fixedPanelSize }).update();
});
const component = mountComponent({ initialFixedPanelSize, onFixedPanelSizeChange });
expectCorrectPanelSizes(component, containerHeight, initialFixedPanelSize);
const newFixedPanelSizePct = 30;
const onPanelSizeChange = component
.find('[data-test-subj="resizableLayoutResizableContainer"]')
.at(0)
.prop('onPanelWidthChange') as Function;
act(() => {
onPanelSizeChange({ [fixedPanelId]: newFixedPanelSizePct });
});
forceRender(component);
const newFixedPanelSize = (newFixedPanelSizePct / 100) * containerHeight;
expect(onFixedPanelSizeChange).toHaveBeenCalledWith(newFixedPanelSize);
expectCorrectPanelSizes(component, containerHeight, newFixedPanelSize);
});
it('should maintain the size of the fixed panel and resize the flex panel when the container size changes', () => {
const initialFixedPanelSize = 200;
const component = mountComponent({ initialFixedPanelSize });
expectCorrectPanelSizes(component, containerHeight, initialFixedPanelSize);
const newContainerSize = 2000;
jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: newContainerSize, width: 0 });
forceRender(component);
expectCorrectPanelSizes(component, newContainerSize, initialFixedPanelSize);
});
it('should resize the fixed panel once the flex panel is at its minimum size', () => {
const initialFixedPanelSize = 500;
const minFixedPanelSize = 100;
const minFlexPanelSize = 100;
const component = mountComponent({
initialFixedPanelSize,
minFixedPanelSize,
minFlexPanelSize,
});
expectCorrectPanelSizes(component, containerHeight, initialFixedPanelSize);
const newContainerSize = 400;
jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: newContainerSize, width: 0 });
forceRender(component);
expectCorrectPanelSizes(component, newContainerSize, newContainerSize - minFlexPanelSize);
jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: containerHeight, width: 0 });
forceRender(component);
expectCorrectPanelSizes(component, containerHeight, initialFixedPanelSize);
});
it('should maintain the minimum sizes of both panels when the container is too small to fit them', () => {
const initialFixedPanelSize = 500;
const minFixedPanelSize = 100;
const minFlexPanelSize = 150;
const component = mountComponent({
initialFixedPanelSize,
minFixedPanelSize,
minFlexPanelSize,
});
expectCorrectPanelSizes(component, containerHeight, initialFixedPanelSize);
const newContainerSize = 200;
jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: newContainerSize, width: 0 });
forceRender(component);
expect(
component.find('[data-test-subj="resizableLayoutResizablePanelFixed"]').at(0).prop('size')
).toBe((minFixedPanelSize / newContainerSize) * 100);
expect(
component.find('[data-test-subj="resizableLayoutResizablePanelFlex"]').at(0).prop('size')
).toBe((minFlexPanelSize / newContainerSize) * 100);
jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: containerHeight, width: 0 });
forceRender(component);
expectCorrectPanelSizes(component, containerHeight, initialFixedPanelSize);
});
it('should blur the resize button after a resize', async () => {
const attachTo = document.createElement('div');
document.body.appendChild(attachTo);
const component = mountComponent({ attachTo });
const getContainer = () =>
component.find('[data-test-subj="resizableLayoutResizableContainer"]').at(0);
const resizeButton = component.find('button[data-test-subj="resizableLayoutResizableButton"]');
act(() => {
const onResizeStart = getContainer().prop('onResizeStart') as Function;
onResizeStart('pointer');
});
(resizeButton.getDOMNode() as HTMLElement).focus();
forceRender(component);
act(() => {
const onResizeEnd = getContainer().prop('onResizeEnd') as Function;
onResizeEnd();
});
expect(resizeButton.getDOMNode()).toHaveFocus();
await waitFor(() => {
expect(resizeButton.getDOMNode()).not.toHaveFocus();
});
});
it('should pass direction "vertical" to EuiResizableContainer when direction is ResizableLayoutDirection.Vertical', () => {
const component = mountComponent({ direction: ResizableLayoutDirection.Vertical });
expect(
component.find('[data-test-subj="resizableLayoutResizableContainer"]').at(0).prop('direction')
).toBe('vertical');
});
it('should pass direction "horizontal" to EuiResizableContainer when direction is ResizableLayoutDirection.Horizontal', () => {
const component = mountComponent({ direction: ResizableLayoutDirection.Horizontal });
expect(
component.find('[data-test-subj="resizableLayoutResizableContainer"]').at(0).prop('direction')
).toBe('horizontal');
});
it('should use containerHeight when direction is ResizableLayoutDirection.Vertical', () => {
const initialFixedPanelSize = 200;
const component = mountComponent({
direction: ResizableLayoutDirection.Vertical,
initialFixedPanelSize,
});
expectCorrectPanelSizes(component, containerHeight, initialFixedPanelSize);
});
it('should use containerWidth when direction is ResizableLayoutDirection.Horizontal', () => {
const initialFixedPanelSize = 200;
const component = mountComponent({
direction: ResizableLayoutDirection.Horizontal,
initialFixedPanelSize,
});
expectCorrectPanelSizes(component, containerWidth, initialFixedPanelSize);
});
});

View file

@ -0,0 +1,228 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { EuiResizableContainer, useGeneratedHtmlId, useResizeObserver } from '@elastic/eui';
import type { ResizeTrigger } from '@elastic/eui/src/components/resizable_container/types';
import { css } from '@emotion/react';
import { isEqual, round } from 'lodash';
import type { ReactElement } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { ResizableLayoutDirection } from '../types';
import { getContainerSize, percentToPixels, pixelsToPercent } from './utils';
export const PanelsResizable = ({
className,
direction,
container,
fixedPanelSize,
minFixedPanelSize,
minFlexPanelSize,
panelSizes,
fixedPanel,
flexPanel,
resizeButtonClassName,
['data-test-subj']: dataTestSubj = 'resizableLayout',
onFixedPanelSizeChange,
setPanelSizes,
}: {
className?: string;
direction: ResizableLayoutDirection;
container: HTMLElement | null;
fixedPanelSize: number;
minFixedPanelSize: number;
minFlexPanelSize: number;
panelSizes: {
fixedPanelSizePct: number;
flexPanelSizePct: number;
};
fixedPanel: ReactElement;
flexPanel: ReactElement;
resizeButtonClassName?: string;
['data-test-subj']?: string;
onFixedPanelSizeChange?: (fixedPanelSize: number) => void;
setPanelSizes: (panelSizes: { fixedPanelSizePct: number; flexPanelSizePct: number }) => void;
}) => {
const fixedPanelId = useGeneratedHtmlId({ prefix: 'fixedPanel' });
const { height: containerHeight, width: containerWidth } = useResizeObserver(container);
const containerSize = getContainerSize(direction, containerWidth, containerHeight);
// EuiResizableContainer doesn't work properly when used with react-reverse-portal and
// will cancel the resize. To work around this we keep track of when resizes start and
// end to toggle the rendering of a transparent overlay which prevents the cancellation.
// EUI issue: https://github.com/elastic/eui/issues/6199
const [resizeWithPortalsHackIsResizing, setResizeWithPortalsHackIsResizing] = useState(false);
const enableResizeWithPortalsHack = useCallback(
() => setResizeWithPortalsHackIsResizing(true),
[]
);
const disableResizeWithPortalsHack = useCallback(
() => setResizeWithPortalsHackIsResizing(false),
[]
);
const defaultButtonCss = css`
z-index: 3;
`;
const resizeWithPortalsHackButtonCss = css`
z-index: 4;
`;
const resizeWithPortalsHackOverlayCss = css`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 3;
`;
// We convert the top panel size from a percentage of the container size
// to a pixel value and emit the change to the parent component. We also convert
// the pixel value back to a percentage before updating the panel sizes to avoid
// rounding issues with the isEqual check in the effect below.
const onPanelSizeChange = useCallback(
({ [fixedPanelId]: currentFixedPanelSize }: { [key: string]: number }) => {
const newFixedPanelSizePx = percentToPixels(containerSize, currentFixedPanelSize);
const newFixedPanelSizePct = pixelsToPercent(containerSize, newFixedPanelSizePx);
setPanelSizes({
fixedPanelSizePct: round(newFixedPanelSizePct, 4),
flexPanelSizePct: round(100 - newFixedPanelSizePct, 4),
});
onFixedPanelSizeChange?.(newFixedPanelSizePx);
},
[fixedPanelId, containerSize, setPanelSizes, onFixedPanelSizeChange]
);
// This effect will update the panel sizes based on the top panel size whenever
// it or the container size changes. This allows us to keep the size of the
// top panel fixed when the window is resized.
useEffect(() => {
if (!containerSize) {
return;
}
let fixedPanelSizePct: number;
let flexPanelSizePct: number;
// If the container size is less than the minimum main content size
// plus the current top panel size, then we need to make some adjustments.
if (containerSize < minFlexPanelSize + fixedPanelSize) {
const newFixedPanelSize = containerSize - minFlexPanelSize;
// Try to make the top panel size fit within the container, but if it
// doesn't then just use the minimum sizes.
if (newFixedPanelSize < minFixedPanelSize) {
fixedPanelSizePct = pixelsToPercent(containerSize, minFixedPanelSize);
flexPanelSizePct = pixelsToPercent(containerSize, minFlexPanelSize);
} else {
fixedPanelSizePct = pixelsToPercent(containerSize, newFixedPanelSize);
flexPanelSizePct = 100 - fixedPanelSizePct;
}
} else {
fixedPanelSizePct = pixelsToPercent(containerSize, fixedPanelSize);
flexPanelSizePct = 100 - fixedPanelSizePct;
}
const newPanelSizes = {
fixedPanelSizePct: round(fixedPanelSizePct, 4),
flexPanelSizePct: round(flexPanelSizePct, 4),
};
// Skip updating the panel sizes if they haven't changed
// since onPanelSizeChange will also trigger this effect.
if (!isEqual(panelSizes, newPanelSizes)) {
setPanelSizes(newPanelSizes);
}
}, [
containerSize,
fixedPanelSize,
minFixedPanelSize,
minFlexPanelSize,
panelSizes,
setPanelSizes,
]);
const onResizeStart = useCallback(
(trigger: ResizeTrigger) => {
if (trigger !== 'pointer') {
return;
}
enableResizeWithPortalsHack();
},
[enableResizeWithPortalsHack]
);
const onResizeEnd = useCallback(() => {
if (!resizeWithPortalsHackIsResizing) {
return;
}
// We don't want the resize button to retain focus after the resize is complete,
// but EuiResizableContainer will force focus it onClick. To work around this we
// use setTimeout to wait until after onClick has been called before blurring.
if (document.activeElement instanceof HTMLElement) {
const button = document.activeElement;
setTimeout(() => {
button.blur();
});
}
disableResizeWithPortalsHack();
}, [disableResizeWithPortalsHack, resizeWithPortalsHackIsResizing]);
// Don't render EuiResizableContainer until we have have valid
// panel sizes or it can cause the resize functionality to break.
if (!panelSizes.fixedPanelSizePct && !panelSizes.flexPanelSizePct) {
return null;
}
return (
<EuiResizableContainer
className={className}
direction={direction}
onPanelWidthChange={onPanelSizeChange}
onResizeStart={onResizeStart}
onResizeEnd={onResizeEnd}
data-test-subj={`${dataTestSubj}ResizableContainer`}
css={css`
height: 100%;
`}
>
{(EuiResizablePanel, EuiResizableButton) => (
<>
<EuiResizablePanel
id={fixedPanelId}
minSize={`${minFixedPanelSize}px`}
size={panelSizes.fixedPanelSizePct}
paddingSize="none"
data-test-subj={`${dataTestSubj}ResizablePanelFixed`}
>
{fixedPanel}
</EuiResizablePanel>
<EuiResizableButton
className={resizeButtonClassName}
css={
resizeWithPortalsHackIsResizing ? resizeWithPortalsHackButtonCss : defaultButtonCss
}
data-test-subj={`${dataTestSubj}ResizableButton`}
/>
<EuiResizablePanel
minSize={`${minFlexPanelSize}px`}
size={panelSizes.flexPanelSizePct}
paddingSize="none"
data-test-subj={`${dataTestSubj}ResizablePanelFlex`}
>
{flexPanel}
</EuiResizablePanel>
{resizeWithPortalsHackIsResizing ? <div css={resizeWithPortalsHackOverlayCss} /> : <></>}
</>
)}
</EuiResizableContainer>
);
};

View file

@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { EuiFlexGroup } from '@elastic/eui';
import { mount } from 'enzyme';
import type { ReactElement } from 'react';
import React from 'react';
import { ResizableLayoutDirection } from '../types';
import { PanelsStatic } from './panels_static';
describe('Panels static', () => {
const mountComponent = ({
direction = ResizableLayoutDirection.Vertical,
hideFixedPanel = false,
fixedPanel = <></>,
flexPanel = <></>,
}: {
direction?: ResizableLayoutDirection;
hideFixedPanel?: boolean;
fixedPanel: ReactElement;
flexPanel: ReactElement;
}) => {
return mount(
<PanelsStatic
direction={direction}
hideFixedPanel={hideFixedPanel}
fixedPanel={fixedPanel}
flexPanel={flexPanel}
/>
);
};
it('should render both panels when hideFixedPanel is false', () => {
const fixedPanel = <div data-test-subj="fixedPanel" />;
const flexPanel = <div data-test-subj="flexPanel" />;
const component = mountComponent({ fixedPanel, flexPanel });
expect(component.contains(fixedPanel)).toBe(true);
expect(component.contains(flexPanel)).toBe(true);
});
it('should render only flex panel when hideFixedPanel is true', () => {
const fixedPanel = <div data-test-subj="fixedPanel" />;
const flexPanel = <div data-test-subj="flexPanel" />;
const component = mountComponent({ hideFixedPanel: true, fixedPanel, flexPanel });
expect(component.contains(fixedPanel)).toBe(false);
expect(component.contains(flexPanel)).toBe(true);
});
it('should pass direction "column" to EuiFlexGroup when direction is ResizableLayoutDirection.Vertical', () => {
const component = mountComponent({
direction: ResizableLayoutDirection.Vertical,
fixedPanel: <></>,
flexPanel: <></>,
});
expect(component.find(EuiFlexGroup).prop('direction')).toBe('column');
});
it('should pass direction "row" to EuiFlexGroup when direction is ResizableLayoutDirection.Horizontal', () => {
const component = mountComponent({
direction: ResizableLayoutDirection.Horizontal,
fixedPanel: <></>,
flexPanel: <></>,
});
expect(component.find(EuiFlexGroup).prop('direction')).toBe('row');
});
});

View file

@ -10,37 +10,43 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { css } from '@emotion/react';
import type { ReactElement } from 'react';
import React from 'react';
import { ResizableLayoutDirection } from '../types';
export const PanelsFixed = ({
export const PanelsStatic = ({
className,
hideTopPanel,
topPanel,
mainPanel,
direction,
hideFixedPanel,
fixedPanel,
flexPanel,
}: {
className?: string;
hideTopPanel?: boolean;
topPanel: ReactElement;
mainPanel: ReactElement;
direction: ResizableLayoutDirection;
hideFixedPanel?: boolean;
fixedPanel: ReactElement;
flexPanel: ReactElement;
}) => {
// By default a flex item has overflow: visible, min-height: auto, and min-width: auto.
// This can cause the item to overflow the flexbox parent when its content is too large.
// Setting the overflow to something other than visible (e.g. auto) resets the min-height
// and min-width to 0 and makes the item respect the flexbox parent's size.
// https://stackoverflow.com/questions/36247140/why-dont-flex-items-shrink-past-content-size
const mainPanelCss = css`
const flexPanelCss = css`
overflow: auto;
`;
return (
<EuiFlexGroup
className={className}
direction="column"
direction={direction === ResizableLayoutDirection.Vertical ? 'column' : 'row'}
alignItems="stretch"
gutterSize="none"
responsive={false}
css={css`
height: 100%;
`}
>
{!hideTopPanel && <EuiFlexItem grow={false}>{topPanel}</EuiFlexItem>}
<EuiFlexItem css={mainPanelCss}>{mainPanel}</EuiFlexItem>
{!hideFixedPanel && <EuiFlexItem grow={false}>{fixedPanel}</EuiFlexItem>}
<EuiFlexItem css={flexPanelCss}>{flexPanel}</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,106 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { mount } from 'enzyme';
import type { ReactElement } from 'react';
import React from 'react';
import ResizableLayout from './resizable_layout';
import { PanelsResizable } from './panels_resizable';
import { PanelsStatic } from './panels_static';
import { ResizableLayoutDirection, ResizableLayoutMode } from '../types';
jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'),
useResizeObserver: jest.fn(() => ({ width: 1000, height: 1000 })),
}));
describe('ResizableLayout component', () => {
const mountComponent = ({
mode = ResizableLayoutMode.Resizable,
container = null,
initialFixedPanelSize = 200,
minFixedPanelSize = 100,
minFlexPanelSize = 100,
fixedPanel = <></>,
flexPanel = <></>,
}: {
mode?: ResizableLayoutMode;
container?: HTMLElement | null;
initialFixedPanelSize?: number;
minFixedPanelSize?: number;
minFlexPanelSize?: number;
flexPanel?: ReactElement;
fixedPanel?: ReactElement;
}) => {
return mount(
<ResizableLayout
mode={mode}
direction={ResizableLayoutDirection.Vertical}
container={container}
fixedPanelSize={initialFixedPanelSize}
minFixedPanelSize={minFixedPanelSize}
minFlexPanelSize={minFlexPanelSize}
fixedPanel={fixedPanel}
flexPanel={flexPanel}
onFixedPanelSizeChange={jest.fn()}
/>
);
};
it('should show PanelsFixed when mode is ResizableLayoutMode.Single', () => {
const fixedPanel = <div data-test-subj="fixedPanel" />;
const flexPanel = <div data-test-subj="flexPanel" />;
const component = mountComponent({ mode: ResizableLayoutMode.Single, fixedPanel, flexPanel });
expect(component.find(PanelsStatic).exists()).toBe(true);
expect(component.find(PanelsResizable).exists()).toBe(false);
expect(component.contains(fixedPanel)).toBe(false);
expect(component.contains(flexPanel)).toBe(true);
});
it('should show PanelsFixed when mode is ResizableLayoutMode.Static', () => {
const fixedPanel = <div data-test-subj="fixedPanel" />;
const flexPanel = <div data-test-subj="flexPanel" />;
const component = mountComponent({ mode: ResizableLayoutMode.Static, fixedPanel, flexPanel });
expect(component.find(PanelsStatic).exists()).toBe(true);
expect(component.find(PanelsResizable).exists()).toBe(false);
expect(component.contains(fixedPanel)).toBe(true);
expect(component.contains(flexPanel)).toBe(true);
});
it('should show PanelsResizable when mode is ResizableLayoutMode.Resizable', () => {
const fixedPanel = <div data-test-subj="fixedPanel" />;
const flexPanel = <div data-test-subj="flexPanel" />;
const component = mountComponent({
mode: ResizableLayoutMode.Resizable,
fixedPanel,
flexPanel,
});
expect(component.find(PanelsStatic).exists()).toBe(false);
expect(component.find(PanelsResizable).exists()).toBe(true);
expect(component.contains(fixedPanel)).toBe(true);
expect(component.contains(flexPanel)).toBe(true);
});
it('should pass true for hideFixedPanel when mode is ResizableLayoutMode.Single', () => {
const fixedPanel = <div data-test-subj="fixedPanel" />;
const flexPanel = <div data-test-subj="flexPanel" />;
const component = mountComponent({ mode: ResizableLayoutMode.Single, fixedPanel, flexPanel });
expect(component.find(PanelsStatic).prop('hideFixedPanel')).toBe(true);
expect(component.contains(fixedPanel)).toBe(false);
expect(component.contains(flexPanel)).toBe(true);
});
it('should pass false for hideFixedPanel when mode is ResizableLayoutMode.Static', () => {
const fixedPanel = <div data-test-subj="fixedPanel" />;
const flexPanel = <div data-test-subj="flexPanel" />;
const component = mountComponent({ mode: ResizableLayoutMode.Static, fixedPanel, flexPanel });
expect(component.find(PanelsStatic).prop('hideFixedPanel')).toBe(false);
expect(component.contains(fixedPanel)).toBe(true);
expect(component.contains(flexPanel)).toBe(true);
});
});

View file

@ -0,0 +1,130 @@
/*
* 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 { ReactElement, useState } from 'react';
import React from 'react';
import { round } from 'lodash';
import { PanelsResizable } from './panels_resizable';
import { PanelsStatic } from './panels_static';
import { ResizableLayoutDirection, ResizableLayoutMode } from '../types';
import { getContainerSize, pixelsToPercent } from './utils';
export interface ResizableLayoutProps {
/**
* Class name for the layout container
*/
className?: string;
/**
* The current layout mode
*/
mode: ResizableLayoutMode;
/**
* The current layout direction
*/
direction: ResizableLayoutDirection;
/**
* The parent container element, used to calculate the layout size
*/
container: HTMLElement | null;
/**
* Current size of the fixed panel in pixels
*/
fixedPanelSize: number;
/**
* Minimum size of the fixed panel in pixels
*/
minFixedPanelSize: number;
/**
* Minimum size of the flex panel in pixels
*/
minFlexPanelSize: number;
/**
* The fixed panel
*/
fixedPanel: ReactElement;
/**
* The flex panel
*/
flexPanel: ReactElement;
/**
* Class name for the resize button
*/
resizeButtonClassName?: string;
/**
* Test subject for the layout container
*/
['data-test-subj']?: string;
/**
* Callback when the fixed panel size changes, receives the new size in pixels
*/
onFixedPanelSizeChange?: (fixedPanelSize: number) => void;
}
const staticModes = [ResizableLayoutMode.Single, ResizableLayoutMode.Static];
const ResizableLayout = ({
className,
mode,
direction,
container,
fixedPanelSize,
minFixedPanelSize,
minFlexPanelSize,
fixedPanel,
flexPanel,
resizeButtonClassName,
['data-test-subj']: dataTestSubj,
onFixedPanelSizeChange,
}: ResizableLayoutProps) => {
const panelsProps = { className, fixedPanel, flexPanel };
const [panelSizes, setPanelSizes] = useState(() => {
if (!container) {
return { fixedPanelSizePct: 0, flexPanelSizePct: 0 };
}
const { width, height } = container.getBoundingClientRect();
const initialContainerSize = getContainerSize(direction, width, height);
if (!initialContainerSize) {
return { fixedPanelSizePct: 0, flexPanelSizePct: 0 };
}
const fixedPanelSizePct = pixelsToPercent(initialContainerSize, fixedPanelSize);
const flexPanelSizePct = 100 - fixedPanelSizePct;
return {
fixedPanelSizePct: round(fixedPanelSizePct, 4),
flexPanelSizePct: round(flexPanelSizePct, 4),
};
});
return staticModes.includes(mode) ? (
<PanelsStatic
direction={direction}
hideFixedPanel={mode === ResizableLayoutMode.Single}
{...panelsProps}
/>
) : (
<PanelsResizable
direction={direction}
container={container}
fixedPanelSize={fixedPanelSize}
minFixedPanelSize={minFixedPanelSize}
minFlexPanelSize={minFlexPanelSize}
panelSizes={panelSizes}
resizeButtonClassName={resizeButtonClassName}
data-test-subj={dataTestSubj}
onFixedPanelSizeChange={onFixedPanelSizeChange}
setPanelSizes={setPanelSizes}
{...panelsProps}
/>
);
};
// eslint-disable-next-line import/no-default-export
export default ResizableLayout;

View file

@ -0,0 +1,40 @@
/*
* 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 { ResizableLayoutDirection } from '../types';
import { getContainerSize, percentToPixels, pixelsToPercent } from './utils';
describe('getContainerSize', () => {
it('should return the width when direction is horizontal', () => {
expect(getContainerSize(ResizableLayoutDirection.Horizontal, 100, 200)).toBe(100);
});
it('should return the height when direction is vertical', () => {
expect(getContainerSize(ResizableLayoutDirection.Vertical, 100, 200)).toBe(200);
});
});
describe('percentToPixels', () => {
it('should convert percentage to pixels', () => {
expect(percentToPixels(250, 50)).toBe(125);
});
});
describe('pixelsToPercent', () => {
it('should convert pixels to percentage', () => {
expect(pixelsToPercent(250, 125)).toBe(50);
});
it('should clamp percentage to 0 when pixels is negative', () => {
expect(pixelsToPercent(250, -125)).toBe(0);
});
it('should clamp percentage to 100 when pixels is greater than container size', () => {
expect(pixelsToPercent(250, 500)).toBe(100);
});
});

View file

@ -0,0 +1,22 @@
/*
* 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 { clamp } from 'lodash';
import { ResizableLayoutDirection } from '../types';
export const percentToPixels = (containerSize: number, percentage: number) =>
Math.round(containerSize * (percentage / 100));
export const pixelsToPercent = (containerSize: number, pixels: number) =>
clamp((pixels / containerSize) * 100, 0, 100);
export const getContainerSize = (
direction: ResizableLayoutDirection,
width: number,
height: number
) => (direction === ResizableLayoutDirection.Vertical ? height : width);

View file

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types"
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["target/**/*"],
"kbn_references": [
"@kbn/shared-ux-utility",
]
}

View file

@ -0,0 +1,33 @@
/*
* 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 enum ResizableLayoutMode {
/**
* Single panel mode -- hides the fixed panel
*/
Single = 'single',
/**
* Static mode -- prevents resizing
*/
Static = 'static',
/**
* Resizable mode -- allows resizing
*/
Resizable = 'resizable',
}
export enum ResizableLayoutDirection {
/**
* Horizontal layout -- panels are side by side
*/
Horizontal = 'horizontal',
/**
* Vertical layout -- panels are stacked
*/
Vertical = 'vertical',
}

View file

@ -3,7 +3,6 @@
margin: 0 !important;
flex-grow: 1;
padding: 0;
width: $euiSize * 19;
height: 100%;
&--collapsed {
@ -11,6 +10,14 @@
padding: $euiSizeS $euiSizeS 0;
}
&.unifiedFieldListSidebar--fullWidth {
min-width: 0 !important;
}
&:not(.unifiedFieldListSidebar--fullWidth) {
width: $euiSize * 19;
}
@include euiBreakpoint('xs', 's') {
width: 100%;
padding: $euiSize;

View file

@ -59,6 +59,11 @@ export type UnifiedFieldListSidebarCustomizableProps = Pick<
*/
showFieldList?: boolean;
/**
* Make the field list full width
*/
fullWidth?: boolean;
/**
* Compressed view
*/
@ -145,6 +150,7 @@ export const UnifiedFieldListSidebarComponent: React.FC<UnifiedFieldListSidebarP
trackUiMetric,
showFieldList = true,
compressed = true,
fullWidth,
isAffectedByGlobalFilter,
prepend,
onAddFieldToWorkspace,
@ -306,6 +312,7 @@ export const UnifiedFieldListSidebarComponent: React.FC<UnifiedFieldListSidebarP
const pageSidebarProps: Partial<EuiPageSidebarProps> = {
className: classnames('unifiedFieldListSidebar', {
'unifiedFieldListSidebar--collapsed': isSidebarCollapsed,
['unifiedFieldListSidebar--fullWidth']: fullWidth,
}),
'aria-label': i18n.translate(
'unifiedFieldList.fieldListSidebar.indexAndFieldsSectionAriaLabel',

View file

@ -30,6 +30,7 @@ import {
EuiShowFor,
EuiTitle,
} from '@elastic/eui';
import { BehaviorSubject, Observable } from 'rxjs';
import {
useExistingFieldsFetcher,
type ExistingFieldsFetcher,
@ -49,6 +50,7 @@ import type {
} from '../../types';
export interface UnifiedFieldListSidebarContainerApi {
isSidebarCollapsed$: Observable<boolean>;
refetchFieldsExistenceInfo: ExistingFieldsFetcher['refetchFieldsExistenceInfo'];
closeFieldListFlyout: () => void;
// no user permission or missing dataViewFieldEditor service will result in `undefined` API methods
@ -121,6 +123,7 @@ 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 canEditDataView =
Boolean(dataViewFieldEditor?.userPermissions.editIndexPattern()) ||
@ -222,16 +225,21 @@ const UnifiedFieldListSidebarContainer = forwardRef<
};
}, []);
useEffect(() => {
isSidebarCollapsed$.next(isSidebarCollapsed);
}, [isSidebarCollapsed, isSidebarCollapsed$]);
useImperativeHandle(
componentRef,
() => ({
isSidebarCollapsed$,
refetchFieldsExistenceInfo,
closeFieldListFlyout,
createField: editField,
editField,
deleteField,
}),
[refetchFieldsExistenceInfo, closeFieldListFlyout, editField, deleteField]
[isSidebarCollapsed$, refetchFieldsExistenceInfo, closeFieldListFlyout, editField, deleteField]
);
if (!dataView) {

View file

@ -122,7 +122,7 @@ const mountComponent = async ({
columns: [],
viewMode: VIEW_MODE.DOCUMENT_LEVEL,
onAddFilter: jest.fn(),
resizeRef: { current: null },
container: null,
};
stateContainer.searchSessionManager = createSearchSessionMock(session).searchSessionManager;

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import React, { RefObject } from 'react';
import React from 'react';
import { UnifiedHistogramContainer } from '@kbn/unified-histogram-plugin/public';
import { css } from '@emotion/react';
import useObservable from 'react-use/lib/useObservable';
@ -17,7 +17,7 @@ import { ResetSearchButton } from './reset_search_button';
import { useAppStateSelector } from '../../services/discover_app_state_container';
export interface DiscoverHistogramLayoutProps extends DiscoverMainContentProps {
resizeRef: RefObject<HTMLDivElement>;
container: HTMLElement | null;
}
const histogramLayoutCss = css`
@ -28,7 +28,7 @@ export const DiscoverHistogramLayout = ({
isPlainRecord,
dataView,
stateContainer,
resizeRef,
container,
...mainContentProps
}: DiscoverHistogramLayoutProps) => {
const { dataState } = stateContainer;
@ -53,7 +53,7 @@ export const DiscoverHistogramLayout = ({
{...unifiedHistogramProps}
searchSessionId={searchSessionId}
requestAdapter={dataState.inspectorAdapters.requests}
resizeRef={resizeRef}
container={container}
appendHitsCounter={
savedSearch.id ? (
<ResetSearchButton resetSavedSearch={stateContainer.actions.undoSavedSearchChanges} />

View file

@ -29,10 +29,24 @@ discover-app {
.dscPageBody__contents {
overflow: hidden;
height: 100%;
}
.dscSidebarResizeButton {
background-color: transparent !important;
&:not(:hover):not(:focus) {
&:before, &:after {
width: 0;
}
}
}
.dscPageContent__wrapper {
overflow: hidden; // Ensures horizontal scroll of table
display: flex;
flex-direction: column;
height: 100%;
}
.dscPageContent {

View file

@ -41,6 +41,11 @@ import { act } from 'react-dom/test-utils';
import { ErrorCallout } from '../../../../components/common/error_callout';
import * as localStorageModule from 'react-use/lib/useLocalStorage';
jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'),
useResizeObserver: jest.fn(() => ({ width: 1000, height: 1000 })),
}));
jest.spyOn(localStorageModule, 'default');
setHeaderActionMenuMounter(jest.fn());

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import './discover_layout.scss';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
@ -31,6 +31,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 { useSavedSearchInitial } from '../../services/discover_state_provider';
import { DiscoverStateContainer } from '../../services/discover_state';
import { VIEW_MODE } from '../../../../../common/constants';
@ -52,6 +53,7 @@ import { SavedSearchURLConflictCallout } from '../../../../components/saved_sear
import { DiscoverHistogramLayout } from './discover_histogram_layout';
import { ErrorCallout } from '../../../../components/common/error_callout';
import { addLog } from '../../../../utils/add_log';
import { DiscoverResizableLayout } from './discover_resizable_layout';
const SidebarMemoized = React.memo(DiscoverSidebarResponsive);
const TopNavMemoized = React.memo(DiscoverTopNav);
@ -182,7 +184,8 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
}
}, [dataState.error, isPlainRecord]);
const resizeRef = useRef<HTMLDivElement>(null);
const [sidebarContainer, setSidebarContainer] = useState<HTMLDivElement | null>(null);
const [mainContainer, setMainContainer] = useState<HTMLDivElement | null>(null);
const [{ dragging }] = useDragDropContext();
const draggingFieldName = dragging?.id;
@ -211,7 +214,7 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
viewMode={viewMode}
onAddFilter={onAddFilter as DocViewFilterFn}
onFieldEdited={onFieldEdited}
resizeRef={resizeRef}
container={mainContainer}
onDropFieldToTable={onDropFieldToTable}
/>
{resultState === 'loading' && <LoadingSpinner />}
@ -221,14 +224,18 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
currentColumns,
dataView,
isPlainRecord,
mainContainer,
onAddFilter,
onDropFieldToTable,
onFieldEdited,
resultState,
stateContainer,
viewMode,
onDropFieldToTable,
]);
const [unifiedFieldListSidebarContainerApi, setUnifiedFieldListSidebarContainerApi] =
useState<UnifiedFieldListSidebarContainerApi | null>(null);
return (
<EuiPage
className="dscPage"
@ -265,73 +272,99 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
onFieldEdited={onFieldEdited}
/>
<EuiPageBody className="dscPageBody" aria-describedby="savedSearchTitle">
<SavedSearchURLConflictCallout
savedSearch={savedSearch}
spaces={spaces}
history={history}
/>
<EuiFlexGroup className="dscPageBody__contents" gutterSize="none">
<EuiFlexItem grow={false}>
<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$}
/>
</EuiFlexItem>
<EuiHideFor sizes={['xs', 's']}>
<EuiFlexItem
grow={false}
css={css`
border-right: ${euiTheme.border.thin};
`}
/>
</EuiHideFor>
<EuiFlexItem className="dscPageContent__wrapper">
{resultState === 'none' ? (
dataState.error ? (
<ErrorCallout
title={i18n.translate('discover.noResults.searchExamples.noResultsErrorTitle', {
defaultMessage: 'Unable to retrieve search results',
})}
error={dataState.error}
data-test-subj="discoverNoResultsError"
/>
) : (
<DiscoverNoResults
stateContainer={stateContainer}
isTimeBased={isTimeBased}
query={globalQueryState.query}
filters={globalQueryState.filters}
dataView={dataView}
onDisableFilters={onDisableFilters}
/>
)
) : (
<EuiPanel
role="main"
panelRef={resizeRef}
paddingSize="none"
borderRadius="none"
hasShadow={false}
hasBorder={false}
color="transparent"
className={classNames('dscPageContent', {
'dscPageContent--centered': contentCentered,
})}
<div
ref={setSidebarContainer}
css={css`
width: 100%;
height: 100%;
`}
>
<SavedSearchURLConflictCallout
savedSearch={savedSearch}
spaces={spaces}
history={history}
/>
<DiscoverResizableLayout
container={sidebarContainer}
unifiedFieldListSidebarContainerApi={unifiedFieldListSidebarContainerApi}
sidebarPanel={
<EuiFlexGroup
gutterSize="none"
css={css`
height: 100%;
`}
>
{mainDisplay}
</EuiPanel>
)}
</EuiFlexItem>
</EuiFlexGroup>
<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>
}
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}
data-test-subj="discoverNoResultsError"
/>
) : (
<DiscoverNoResults
stateContainer={stateContainer}
isTimeBased={isTimeBased}
query={globalQueryState.query}
filters={globalQueryState.filters}
dataView={dataView}
onDisableFilters={onDisableFilters}
/>
)
) : (
<EuiPanel
role="main"
panelRef={setMainContainer}
paddingSize="none"
borderRadius="none"
hasShadow={false}
hasBorder={false}
color="transparent"
className={classNames('dscPageContent', {
'dscPageContent--centered': contentCentered,
})}
>
{mainDisplay}
</EuiPanel>
)}
</div>
}
/>
</div>
</EuiPageBody>
</EuiPage>
);

View file

@ -0,0 +1,169 @@
/*
* 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 {
ResizableLayout,
ResizableLayoutDirection,
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';
const mockSidebarKey = SIDEBAR_WIDTH_KEY;
let mockSidebarWidth: number | undefined;
jest.mock('react-use/lib/useLocalStorage', () => {
return jest.fn((key: string, initialValue: number) => {
if (key !== mockSidebarKey) {
throw new Error(`Unexpected key: ${key}`);
}
return [mockSidebarWidth ?? initialValue, jest.fn()];
});
});
let mockIsMobile = false;
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
useIsWithinBreakpoints: jest.fn((breakpoints: string[]) => {
if (!mockIsEqual(breakpoints, ['xs', 's'])) {
throw new Error(`Unexpected breakpoints: ${breakpoints}`);
}
return mockIsMobile;
}),
};
});
describe('DiscoverResizableLayout', () => {
beforeEach(() => {
mockSidebarWidth = undefined;
mockIsMobile = false;
});
it('should render sidebarPanel and mainPanel', () => {
const wrapper = mount(
<DiscoverResizableLayout
container={null}
unifiedFieldListSidebarContainerApi={null}
sidebarPanel={<div data-test-subj="sidebarPanel" />}
mainPanel={<div data-test-subj="mainPanel" />}
/>
);
expect(findTestSubject(wrapper, 'sidebarPanel')).toHaveLength(1);
expect(findTestSubject(wrapper, 'mainPanel')).toHaveLength(1);
});
it('should use the default sidebar width when no value is stored in local storage', () => {
const wrapper = mount(
<DiscoverResizableLayout
container={null}
unifiedFieldListSidebarContainerApi={null}
sidebarPanel={<div data-test-subj="sidebarPanel" />}
mainPanel={<div data-test-subj="mainPanel" />}
/>
);
expect(wrapper.find(ResizableLayout).prop('fixedPanelSize')).toBe(304);
});
it('should use the stored sidebar width from local storage', () => {
mockSidebarWidth = 400;
const wrapper = mount(
<DiscoverResizableLayout
container={null}
unifiedFieldListSidebarContainerApi={null}
sidebarPanel={<div data-test-subj="sidebarPanel" />}
mainPanel={<div data-test-subj="mainPanel" />}
/>
);
expect(wrapper.find(ResizableLayout).prop('fixedPanelSize')).toBe(400);
});
it('should pass mode ResizableLayoutMode.Resizable when not mobile and sidebar is not collapsed', () => {
mockIsMobile = false;
const wrapper = mount(
<DiscoverResizableLayout
container={null}
unifiedFieldListSidebarContainerApi={
{ isSidebarCollapsed$: of(false) } as UnifiedFieldListSidebarContainerApi
}
sidebarPanel={<div data-test-subj="sidebarPanel" />}
mainPanel={<div data-test-subj="mainPanel" />}
/>
);
expect(wrapper.find(ResizableLayout).prop('mode')).toBe(ResizableLayoutMode.Resizable);
});
it('should pass mode ResizableLayoutMode.Static when mobile', () => {
mockIsMobile = true;
const wrapper = mount(
<DiscoverResizableLayout
container={null}
unifiedFieldListSidebarContainerApi={
{ isSidebarCollapsed$: of(false) } as UnifiedFieldListSidebarContainerApi
}
sidebarPanel={<div data-test-subj="sidebarPanel" />}
mainPanel={<div data-test-subj="mainPanel" />}
/>
);
expect(wrapper.find(ResizableLayout).prop('mode')).toBe(ResizableLayoutMode.Static);
});
it('should pass mode ResizableLayoutMode.Static when not mobile and sidebar is collapsed', () => {
mockIsMobile = false;
const wrapper = mount(
<DiscoverResizableLayout
container={null}
unifiedFieldListSidebarContainerApi={
{ isSidebarCollapsed$: of(true) } as UnifiedFieldListSidebarContainerApi
}
sidebarPanel={<div data-test-subj="sidebarPanel" />}
mainPanel={<div data-test-subj="mainPanel" />}
/>
);
expect(wrapper.find(ResizableLayout).prop('mode')).toBe(ResizableLayoutMode.Static);
});
it('should pass direction ResizableLayoutDirection.Horizontal when not mobile', () => {
mockIsMobile = false;
const wrapper = mount(
<DiscoverResizableLayout
container={null}
unifiedFieldListSidebarContainerApi={
{ isSidebarCollapsed$: of(false) } as UnifiedFieldListSidebarContainerApi
}
sidebarPanel={<div data-test-subj="sidebarPanel" />}
mainPanel={<div data-test-subj="mainPanel" />}
/>
);
expect(wrapper.find(ResizableLayout).prop('direction')).toBe(
ResizableLayoutDirection.Horizontal
);
});
it('should pass direction ResizableLayoutDirection.Vertical when mobile', () => {
mockIsMobile = true;
const wrapper = mount(
<DiscoverResizableLayout
container={null}
unifiedFieldListSidebarContainerApi={
{ isSidebarCollapsed$: of(false) } as UnifiedFieldListSidebarContainerApi
}
sidebarPanel={<div data-test-subj="sidebarPanel" />}
mainPanel={<div data-test-subj="mainPanel" />}
/>
);
expect(wrapper.find(ResizableLayout).prop('direction')).toBe(ResizableLayoutDirection.Vertical);
});
});

View file

@ -0,0 +1,80 @@
/*
* 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 { useEuiTheme, useIsWithinBreakpoints } from '@elastic/eui';
import {
ResizableLayout,
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';
export const SIDEBAR_WIDTH_KEY = 'discover:sidebarWidth';
export const DiscoverResizableLayout = ({
container,
unifiedFieldListSidebarContainerApi,
sidebarPanel,
mainPanel,
}: {
container: HTMLElement | null;
unifiedFieldListSidebarContainerApi: UnifiedFieldListSidebarContainerApi | null;
sidebarPanel: ReactNode;
mainPanel: ReactNode;
}) => {
const [sidebarPanelNode] = useState(() =>
createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } })
);
const [mainPanelNode] = useState(() =>
createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } })
);
const { euiTheme } = useEuiTheme();
const minSidebarWidth = euiTheme.base * 13;
const defaultSidebarWidth = euiTheme.base * 19;
const minMainPanelWidth = euiTheme.base * 30;
const [sidebarWidth, setSidebarWidth] = useLocalStorage(SIDEBAR_WIDTH_KEY, defaultSidebarWidth);
const isSidebarCollapsed = useObservable(
unifiedFieldListSidebarContainerApi?.isSidebarCollapsed$ ?? of(true),
true
);
const isMobile = useIsWithinBreakpoints(['xs', 's']);
const layoutMode =
isMobile || isSidebarCollapsed ? ResizableLayoutMode.Static : ResizableLayoutMode.Resizable;
const layoutDirection = isMobile
? ResizableLayoutDirection.Vertical
: ResizableLayoutDirection.Horizontal;
return (
<>
<InPortal node={sidebarPanelNode}>{sidebarPanel}</InPortal>
<InPortal node={mainPanelNode}>{mainPanel}</InPortal>
<ResizableLayout
className="dscPageBody__contents"
mode={layoutMode}
direction={layoutDirection}
container={container}
fixedPanelSize={sidebarWidth ?? defaultSidebarWidth}
minFixedPanelSize={minSidebarWidth}
minFlexPanelSize={minMainPanelWidth}
fixedPanel={<OutPortal node={sidebarPanelNode} />}
flexPanel={<OutPortal node={mainPanelNode} />}
resizeButtonClassName="dscSidebarResizeButton"
data-test-subj="discoverLayout"
onFixedPanelSizeChange={setSidebarWidth}
/>
</>
);
};

View file

@ -13,7 +13,7 @@ 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 from 'react';
import React, { useState } from 'react';
import {
DiscoverSidebarResponsive,
DiscoverSidebarResponsiveProps,
@ -37,6 +37,7 @@ 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',
@ -168,6 +169,8 @@ function getCompProps(options?: { hits?: DataTableRecord[] }): DiscoverSidebarRe
trackUiMetric: jest.fn(),
onFieldEdited: jest.fn(),
onDataViewCreated: jest.fn(),
unifiedFieldListSidebarContainerApi: null,
setUnifiedFieldListSidebarContainerApi: jest.fn(),
};
}
@ -199,19 +202,30 @@ async function mountComponent(
mockedServices.data.query.getState = jest.fn().mockImplementation(() => appState.getState());
await act(async () => {
comp = await mountWithIntl(
const SidebarWrapper = () => {
const [api, setApi] = useState<UnifiedFieldListSidebarContainerApi | null>(null);
return (
<DiscoverSidebarResponsive
{...props}
unifiedFieldListSidebarContainerApi={api}
setUnifiedFieldListSidebarContainerApi={setApi}
/>
);
};
comp = mountWithIntl(
<KibanaContextProvider services={mockedServices}>
<DiscoverAppStateProvider value={appState}>
<DiscoverSidebarResponsive {...props} />
<SidebarWrapper />
</DiscoverAppStateProvider>
</KibanaContextProvider>
);
// wait for lazy modules
await new Promise((resolve) => setTimeout(resolve, 0));
await comp.update();
comp.update();
});
await comp!.update();
comp!.update();
return comp!;
}
@ -251,7 +265,7 @@ describe('discover responsive sidebar', function () {
await act(async () => {
// wait for lazy modules
await new Promise((resolve) => setTimeout(resolve, 0));
await compLoadingExistence.update();
compLoadingExistence.update();
});
expect(
@ -273,11 +287,11 @@ describe('discover responsive sidebar', function () {
indexPatternTitle: 'test-loaded',
existingFieldNames: Object.keys(mockfieldCounts),
});
await compLoadingExistence.update();
compLoadingExistence.update();
});
await act(async () => {
await compLoadingExistence.update();
compLoadingExistence.update();
});
expect(
@ -419,11 +433,11 @@ describe('discover responsive sidebar', function () {
const availableFields = findTestSubject(comp, 'fieldListGroupedAvailableFields');
await act(async () => {
const button = findTestSubject(availableFields, 'field-extension-showDetails');
await button.simulate('click');
await comp.update();
button.simulate('click');
comp.update();
});
await comp.update();
comp.update();
findTestSubject(comp, 'plus-extension-gif').simulate('click');
expect(props.onAddFilter).toHaveBeenCalled();
});
@ -432,11 +446,11 @@ describe('discover responsive sidebar', function () {
const availableFields = findTestSubject(comp, 'fieldListGroupedAvailableFields');
await act(async () => {
const button = findTestSubject(availableFields, 'field-extension-showDetails');
await button.simulate('click');
await comp.update();
button.simulate('click');
comp.update();
});
await comp.update();
comp.update();
findTestSubject(comp, 'discoverFieldListPanelAddExistFilter-extension').simulate('click');
expect(props.onAddFilter).toHaveBeenCalledWith('_exists_', 'extension', '+');
});
@ -450,7 +464,7 @@ describe('discover responsive sidebar', function () {
);
await act(async () => {
await findTestSubject(comp, 'fieldListFiltersFieldSearch').simulate('change', {
findTestSubject(comp, 'fieldListFiltersFieldSearch').simulate('change', {
target: { value: 'bytes' },
});
});
@ -471,16 +485,16 @@ describe('discover responsive sidebar', function () {
);
await act(async () => {
await findTestSubject(comp, 'fieldListFiltersFieldTypeFilterToggle').simulate('click');
findTestSubject(comp, 'fieldListFiltersFieldTypeFilterToggle').simulate('click');
});
await comp.update();
comp.update();
await act(async () => {
await findTestSubject(comp, 'typeFilter-number').simulate('click');
findTestSubject(comp, 'typeFilter-number').simulate('click');
});
await comp.update();
comp.update();
expect(findTestSubject(comp, 'fieldListGroupedAvailableFields-count').text()).toBe('2');
expect(findTestSubject(comp, 'fieldListGrouped__ariaDescription').text()).toBe(
@ -519,7 +533,7 @@ describe('discover responsive sidebar', function () {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
await compInTextBasedMode.update();
compInTextBasedMode.update();
});
expect(findTestSubject(compInTextBasedMode, 'indexPattern-add-field_btn').length).toBe(0);
@ -619,7 +633,7 @@ describe('discover responsive sidebar', function () {
);
const addFieldButton = findTestSubject(comp, 'dataView-add-field_btn');
expect(addFieldButton.length).toBe(1);
await addFieldButton.simulate('click');
addFieldButton.simulate('click');
expect(services.dataViewFieldEditor.openEditor).toHaveBeenCalledTimes(1);
});
@ -630,10 +644,10 @@ describe('discover responsive sidebar', function () {
await act(async () => {
findTestSubject(availableFields, 'field-bytes').simulate('click');
});
await comp.update();
comp.update();
const editFieldButton = findTestSubject(comp, 'discoverFieldListPanelEdit-bytes');
expect(editFieldButton.length).toBe(1);
await editFieldButton.simulate('click');
editFieldButton.simulate('click');
expect(services.dataViewFieldEditor.openEditor).toHaveBeenCalledTimes(1);
});
@ -662,12 +676,12 @@ describe('discover responsive sidebar', function () {
// open flyout
await act(async () => {
compWithPicker.find('.unifiedFieldListSidebar__mobileButton').last().simulate('click');
await compWithPicker.update();
compWithPicker.update();
});
await compWithPicker.update();
compWithPicker.update();
// open data view picker
await findTestSubject(compWithPicker, 'dataView-switch-link').simulate('click');
findTestSubject(compWithPicker, 'dataView-switch-link').simulate('click');
expect(findTestSubject(compWithPicker, 'changeDataViewPopover').length).toBe(1);
// check "Add a field"
const addFieldButtonInDataViewPicker = findTestSubject(
@ -678,7 +692,7 @@ describe('discover responsive sidebar', function () {
// click "Create a data view"
const createDataViewButton = findTestSubject(compWithPicker, 'dataview-create-new');
expect(createDataViewButton.length).toBe(1);
await createDataViewButton.simulate('click');
createDataViewButton.simulate('click');
expect(services.dataViewEditor.openEditor).toHaveBeenCalled();
});
@ -697,10 +711,10 @@ describe('discover responsive sidebar', function () {
.find('.unifiedFieldListSidebar__mobileButton')
.last()
.simulate('click');
await compWithPickerInViewerMode.update();
compWithPickerInViewerMode.update();
});
await compWithPickerInViewerMode.update();
compWithPickerInViewerMode.update();
// open data view picker
findTestSubject(compWithPickerInViewerMode, 'dataView-switch-link').simulate('click');
expect(findTestSubject(compWithPickerInViewerMode, 'changeDataViewPopover').length).toBe(1);
@ -724,10 +738,10 @@ describe('discover responsive sidebar', function () {
await act(async () => {
comp.find('.unifiedFieldListSidebar__mobileButton').last().simulate('click');
await comp.update();
comp.update();
});
await comp.update();
comp.update();
expect(comp.find('[data-test-subj="custom-data-view-picker"]').exists()).toBe(false);
});
@ -741,10 +755,10 @@ describe('discover responsive sidebar', function () {
await act(async () => {
comp.find('.unifiedFieldListSidebar__mobileButton').last().simulate('click');
await comp.update();
comp.update();
});
await comp.update();
comp.update();
expect(comp.find('[data-test-subj="custom-data-view-picker"]').exists()).toBe(true);
});

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useReducer, useRef } from 'react';
import { UiCounterMetricType } from '@kbn/analytics';
import { i18n } from '@kbn/i18n';
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
@ -133,6 +133,9 @@ export interface DiscoverSidebarResponsiveProps {
* For customization and testing purposes
*/
fieldListVariant?: UnifiedFieldListSidebarContainerProps['variant'];
unifiedFieldListSidebarContainerApi: UnifiedFieldListSidebarContainerApi | null;
setUnifiedFieldListSidebarContainerApi: (api: UnifiedFieldListSidebarContainerApi) => void;
}
/**
@ -153,6 +156,8 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
onChangeDataView,
onAddField,
onRemoveField,
unifiedFieldListSidebarContainerApi,
setUnifiedFieldListSidebarContainerApi,
} = props;
const [sidebarState, dispatchSidebarStateAction] = useReducer(
discoverSidebarReducer,
@ -161,8 +166,6 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
);
const selectedDataViewRef = useRef<DataView | null | undefined>(selectedDataView);
const showFieldList = sidebarState.status !== DiscoverSidebarReducerStatus.INITIAL;
const [unifiedFieldListSidebarContainerApi, setUnifiedFieldListSidebarContainerApi] =
useState<UnifiedFieldListSidebarContainerApi | null>(null);
useEffect(() => {
const subscription = props.documents$.subscribe((documentState) => {
@ -385,6 +388,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
allFields={sidebarState.allFields}
showFieldList={showFieldList}
workspaceSelectedFieldNames={columns}
fullWidth
onAddFieldToWorkspace={onAddFieldToWorkspace}
onRemoveFieldFromWorkspace={onRemoveFieldFromWorkspace}
onAddFilter={onAddFilter}

View file

@ -73,7 +73,8 @@
"@kbn/unified-data-table",
"@kbn/no-data-page-plugin",
"@kbn/rule-data-utils",
"@kbn/global-search-plugin"
"@kbn/global-search-plugin",
"@kbn/resizable-layout"
],
"exclude": [
"target/**/*"

View file

@ -33,7 +33,7 @@ describe('UnifiedHistogramContainer', () => {
requestAdapter={new RequestAdapter()}
searchSessionId={'123'}
timeRange={{ from: 'now-15m', to: 'now' }}
resizeRef={{ current: null }}
container={null}
/>
);
expect(component.update().isEmptyRender()).toBe(true);
@ -62,7 +62,7 @@ describe('UnifiedHistogramContainer', () => {
requestAdapter={new RequestAdapter()}
searchSessionId={'123'}
timeRange={{ from: 'now-15m', to: 'now' }}
resizeRef={{ current: null }}
container={null}
/>
);
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));

View file

@ -53,7 +53,7 @@ export type UnifiedHistogramContainerProps = {
| 'timeRange'
| 'relativeTimeRange'
| 'columns'
| 'resizeRef'
| 'container'
| 'appendHitsCounter'
| 'children'
| 'onBrushEnd'

View file

@ -13,7 +13,6 @@ import React from 'react';
import { act } from 'react-dom/test-utils';
import { of } from 'rxjs';
import { Chart } from '../chart';
import { Panels, PANELS_MODE } from '../panels';
import {
UnifiedHistogramChartContext,
UnifiedHistogramFetchStatus,
@ -22,6 +21,7 @@ import {
import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield';
import { unifiedHistogramServicesMock } from '../__mocks__/services';
import { UnifiedHistogramLayout, UnifiedHistogramLayoutProps } from './layout';
import { ResizableLayout, ResizableLayoutMode } from '@kbn/resizable-layout';
let mockBreakpoint = 'l';
@ -50,7 +50,7 @@ describe('Layout', () => {
services = unifiedHistogramServicesMock,
hits = createHits(),
chart = createChart(),
resizeRef = { current: null },
container = null,
...rest
}: Partial<Omit<UnifiedHistogramLayoutProps, 'hits' | 'chart'>> & {
hits?: UnifiedHistogramHitsContext | null;
@ -65,7 +65,7 @@ describe('Layout', () => {
services={services}
hits={hits ?? undefined}
chart={chart ?? undefined}
resizeRef={resizeRef}
container={container}
dataView={dataViewWithTimefieldMock}
query={{
language: 'kuery',
@ -95,66 +95,66 @@ describe('Layout', () => {
});
describe('PANELS_MODE', () => {
it('should set the panels mode to PANELS_MODE.RESIZABLE when viewing on medium screens and above', async () => {
it('should set the layout mode to ResizableLayoutMode.Resizable when viewing on medium screens and above', async () => {
const component = await mountComponent();
setBreakpoint(component, 'm');
expect(component.find(Panels).prop('mode')).toBe(PANELS_MODE.RESIZABLE);
expect(component.find(ResizableLayout).prop('mode')).toBe(ResizableLayoutMode.Resizable);
});
it('should set the panels mode to PANELS_MODE.FIXED when viewing on small screens and below', async () => {
it('should set the layout mode to ResizableLayoutMode.Static when viewing on small screens and below', async () => {
const component = await mountComponent();
setBreakpoint(component, 's');
expect(component.find(Panels).prop('mode')).toBe(PANELS_MODE.FIXED);
expect(component.find(ResizableLayout).prop('mode')).toBe(ResizableLayoutMode.Static);
});
it('should set the panels mode to PANELS_MODE.FIXED if chart.hidden is true', async () => {
it('should set the layout mode to ResizableLayoutMode.Static if chart.hidden is true', async () => {
const component = await mountComponent({
chart: {
...createChart(),
hidden: true,
},
});
expect(component.find(Panels).prop('mode')).toBe(PANELS_MODE.FIXED);
expect(component.find(ResizableLayout).prop('mode')).toBe(ResizableLayoutMode.Static);
});
it('should set the panels mode to PANELS_MODE.FIXED if chart is undefined', async () => {
it('should set the layout mode to ResizableLayoutMode.Static if chart is undefined', async () => {
const component = await mountComponent({ chart: null });
expect(component.find(Panels).prop('mode')).toBe(PANELS_MODE.FIXED);
expect(component.find(ResizableLayout).prop('mode')).toBe(ResizableLayoutMode.Static);
});
it('should set the panels mode to PANELS_MODE.SINGLE if chart and hits are undefined', async () => {
it('should set the layout mode to ResizableLayoutMode.Single if chart and hits are undefined', async () => {
const component = await mountComponent({ chart: null, hits: null });
expect(component.find(Panels).prop('mode')).toBe(PANELS_MODE.SINGLE);
expect(component.find(ResizableLayout).prop('mode')).toBe(ResizableLayoutMode.Single);
});
it('should set a fixed height for Chart when panels mode is PANELS_MODE.FIXED and chart.hidden is false', async () => {
it('should set a fixed height for Chart when layout mode is ResizableLayoutMode.Static and chart.hidden is false', async () => {
const component = await mountComponent();
setBreakpoint(component, 's');
const expectedHeight = component.find(Panels).prop('topPanelHeight');
const expectedHeight = component.find(ResizableLayout).prop('fixedPanelSize');
expect(component.find(Chart).find('div.euiFlexGroup').first().getDOMNode()).toHaveStyle({
height: `${expectedHeight}px`,
});
});
it('should not set a fixed height for Chart when panels mode is PANELS_MODE.FIXED and chart.hidden is true', async () => {
it('should not set a fixed height for Chart when layout mode is ResizableLayoutMode.Static and chart.hidden is true', async () => {
const component = await mountComponent({ chart: { ...createChart(), hidden: true } });
setBreakpoint(component, 's');
const expectedHeight = component.find(Panels).prop('topPanelHeight');
const expectedHeight = component.find(ResizableLayout).prop('fixedPanelSize');
expect(component.find(Chart).find('div.euiFlexGroup').first().getDOMNode()).not.toHaveStyle({
height: `${expectedHeight}px`,
});
});
it('should not set a fixed height for Chart when panels mode is PANELS_MODE.FIXED and chart is undefined', async () => {
it('should not set a fixed height for Chart when layout mode is ResizableLayoutMode.Static and chart is undefined', async () => {
const component = await mountComponent({ chart: null });
setBreakpoint(component, 's');
const expectedHeight = component.find(Panels).prop('topPanelHeight');
const expectedHeight = component.find(ResizableLayout).prop('fixedPanelSize');
expect(component.find(Chart).find('div.euiFlexGroup').first().getDOMNode()).not.toHaveStyle({
height: `${expectedHeight}px`,
});
});
it('should pass undefined for onResetChartHeight to Chart when panels mode is PANELS_MODE.FIXED', async () => {
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');
@ -163,28 +163,28 @@ describe('Layout', () => {
});
describe('topPanelHeight', () => {
it('should pass a default topPanelHeight to Panels when the topPanelHeight prop is undefined', async () => {
it('should pass a default fixedPanelSize to ResizableLayout when the topPanelHeight prop is undefined', async () => {
const component = await mountComponent({ topPanelHeight: undefined });
expect(component.find(Panels).prop('topPanelHeight')).toBeGreaterThan(0);
expect(component.find(ResizableLayout).prop('fixedPanelSize')).toBeGreaterThan(0);
});
it('should reset the topPanelHeight to the default when onResetChartHeight is called on Chart', async () => {
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(Panels).prop('topPanelHeight');
const defaultTopPanelHeight = component.find(ResizableLayout).prop('fixedPanelSize');
const newTopPanelHeight = 123;
expect(component.find(Panels).prop('topPanelHeight')).not.toBe(newTopPanelHeight);
expect(component.find(ResizableLayout).prop('fixedPanelSize')).not.toBe(newTopPanelHeight);
act(() => {
component.find(Panels).prop('onTopPanelHeightChange')!(newTopPanelHeight);
component.find(ResizableLayout).prop('onFixedPanelSizeChange')!(newTopPanelHeight);
});
expect(component.find(Panels).prop('topPanelHeight')).toBe(newTopPanelHeight);
expect(component.find(ResizableLayout).prop('fixedPanelSize')).toBe(newTopPanelHeight);
act(() => {
component.find(Chart).prop('onResetChartHeight')!();
});
expect(component.find(Panels).prop('topPanelHeight')).toBe(defaultTopPanelHeight);
expect(component.find(ResizableLayout).prop('fixedPanelSize')).toBe(defaultTopPanelHeight);
});
it('should pass undefined for onResetChartHeight to Chart when the chart is the default height', async () => {

View file

@ -7,8 +7,7 @@
*/
import { EuiSpacer, useEuiTheme, useIsWithinBreakpoints } from '@elastic/eui';
import { PropsWithChildren, ReactElement, RefObject } from 'react';
import React, { useMemo } from 'react';
import React, { PropsWithChildren, ReactElement, useMemo, useState } from 'react';
import { Observable } from 'rxjs';
import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
import { css } from '@emotion/css';
@ -21,9 +20,13 @@ import type {
LensSuggestionsApi,
Suggestion,
} from '@kbn/lens-plugin/public';
import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
import {
ResizableLayout,
ResizableLayoutMode,
ResizableLayoutDirection,
} from '@kbn/resizable-layout';
import { Chart } from '../chart';
import { Panels, PANELS_MODE } from '../panels';
import type {
UnifiedHistogramChartContext,
UnifiedHistogramServices,
@ -96,9 +99,9 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren<unknown>
*/
breakdown?: UnifiedHistogramBreakdownContext;
/**
* Ref to the element wrapping the layout which will be used for resize calculations
* The parent container element, used to calculate the layout size
*/
resizeRef: RefObject<HTMLDivElement>;
container: HTMLElement | null;
/**
* Current top panel height -- leave undefined to use the default
*/
@ -192,7 +195,7 @@ export const UnifiedHistogramLayout = ({
lensEmbeddableOutput$,
chart: originalChart,
breakdown,
resizeRef,
container,
topPanelHeight,
appendHitsCounter,
disableAutoFetching,
@ -226,14 +229,11 @@ export const UnifiedHistogramLayout = ({
});
const chart = suggestionUnsupported ? undefined : originalChart;
const topPanelNode = useMemo(
() => createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } }),
[]
const [topPanelNode] = useState(() =>
createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } })
);
const mainPanelNode = useMemo(
() => createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } }),
[]
const [mainPanelNode] = useState(() =>
createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } })
);
const isMobile = useIsWithinBreakpoints(['xs', 's']);
@ -252,14 +252,15 @@ export const UnifiedHistogramLayout = ({
const panelsMode =
chart || hits
? showFixedPanels
? PANELS_MODE.FIXED
: PANELS_MODE.RESIZABLE
: PANELS_MODE.SINGLE;
? ResizableLayoutMode.Static
: ResizableLayoutMode.Resizable
: ResizableLayoutMode.Single;
const currentTopPanelHeight = topPanelHeight ?? defaultTopPanelHeight;
const onResetChartHeight = useMemo(() => {
return currentTopPanelHeight !== defaultTopPanelHeight && panelsMode === PANELS_MODE.RESIZABLE
return currentTopPanelHeight !== defaultTopPanelHeight &&
panelsMode === ResizableLayoutMode.Resizable
? () => onTopPanelHeightChange?.(undefined)
: undefined;
}, [currentTopPanelHeight, defaultTopPanelHeight, onTopPanelHeightChange, panelsMode]);
@ -305,16 +306,18 @@ export const UnifiedHistogramLayout = ({
/>
</InPortal>
<InPortal node={mainPanelNode}>{children}</InPortal>
<Panels
<ResizableLayout
className={className}
mode={panelsMode}
resizeRef={resizeRef}
topPanelHeight={currentTopPanelHeight}
minTopPanelHeight={defaultTopPanelHeight}
minMainPanelHeight={minMainPanelHeight}
topPanel={<OutPortal node={topPanelNode} />}
mainPanel={<OutPortal node={mainPanelNode} />}
onTopPanelHeightChange={onTopPanelHeightChange}
direction={ResizableLayoutDirection.Vertical}
container={container}
fixedPanelSize={currentTopPanelHeight}
minFixedPanelSize={defaultTopPanelHeight}
minFlexPanelSize={minMainPanelHeight}
fixedPanel={<OutPortal node={topPanelNode} />}
flexPanel={<OutPortal node={mainPanelNode} />}
data-test-subj="unifiedHistogram"
onFixedPanelSizeChange={onTopPanelHeightChange}
/>
</>
);

View file

@ -1,95 +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 { mount } from 'enzyme';
import type { ReactElement, RefObject } from 'react';
import React from 'react';
import { Panels, PANELS_MODE } from './panels';
import { PanelsResizable } from './panels_resizable';
import { PanelsFixed } from './panels_fixed';
describe('Panels component', () => {
const mountComponent = ({
mode = PANELS_MODE.RESIZABLE,
resizeRef = { current: null },
initialTopPanelHeight = 200,
minTopPanelHeight = 100,
minMainPanelHeight = 100,
topPanel = <></>,
mainPanel = <></>,
}: {
mode?: PANELS_MODE;
resizeRef?: RefObject<HTMLDivElement>;
initialTopPanelHeight?: number;
minTopPanelHeight?: number;
minMainPanelHeight?: number;
mainPanel?: ReactElement;
topPanel?: ReactElement;
}) => {
return mount(
<Panels
mode={mode}
resizeRef={resizeRef}
topPanelHeight={initialTopPanelHeight}
minTopPanelHeight={minTopPanelHeight}
minMainPanelHeight={minMainPanelHeight}
topPanel={topPanel}
mainPanel={mainPanel}
onTopPanelHeightChange={jest.fn()}
/>
);
};
it('should show PanelsFixed when mode is PANELS_MODE.SINGLE', () => {
const topPanel = <div data-test-subj="topPanel" />;
const mainPanel = <div data-test-subj="mainPanel" />;
const component = mountComponent({ mode: PANELS_MODE.SINGLE, topPanel, mainPanel });
expect(component.find(PanelsFixed).exists()).toBe(true);
expect(component.find(PanelsResizable).exists()).toBe(false);
expect(component.contains(topPanel)).toBe(false);
expect(component.contains(mainPanel)).toBe(true);
});
it('should show PanelsFixed when mode is PANELS_MODE.FIXED', () => {
const topPanel = <div data-test-subj="topPanel" />;
const mainPanel = <div data-test-subj="mainPanel" />;
const component = mountComponent({ mode: PANELS_MODE.FIXED, topPanel, mainPanel });
expect(component.find(PanelsFixed).exists()).toBe(true);
expect(component.find(PanelsResizable).exists()).toBe(false);
expect(component.contains(topPanel)).toBe(true);
expect(component.contains(mainPanel)).toBe(true);
});
it('should show PanelsResizable when mode is PANELS_MODE.RESIZABLE', () => {
const topPanel = <div data-test-subj="topPanel" />;
const mainPanel = <div data-test-subj="mainPanel" />;
const component = mountComponent({ mode: PANELS_MODE.RESIZABLE, topPanel, mainPanel });
expect(component.find(PanelsFixed).exists()).toBe(false);
expect(component.find(PanelsResizable).exists()).toBe(true);
expect(component.contains(topPanel)).toBe(true);
expect(component.contains(mainPanel)).toBe(true);
});
it('should pass true for hideTopPanel when mode is PANELS_MODE.SINGLE', () => {
const topPanel = <div data-test-subj="topPanel" />;
const mainPanel = <div data-test-subj="mainPanel" />;
const component = mountComponent({ mode: PANELS_MODE.SINGLE, topPanel, mainPanel });
expect(component.find(PanelsFixed).prop('hideTopPanel')).toBe(true);
expect(component.contains(topPanel)).toBe(false);
expect(component.contains(mainPanel)).toBe(true);
});
it('should pass false for hideTopPanel when mode is PANELS_MODE.FIXED', () => {
const topPanel = <div data-test-subj="topPanel" />;
const mainPanel = <div data-test-subj="mainPanel" />;
const component = mountComponent({ mode: PANELS_MODE.FIXED, topPanel, mainPanel });
expect(component.find(PanelsFixed).prop('hideTopPanel')).toBe(false);
expect(component.contains(topPanel)).toBe(true);
expect(component.contains(mainPanel)).toBe(true);
});
});

View file

@ -1,59 +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, RefObject } from 'react';
import React from 'react';
import { PanelsResizable } from './panels_resizable';
import { PanelsFixed } from './panels_fixed';
export enum PANELS_MODE {
SINGLE = 'single',
FIXED = 'fixed',
RESIZABLE = 'resizable',
}
export interface PanelsProps {
className?: string;
mode: PANELS_MODE;
resizeRef: RefObject<HTMLDivElement>;
topPanelHeight: number;
minTopPanelHeight: number;
minMainPanelHeight: number;
topPanel: ReactElement;
mainPanel: ReactElement;
onTopPanelHeightChange?: (topPanelHeight: number) => void;
}
const fixedModes = [PANELS_MODE.SINGLE, PANELS_MODE.FIXED];
export const Panels = ({
className,
mode,
resizeRef,
topPanelHeight,
minTopPanelHeight,
minMainPanelHeight,
topPanel,
mainPanel,
onTopPanelHeightChange,
}: PanelsProps) => {
const panelsProps = { className, topPanel, mainPanel };
return fixedModes.includes(mode) ? (
<PanelsFixed hideTopPanel={mode === PANELS_MODE.SINGLE} {...panelsProps} />
) : (
<PanelsResizable
resizeRef={resizeRef}
topPanelHeight={topPanelHeight}
minTopPanelHeight={minTopPanelHeight}
minMainPanelHeight={minMainPanelHeight}
onTopPanelHeightChange={onTopPanelHeightChange}
{...panelsProps}
/>
);
};

View file

@ -1,44 +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 { mount } from 'enzyme';
import type { ReactElement } from 'react';
import React from 'react';
import { PanelsFixed } from './panels_fixed';
describe('Panels fixed', () => {
const mountComponent = ({
hideTopPanel = false,
topPanel = <></>,
mainPanel = <></>,
}: {
hideTopPanel?: boolean;
topPanel: ReactElement;
mainPanel: ReactElement;
}) => {
return mount(
<PanelsFixed hideTopPanel={hideTopPanel} topPanel={topPanel} mainPanel={mainPanel} />
);
};
it('should render both panels when hideTopPanel is false', () => {
const topPanel = <div data-test-subj="topPanel" />;
const mainPanel = <div data-test-subj="mainPanel" />;
const component = mountComponent({ topPanel, mainPanel });
expect(component.contains(topPanel)).toBe(true);
expect(component.contains(mainPanel)).toBe(true);
});
it('should render only main panel when hideTopPanel is true', () => {
const topPanel = <div data-test-subj="topPanel" />;
const mainPanel = <div data-test-subj="mainPanel" />;
const component = mountComponent({ hideTopPanel: true, topPanel, mainPanel });
expect(component.contains(topPanel)).toBe(false);
expect(component.contains(mainPanel)).toBe(true);
});
});

View file

@ -1,197 +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 { ReactWrapper } from 'enzyme';
import { mount } from 'enzyme';
import type { ReactElement, RefObject } from 'react';
import React from 'react';
import { PanelsResizable } from './panels_resizable';
import { act } from 'react-dom/test-utils';
const containerHeight = 1000;
const topPanelId = 'topPanel';
jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'),
useResizeObserver: jest.fn(),
useGeneratedHtmlId: jest.fn(() => topPanelId),
}));
import * as eui from '@elastic/eui';
import { waitFor } from '@testing-library/dom';
describe('Panels resizable', () => {
const mountComponent = ({
className = '',
resizeRef = { current: null },
initialTopPanelHeight = 0,
minTopPanelHeight = 0,
minMainPanelHeight = 0,
topPanel = <></>,
mainPanel = <></>,
attachTo,
onTopPanelHeightChange = jest.fn(),
}: {
className?: string;
resizeRef?: RefObject<HTMLDivElement>;
initialTopPanelHeight?: number;
minTopPanelHeight?: number;
minMainPanelHeight?: number;
topPanel?: ReactElement;
mainPanel?: ReactElement;
attachTo?: HTMLElement;
onTopPanelHeightChange?: (topPanelHeight: number) => void;
}) => {
return mount(
<PanelsResizable
className={className}
resizeRef={resizeRef}
topPanelHeight={initialTopPanelHeight}
minTopPanelHeight={minTopPanelHeight}
minMainPanelHeight={minMainPanelHeight}
topPanel={topPanel}
mainPanel={mainPanel}
onTopPanelHeightChange={onTopPanelHeightChange}
/>,
attachTo ? { attachTo } : undefined
);
};
const expectCorrectPanelSizes = (
component: ReactWrapper,
currentContainerHeight: number,
topPanelHeight: number
) => {
const topPanelSize = (topPanelHeight / currentContainerHeight) * 100;
expect(
component.find('[data-test-subj="unifiedHistogramResizablePanelTop"]').at(0).prop('size')
).toBe(topPanelSize);
expect(
component.find('[data-test-subj="unifiedHistogramResizablePanelMain"]').at(0).prop('size')
).toBe(100 - topPanelSize);
};
const forceRender = (component: ReactWrapper) => {
component.setProps({}).update();
};
beforeEach(() => {
jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: containerHeight, width: 0 });
});
it('should render both panels', () => {
const topPanel = <div data-test-subj="topPanel" />;
const mainPanel = <div data-test-subj="mainPanel" />;
const component = mountComponent({ topPanel, mainPanel });
expect(component.contains(topPanel)).toBe(true);
expect(component.contains(mainPanel)).toBe(true);
});
it('should set the initial heights of both panels', () => {
const initialTopPanelHeight = 200;
const component = mountComponent({ initialTopPanelHeight });
expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight);
});
it('should set the correct heights of both panels when the panels are resized', () => {
const initialTopPanelHeight = 200;
const onTopPanelHeightChange = jest.fn((topPanelHeight) => {
component.setProps({ topPanelHeight }).update();
});
const component = mountComponent({ initialTopPanelHeight, onTopPanelHeightChange });
expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight);
const newTopPanelSize = 30;
const onPanelSizeChange = component
.find('[data-test-subj="unifiedHistogramResizableContainer"]')
.at(0)
.prop('onPanelWidthChange') as Function;
act(() => {
onPanelSizeChange({ [topPanelId]: newTopPanelSize });
});
forceRender(component);
const newTopPanelHeight = (newTopPanelSize / 100) * containerHeight;
expect(onTopPanelHeightChange).toHaveBeenCalledWith(newTopPanelHeight);
expectCorrectPanelSizes(component, containerHeight, newTopPanelHeight);
});
it('should maintain the height of the top panel and resize the main panel when the container height changes', () => {
const initialTopPanelHeight = 200;
const component = mountComponent({ initialTopPanelHeight });
expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight);
const newContainerHeight = 2000;
jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: newContainerHeight, width: 0 });
forceRender(component);
expectCorrectPanelSizes(component, newContainerHeight, initialTopPanelHeight);
});
it('should resize the top panel once the main panel is at its minimum height', () => {
const initialTopPanelHeight = 500;
const minTopPanelHeight = 100;
const minMainPanelHeight = 100;
const component = mountComponent({
initialTopPanelHeight,
minTopPanelHeight,
minMainPanelHeight,
});
expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight);
const newContainerHeight = 400;
jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: newContainerHeight, width: 0 });
forceRender(component);
expectCorrectPanelSizes(component, newContainerHeight, newContainerHeight - minMainPanelHeight);
jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: containerHeight, width: 0 });
forceRender(component);
expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight);
});
it('should maintain the minimum heights of both panels when the container is too small to fit them', () => {
const initialTopPanelHeight = 500;
const minTopPanelHeight = 100;
const minMainPanelHeight = 150;
const component = mountComponent({
initialTopPanelHeight,
minTopPanelHeight,
minMainPanelHeight,
});
expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight);
const newContainerHeight = 200;
jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: newContainerHeight, width: 0 });
forceRender(component);
expect(
component.find('[data-test-subj="unifiedHistogramResizablePanelTop"]').at(0).prop('size')
).toBe((minTopPanelHeight / newContainerHeight) * 100);
expect(
component.find('[data-test-subj="unifiedHistogramResizablePanelMain"]').at(0).prop('size')
).toBe((minMainPanelHeight / newContainerHeight) * 100);
jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: containerHeight, width: 0 });
forceRender(component);
expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight);
});
it('should blur the resize button after a resize', async () => {
const attachTo = document.createElement('div');
document.body.appendChild(attachTo);
const component = mountComponent({ attachTo });
const getContainer = () =>
component.find('[data-test-subj="unifiedHistogramResizableContainer"]').at(0);
const resizeButton = component.find('button[data-test-subj="unifiedHistogramResizableButton"]');
act(() => {
const onResizeStart = getContainer().prop('onResizeStart') as Function;
onResizeStart('pointer');
});
(resizeButton.getDOMNode() as HTMLElement).focus();
forceRender(component);
act(() => {
const onResizeEnd = getContainer().prop('onResizeEnd') as Function;
onResizeEnd();
});
expect(resizeButton.getDOMNode()).toHaveFocus();
await waitFor(() => {
expect(resizeButton.getDOMNode()).not.toHaveFocus();
});
});
});

View file

@ -1,197 +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 { EuiResizableContainer, useGeneratedHtmlId, useResizeObserver } from '@elastic/eui';
import type { ResizeTrigger } from '@elastic/eui/src/components/resizable_container/types';
import { css } from '@emotion/react';
import { isEqual, round } from 'lodash';
import type { ReactElement, RefObject } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
const percentToPixels = (containerHeight: number, percentage: number) =>
Math.round(containerHeight * (percentage / 100));
const pixelsToPercent = (containerHeight: number, pixels: number) =>
(pixels / containerHeight) * 100;
export const PanelsResizable = ({
className,
resizeRef,
topPanelHeight,
minTopPanelHeight,
minMainPanelHeight,
topPanel,
mainPanel,
onTopPanelHeightChange,
}: {
className?: string;
resizeRef: RefObject<HTMLDivElement>;
topPanelHeight: number;
minTopPanelHeight: number;
minMainPanelHeight: number;
topPanel: ReactElement;
mainPanel: ReactElement;
onTopPanelHeightChange?: (topPanelHeight: number) => void;
}) => {
const topPanelId = useGeneratedHtmlId({ prefix: 'topPanel' });
const { height: containerHeight } = useResizeObserver(resizeRef.current);
const [panelSizes, setPanelSizes] = useState({ topPanelSize: 0, mainPanelSize: 0 });
// EuiResizableContainer doesn't work properly when used with react-reverse-portal and
// will cancel the resize. To work around this we keep track of when resizes start and
// end to toggle the rendering of a transparent overlay which prevents the cancellation.
// EUI issue: https://github.com/elastic/eui/issues/6199
const [resizeWithPortalsHackIsResizing, setResizeWithPortalsHackIsResizing] = useState(false);
const enableResizeWithPortalsHack = useCallback(
() => setResizeWithPortalsHackIsResizing(true),
[]
);
const disableResizeWithPortalsHack = useCallback(
() => setResizeWithPortalsHackIsResizing(false),
[]
);
const resizeWithPortalsHackButtonCss = css`
z-index: 3;
`;
const resizeWithPortalsHackOverlayCss = css`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2;
`;
// We convert the top panel height from a percentage of the container height
// to a pixel value and emit the change to the parent component. We also convert
// the pixel value back to a percentage before updating the panel sizes to avoid
// rounding issues with the isEqual check in the effect below.
const onPanelSizeChange = useCallback(
({ [topPanelId]: topPanelSize }: { [key: string]: number }) => {
const newTopPanelHeight = percentToPixels(containerHeight, topPanelSize);
const newTopPanelSize = pixelsToPercent(containerHeight, newTopPanelHeight);
setPanelSizes({
topPanelSize: round(newTopPanelSize, 4),
mainPanelSize: round(100 - newTopPanelSize, 4),
});
onTopPanelHeightChange?.(newTopPanelHeight);
},
[containerHeight, onTopPanelHeightChange, topPanelId]
);
// This effect will update the panel sizes based on the top panel height whenever
// it or the container height changes. This allows us to keep the height of the
// top panel fixed when the window is resized.
useEffect(() => {
if (!containerHeight) {
return;
}
let topPanelSize: number;
let mainPanelSize: number;
// If the container height is less than the minimum main content height
// plus the current top panel height, then we need to make some adjustments.
if (containerHeight < minMainPanelHeight + topPanelHeight) {
const newTopPanelHeight = containerHeight - minMainPanelHeight;
// Try to make the top panel height fit within the container, but if it
// doesn't then just use the minimum heights.
if (newTopPanelHeight < minTopPanelHeight) {
topPanelSize = pixelsToPercent(containerHeight, minTopPanelHeight);
mainPanelSize = pixelsToPercent(containerHeight, minMainPanelHeight);
} else {
topPanelSize = pixelsToPercent(containerHeight, newTopPanelHeight);
mainPanelSize = 100 - topPanelSize;
}
} else {
topPanelSize = pixelsToPercent(containerHeight, topPanelHeight);
mainPanelSize = 100 - topPanelSize;
}
const newPanelSizes = {
topPanelSize: round(topPanelSize, 4),
mainPanelSize: round(mainPanelSize, 4),
};
// Skip updating the panel sizes if they haven't changed
// since onPanelSizeChange will also trigger this effect.
if (!isEqual(panelSizes, newPanelSizes)) {
setPanelSizes(newPanelSizes);
}
}, [containerHeight, minMainPanelHeight, minTopPanelHeight, panelSizes, topPanelHeight]);
const onResizeStart = useCallback(
(trigger: ResizeTrigger) => {
if (trigger !== 'pointer') {
return;
}
enableResizeWithPortalsHack();
},
[enableResizeWithPortalsHack]
);
const onResizeEnd = useCallback(() => {
if (!resizeWithPortalsHackIsResizing) {
return;
}
// We don't want the resize button to retain focus after the resize is complete,
// but EuiResizableContainer will force focus it onClick. To work around this we
// use setTimeout to wait until after onClick has been called before blurring.
if (document.activeElement instanceof HTMLElement) {
const button = document.activeElement;
setTimeout(() => {
button.blur();
});
}
disableResizeWithPortalsHack();
}, [disableResizeWithPortalsHack, resizeWithPortalsHackIsResizing]);
return (
<EuiResizableContainer
className={className}
direction="vertical"
onPanelWidthChange={onPanelSizeChange}
onResizeStart={onResizeStart}
onResizeEnd={onResizeEnd}
data-test-subj="unifiedHistogramResizableContainer"
>
{(EuiResizablePanel, EuiResizableButton) => (
<>
<EuiResizablePanel
id={topPanelId}
minSize={`${minTopPanelHeight}px`}
size={panelSizes.topPanelSize}
paddingSize="none"
data-test-subj="unifiedHistogramResizablePanelTop"
>
{topPanel}
</EuiResizablePanel>
<EuiResizableButton
css={resizeWithPortalsHackButtonCss}
data-test-subj="unifiedHistogramResizableButton"
/>
<EuiResizablePanel
minSize={`${minMainPanelHeight}px`}
size={panelSizes.mainPanelSize}
paddingSize="none"
data-test-subj="unifiedHistogramResizablePanelMain"
>
{mainPanel}
</EuiResizablePanel>
{resizeWithPortalsHackIsResizing ? <div css={resizeWithPortalsHackOverlayCss} /> : <></>}
</>
)}
</EuiResizableContainer>
);
};

View file

@ -25,6 +25,7 @@
"@kbn/kibana-utils-plugin",
"@kbn/visualizations-plugin",
"@kbn/discover-utils",
"@kbn/resizable-layout",
],
"exclude": [
"target/**/*",

View file

@ -31,8 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
defaultIndex: 'logstash-*',
};
// FLAKY: https://github.com/elastic/kibana/issues/146223
describe.skip('discover test', function describeIndexTests() {
describe('discover test', function describeIndexTests() {
before(async function () {
log.debug('load kibana index with default index pattern');
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover');
@ -112,7 +111,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
});
it('should show correct initial chart interval of Auto', async function () {
// FLAKY: https://github.com/elastic/kibana/issues/146223
it.skip('should show correct initial chart interval of Auto', async function () {
await PageObjects.timePicker.setDefaultAbsoluteRange();
await PageObjects.discover.waitUntilSearchingHasFinished();
const actualInterval = await PageObjects.discover.getChartInterval();
@ -127,6 +127,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should reload the saved search with persisted query to show the initial hit count', async function () {
await PageObjects.timePicker.setDefaultAbsoluteRange();
await PageObjects.discover.waitUntilSearchingHasFinished();
// apply query some changes
await queryBar.setQuery('test');
await queryBar.submitQuery();
@ -298,10 +300,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
describe('resizable layout panels', () => {
it('should allow resizing the layout panels', async () => {
it('should allow resizing the histogram layout panels', async () => {
const resizeDistance = 100;
const topPanel = await testSubjects.find('unifiedHistogramResizablePanelTop');
const mainPanel = await testSubjects.find('unifiedHistogramResizablePanelMain');
const topPanel = await testSubjects.find('unifiedHistogramResizablePanelFixed');
const mainPanel = await testSubjects.find('unifiedHistogramResizablePanelFlex');
const resizeButton = await testSubjects.find('unifiedHistogramResizableButton');
const topPanelSize = (await topPanel.getPosition()).height;
const mainPanelSize = (await mainPanel.getPosition()).height;
@ -314,6 +316,23 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(newTopPanelSize).to.be(topPanelSize + resizeDistance);
expect(newMainPanelSize).to.be(mainPanelSize - resizeDistance);
});
it('should allow resizing the sidebar layout panels', async () => {
const resizeDistance = 100;
const leftPanel = await testSubjects.find('discoverLayoutResizablePanelFixed');
const mainPanel = await testSubjects.find('discoverLayoutResizablePanelFlex');
const resizeButton = await testSubjects.find('discoverLayoutResizableButton');
const leftPanelSize = (await leftPanel.getPosition()).width;
const mainPanelSize = (await mainPanel.getPosition()).width;
await browser.dragAndDrop(
{ location: resizeButton },
{ location: { x: resizeDistance, y: 0 } }
);
const newLeftPanelSize = (await leftPanel.getPosition()).width;
const newMainPanelSize = (await mainPanel.getPosition()).width;
expect(newLeftPanelSize).to.be(leftPanelSize + resizeDistance);
expect(newMainPanelSize).to.be(mainPanelSize - resizeDistance);
});
});
describe('URL state', () => {

View file

@ -155,7 +155,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should see scripted field value in Discover', async function () {
await PageObjects.common.navigateToApp('discover');
await PageObjects.unifiedFieldList.clickFieldListItem(scriptedPainlessFieldName);
await retry.try(async function () {
await PageObjects.unifiedFieldList.clickFieldListItemAdd(scriptedPainlessFieldName);
});
@ -261,7 +260,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should see scripted field value in Discover', async function () {
await PageObjects.common.navigateToApp('discover');
await PageObjects.unifiedFieldList.clickFieldListItem(scriptedPainlessFieldName2);
await retry.try(async function () {
await PageObjects.unifiedFieldList.clickFieldListItemAdd(scriptedPainlessFieldName2);
});
@ -366,7 +364,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should see scripted field value in Discover', async function () {
await PageObjects.common.navigateToApp('discover');
await PageObjects.unifiedFieldList.clickFieldListItem(scriptedPainlessFieldName2);
await retry.try(async function () {
await PageObjects.unifiedFieldList.clickFieldListItemAdd(scriptedPainlessFieldName2);
});
@ -464,7 +461,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should see scripted field value in Discover', async function () {
await PageObjects.common.navigateToApp('discover');
await PageObjects.unifiedFieldList.clickFieldListItem(scriptedPainlessFieldName2);
await retry.try(async function () {
await PageObjects.unifiedFieldList.clickFieldListItemAdd(scriptedPainlessFieldName2);
});

View file

@ -143,7 +143,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.common.navigateToApp('discover');
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
await PageObjects.unifiedFieldList.clickFieldListItem(scriptedPainlessFieldName);
await retry.try(async function () {
await PageObjects.unifiedFieldList.clickFieldListItemAdd(scriptedPainlessFieldName);
});
@ -233,7 +232,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.common.navigateToApp('discover');
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
await PageObjects.unifiedFieldList.clickFieldListItem(scriptedPainlessFieldName2);
await retry.try(async function () {
await PageObjects.unifiedFieldList.clickFieldListItemAdd(scriptedPainlessFieldName2);
});
@ -322,7 +320,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.common.navigateToApp('discover');
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
await PageObjects.unifiedFieldList.clickFieldListItem(scriptedPainlessFieldName2);
await retry.try(async function () {
await PageObjects.unifiedFieldList.clickFieldListItemAdd(scriptedPainlessFieldName2);
});
@ -412,7 +409,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.common.navigateToApp('discover');
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
await PageObjects.unifiedFieldList.clickFieldListItem(scriptedPainlessFieldName2);
await retry.try(async function () {
await PageObjects.unifiedFieldList.clickFieldListItemAdd(scriptedPainlessFieldName2);
});

View file

@ -1156,6 +1156,10 @@
"@kbn/reporting-example-plugin/*": ["x-pack/examples/reporting_example/*"],
"@kbn/reporting-plugin": ["x-pack/plugins/reporting"],
"@kbn/reporting-plugin/*": ["x-pack/plugins/reporting/*"],
"@kbn/resizable-layout": ["packages/kbn-resizable-layout"],
"@kbn/resizable-layout/*": ["packages/kbn-resizable-layout/*"],
"@kbn/resizable-layout-examples-plugin": ["examples/resizable_layout_examples"],
"@kbn/resizable-layout-examples-plugin/*": ["examples/resizable_layout_examples/*"],
"@kbn/resolver-test-plugin": ["x-pack/test/plugin_functional/plugins/resolver_test"],
"@kbn/resolver-test-plugin/*": ["x-pack/test/plugin_functional/plugins/resolver_test/*"],
"@kbn/response-stream-plugin": ["examples/response_stream"],

View file

@ -5255,6 +5255,14 @@
version "0.0.0"
uid ""
"@kbn/resizable-layout-examples-plugin@link:examples/resizable_layout_examples":
version "0.0.0"
uid ""
"@kbn/resizable-layout@link:packages/kbn-resizable-layout":
version "0.0.0"
uid ""
"@kbn/resolver-test-plugin@link:x-pack/test/plugin_functional/plugins/resolver_test":
version "0.0.0"
uid ""