mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[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.

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:
parent
272219ca49
commit
3e1865513d
51 changed files with 1822 additions and 789 deletions
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
|
@ -581,6 +581,8 @@ packages/kbn-repo-source-classifier-cli @elastic/kibana-operations
|
||||||
packages/kbn-reporting/common @elastic/appex-sharedux
|
packages/kbn-reporting/common @elastic/appex-sharedux
|
||||||
x-pack/examples/reporting_example @elastic/appex-sharedux
|
x-pack/examples/reporting_example @elastic/appex-sharedux
|
||||||
x-pack/plugins/reporting @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
|
x-pack/test/plugin_functional/plugins/resolver_test @elastic/security-solution
|
||||||
examples/response_stream @elastic/ml-ui
|
examples/response_stream @elastic/ml-ui
|
||||||
packages/kbn-rison @elastic/kibana-operations
|
packages/kbn-rison @elastic/kibana-operations
|
||||||
|
|
11
examples/resizable_layout_examples/kibana.jsonc
Normal file
11
examples/resizable_layout_examples/kibana.jsonc
Normal 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"]
|
||||||
|
}
|
||||||
|
}
|
161
examples/resizable_layout_examples/public/application.tsx
Normal file
161
examples/resizable_layout_examples/public/application.tsx
Normal 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);
|
||||||
|
};
|
||||||
|
};
|
13
examples/resizable_layout_examples/public/index.ts
Normal file
13
examples/resizable_layout_examples/public/index.ts
Normal 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();
|
||||||
|
}
|
44
examples/resizable_layout_examples/public/plugin.tsx
Normal file
44
examples/resizable_layout_examples/public/plugin.tsx
Normal 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 |
15
examples/resizable_layout_examples/tsconfig.json
Normal file
15
examples/resizable_layout_examples/tsconfig.json
Normal 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/**/*"]
|
||||||
|
}
|
|
@ -588,6 +588,8 @@
|
||||||
"@kbn/reporting-common": "link:packages/kbn-reporting/common",
|
"@kbn/reporting-common": "link:packages/kbn-reporting/common",
|
||||||
"@kbn/reporting-example-plugin": "link:x-pack/examples/reporting_example",
|
"@kbn/reporting-example-plugin": "link:x-pack/examples/reporting_example",
|
||||||
"@kbn/reporting-plugin": "link:x-pack/plugins/reporting",
|
"@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/resolver-test-plugin": "link:x-pack/test/plugin_functional/plugins/resolver_test",
|
||||||
"@kbn/response-stream-plugin": "link:examples/response_stream",
|
"@kbn/response-stream-plugin": "link:examples/response_stream",
|
||||||
"@kbn/rison": "link:packages/kbn-rison",
|
"@kbn/rison": "link:packages/kbn-rison",
|
||||||
|
|
85
packages/kbn-resizable-layout/README.md
Normal file
85
packages/kbn-resizable-layout/README.md
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
14
packages/kbn-resizable-layout/index.ts
Normal file
14
packages/kbn-resizable-layout/index.ts
Normal 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')));
|
|
@ -6,4 +6,8 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { Panels, PANELS_MODE } from './panels';
|
module.exports = {
|
||||||
|
preset: '@kbn/test',
|
||||||
|
rootDir: '../..',
|
||||||
|
roots: ['<rootDir>/packages/kbn-resizable-layout'],
|
||||||
|
};
|
6
packages/kbn-resizable-layout/kibana.jsonc
Normal file
6
packages/kbn-resizable-layout/kibana.jsonc
Normal 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"
|
||||||
|
}
|
6
packages/kbn-resizable-layout/package.json
Normal file
6
packages/kbn-resizable-layout/package.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "@kbn/resizable-layout",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||||
|
}
|
246
packages/kbn-resizable-layout/src/panels_resizable.test.tsx
Normal file
246
packages/kbn-resizable-layout/src/panels_resizable.test.tsx
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
228
packages/kbn-resizable-layout/src/panels_resizable.tsx
Normal file
228
packages/kbn-resizable-layout/src/panels_resizable.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
71
packages/kbn-resizable-layout/src/panels_static.test.tsx
Normal file
71
packages/kbn-resizable-layout/src/panels_static.test.tsx
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -10,37 +10,43 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { ResizableLayoutDirection } from '../types';
|
||||||
|
|
||||||
export const PanelsFixed = ({
|
export const PanelsStatic = ({
|
||||||
className,
|
className,
|
||||||
hideTopPanel,
|
direction,
|
||||||
topPanel,
|
hideFixedPanel,
|
||||||
mainPanel,
|
fixedPanel,
|
||||||
|
flexPanel,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
hideTopPanel?: boolean;
|
direction: ResizableLayoutDirection;
|
||||||
topPanel: ReactElement;
|
hideFixedPanel?: boolean;
|
||||||
mainPanel: ReactElement;
|
fixedPanel: ReactElement;
|
||||||
|
flexPanel: ReactElement;
|
||||||
}) => {
|
}) => {
|
||||||
// By default a flex item has overflow: visible, min-height: auto, and min-width: auto.
|
// 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.
|
// 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
|
// 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.
|
// 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
|
// https://stackoverflow.com/questions/36247140/why-dont-flex-items-shrink-past-content-size
|
||||||
const mainPanelCss = css`
|
const flexPanelCss = css`
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiFlexGroup
|
<EuiFlexGroup
|
||||||
className={className}
|
className={className}
|
||||||
direction="column"
|
direction={direction === ResizableLayoutDirection.Vertical ? 'column' : 'row'}
|
||||||
alignItems="stretch"
|
alignItems="stretch"
|
||||||
gutterSize="none"
|
gutterSize="none"
|
||||||
responsive={false}
|
responsive={false}
|
||||||
|
css={css`
|
||||||
|
height: 100%;
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
{!hideTopPanel && <EuiFlexItem grow={false}>{topPanel}</EuiFlexItem>}
|
{!hideFixedPanel && <EuiFlexItem grow={false}>{fixedPanel}</EuiFlexItem>}
|
||||||
<EuiFlexItem css={mainPanelCss}>{mainPanel}</EuiFlexItem>
|
<EuiFlexItem css={flexPanelCss}>{flexPanel}</EuiFlexItem>
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
106
packages/kbn-resizable-layout/src/resizable_layout.test.tsx
Normal file
106
packages/kbn-resizable-layout/src/resizable_layout.test.tsx
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
130
packages/kbn-resizable-layout/src/resizable_layout.tsx
Normal file
130
packages/kbn-resizable-layout/src/resizable_layout.tsx
Normal 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;
|
40
packages/kbn-resizable-layout/src/utils.test.ts
Normal file
40
packages/kbn-resizable-layout/src/utils.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
22
packages/kbn-resizable-layout/src/utils.ts
Normal file
22
packages/kbn-resizable-layout/src/utils.ts
Normal 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);
|
11
packages/kbn-resizable-layout/tsconfig.json
Normal file
11
packages/kbn-resizable-layout/tsconfig.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "target/types"
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["target/**/*"],
|
||||||
|
"kbn_references": [
|
||||||
|
"@kbn/shared-ux-utility",
|
||||||
|
]
|
||||||
|
}
|
33
packages/kbn-resizable-layout/types.ts
Normal file
33
packages/kbn-resizable-layout/types.ts
Normal 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',
|
||||||
|
}
|
|
@ -3,7 +3,6 @@
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: $euiSize * 19;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
&--collapsed {
|
&--collapsed {
|
||||||
|
@ -11,6 +10,14 @@
|
||||||
padding: $euiSizeS $euiSizeS 0;
|
padding: $euiSizeS $euiSizeS 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.unifiedFieldListSidebar--fullWidth {
|
||||||
|
min-width: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.unifiedFieldListSidebar--fullWidth) {
|
||||||
|
width: $euiSize * 19;
|
||||||
|
}
|
||||||
|
|
||||||
@include euiBreakpoint('xs', 's') {
|
@include euiBreakpoint('xs', 's') {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: $euiSize;
|
padding: $euiSize;
|
||||||
|
|
|
@ -59,6 +59,11 @@ export type UnifiedFieldListSidebarCustomizableProps = Pick<
|
||||||
*/
|
*/
|
||||||
showFieldList?: boolean;
|
showFieldList?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make the field list full width
|
||||||
|
*/
|
||||||
|
fullWidth?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compressed view
|
* Compressed view
|
||||||
*/
|
*/
|
||||||
|
@ -145,6 +150,7 @@ export const UnifiedFieldListSidebarComponent: React.FC<UnifiedFieldListSidebarP
|
||||||
trackUiMetric,
|
trackUiMetric,
|
||||||
showFieldList = true,
|
showFieldList = true,
|
||||||
compressed = true,
|
compressed = true,
|
||||||
|
fullWidth,
|
||||||
isAffectedByGlobalFilter,
|
isAffectedByGlobalFilter,
|
||||||
prepend,
|
prepend,
|
||||||
onAddFieldToWorkspace,
|
onAddFieldToWorkspace,
|
||||||
|
@ -306,6 +312,7 @@ export const UnifiedFieldListSidebarComponent: React.FC<UnifiedFieldListSidebarP
|
||||||
const pageSidebarProps: Partial<EuiPageSidebarProps> = {
|
const pageSidebarProps: Partial<EuiPageSidebarProps> = {
|
||||||
className: classnames('unifiedFieldListSidebar', {
|
className: classnames('unifiedFieldListSidebar', {
|
||||||
'unifiedFieldListSidebar--collapsed': isSidebarCollapsed,
|
'unifiedFieldListSidebar--collapsed': isSidebarCollapsed,
|
||||||
|
['unifiedFieldListSidebar--fullWidth']: fullWidth,
|
||||||
}),
|
}),
|
||||||
'aria-label': i18n.translate(
|
'aria-label': i18n.translate(
|
||||||
'unifiedFieldList.fieldListSidebar.indexAndFieldsSectionAriaLabel',
|
'unifiedFieldList.fieldListSidebar.indexAndFieldsSectionAriaLabel',
|
||||||
|
|
|
@ -30,6 +30,7 @@ import {
|
||||||
EuiShowFor,
|
EuiShowFor,
|
||||||
EuiTitle,
|
EuiTitle,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
useExistingFieldsFetcher,
|
useExistingFieldsFetcher,
|
||||||
type ExistingFieldsFetcher,
|
type ExistingFieldsFetcher,
|
||||||
|
@ -49,6 +50,7 @@ import type {
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|
||||||
export interface UnifiedFieldListSidebarContainerApi {
|
export interface UnifiedFieldListSidebarContainerApi {
|
||||||
|
isSidebarCollapsed$: Observable<boolean>;
|
||||||
refetchFieldsExistenceInfo: ExistingFieldsFetcher['refetchFieldsExistenceInfo'];
|
refetchFieldsExistenceInfo: ExistingFieldsFetcher['refetchFieldsExistenceInfo'];
|
||||||
closeFieldListFlyout: () => void;
|
closeFieldListFlyout: () => void;
|
||||||
// no user permission or missing dataViewFieldEditor service will result in `undefined` API methods
|
// 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 { data, dataViewFieldEditor } = services;
|
||||||
const [isFieldListFlyoutVisible, setIsFieldListFlyoutVisible] = useState<boolean>(false);
|
const [isFieldListFlyoutVisible, setIsFieldListFlyoutVisible] = useState<boolean>(false);
|
||||||
const { isSidebarCollapsed, onToggleSidebar } = useSidebarToggle({ stateService });
|
const { isSidebarCollapsed, onToggleSidebar } = useSidebarToggle({ stateService });
|
||||||
|
const [isSidebarCollapsed$] = useState(() => new BehaviorSubject(isSidebarCollapsed));
|
||||||
|
|
||||||
const canEditDataView =
|
const canEditDataView =
|
||||||
Boolean(dataViewFieldEditor?.userPermissions.editIndexPattern()) ||
|
Boolean(dataViewFieldEditor?.userPermissions.editIndexPattern()) ||
|
||||||
|
@ -222,16 +225,21 @@ const UnifiedFieldListSidebarContainer = forwardRef<
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
isSidebarCollapsed$.next(isSidebarCollapsed);
|
||||||
|
}, [isSidebarCollapsed, isSidebarCollapsed$]);
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
componentRef,
|
componentRef,
|
||||||
() => ({
|
() => ({
|
||||||
|
isSidebarCollapsed$,
|
||||||
refetchFieldsExistenceInfo,
|
refetchFieldsExistenceInfo,
|
||||||
closeFieldListFlyout,
|
closeFieldListFlyout,
|
||||||
createField: editField,
|
createField: editField,
|
||||||
editField,
|
editField,
|
||||||
deleteField,
|
deleteField,
|
||||||
}),
|
}),
|
||||||
[refetchFieldsExistenceInfo, closeFieldListFlyout, editField, deleteField]
|
[isSidebarCollapsed$, refetchFieldsExistenceInfo, closeFieldListFlyout, editField, deleteField]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!dataView) {
|
if (!dataView) {
|
||||||
|
|
|
@ -122,7 +122,7 @@ const mountComponent = async ({
|
||||||
columns: [],
|
columns: [],
|
||||||
viewMode: VIEW_MODE.DOCUMENT_LEVEL,
|
viewMode: VIEW_MODE.DOCUMENT_LEVEL,
|
||||||
onAddFilter: jest.fn(),
|
onAddFilter: jest.fn(),
|
||||||
resizeRef: { current: null },
|
container: null,
|
||||||
};
|
};
|
||||||
stateContainer.searchSessionManager = createSearchSessionMock(session).searchSessionManager;
|
stateContainer.searchSessionManager = createSearchSessionMock(session).searchSessionManager;
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { RefObject } from 'react';
|
import React from 'react';
|
||||||
import { UnifiedHistogramContainer } from '@kbn/unified-histogram-plugin/public';
|
import { UnifiedHistogramContainer } from '@kbn/unified-histogram-plugin/public';
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
import useObservable from 'react-use/lib/useObservable';
|
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';
|
import { useAppStateSelector } from '../../services/discover_app_state_container';
|
||||||
|
|
||||||
export interface DiscoverHistogramLayoutProps extends DiscoverMainContentProps {
|
export interface DiscoverHistogramLayoutProps extends DiscoverMainContentProps {
|
||||||
resizeRef: RefObject<HTMLDivElement>;
|
container: HTMLElement | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const histogramLayoutCss = css`
|
const histogramLayoutCss = css`
|
||||||
|
@ -28,7 +28,7 @@ export const DiscoverHistogramLayout = ({
|
||||||
isPlainRecord,
|
isPlainRecord,
|
||||||
dataView,
|
dataView,
|
||||||
stateContainer,
|
stateContainer,
|
||||||
resizeRef,
|
container,
|
||||||
...mainContentProps
|
...mainContentProps
|
||||||
}: DiscoverHistogramLayoutProps) => {
|
}: DiscoverHistogramLayoutProps) => {
|
||||||
const { dataState } = stateContainer;
|
const { dataState } = stateContainer;
|
||||||
|
@ -53,7 +53,7 @@ export const DiscoverHistogramLayout = ({
|
||||||
{...unifiedHistogramProps}
|
{...unifiedHistogramProps}
|
||||||
searchSessionId={searchSessionId}
|
searchSessionId={searchSessionId}
|
||||||
requestAdapter={dataState.inspectorAdapters.requests}
|
requestAdapter={dataState.inspectorAdapters.requests}
|
||||||
resizeRef={resizeRef}
|
container={container}
|
||||||
appendHitsCounter={
|
appendHitsCounter={
|
||||||
savedSearch.id ? (
|
savedSearch.id ? (
|
||||||
<ResetSearchButton resetSavedSearch={stateContainer.actions.undoSavedSearchChanges} />
|
<ResetSearchButton resetSavedSearch={stateContainer.actions.undoSavedSearchChanges} />
|
||||||
|
|
|
@ -29,10 +29,24 @@ discover-app {
|
||||||
|
|
||||||
.dscPageBody__contents {
|
.dscPageBody__contents {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dscSidebarResizeButton {
|
||||||
|
background-color: transparent !important;
|
||||||
|
|
||||||
|
&:not(:hover):not(:focus) {
|
||||||
|
&:before, &:after {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dscPageContent__wrapper {
|
.dscPageContent__wrapper {
|
||||||
overflow: hidden; // Ensures horizontal scroll of table
|
overflow: hidden; // Ensures horizontal scroll of table
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dscPageContent {
|
.dscPageContent {
|
||||||
|
|
|
@ -41,6 +41,11 @@ import { act } from 'react-dom/test-utils';
|
||||||
import { ErrorCallout } from '../../../../components/common/error_callout';
|
import { ErrorCallout } from '../../../../components/common/error_callout';
|
||||||
import * as localStorageModule from 'react-use/lib/useLocalStorage';
|
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');
|
jest.spyOn(localStorageModule, 'default');
|
||||||
|
|
||||||
setHeaderActionMenuMounter(jest.fn());
|
setHeaderActionMenuMounter(jest.fn());
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
import './discover_layout.scss';
|
import './discover_layout.scss';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
EuiFlexGroup,
|
EuiFlexGroup,
|
||||||
EuiFlexItem,
|
EuiFlexItem,
|
||||||
|
@ -31,6 +31,7 @@ import {
|
||||||
} from '@kbn/discover-utils';
|
} from '@kbn/discover-utils';
|
||||||
import { popularizeField, useColumns } from '@kbn/unified-data-table';
|
import { popularizeField, useColumns } from '@kbn/unified-data-table';
|
||||||
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
|
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
|
||||||
|
import type { UnifiedFieldListSidebarContainerApi } from '@kbn/unified-field-list';
|
||||||
import { useSavedSearchInitial } from '../../services/discover_state_provider';
|
import { useSavedSearchInitial } from '../../services/discover_state_provider';
|
||||||
import { DiscoverStateContainer } from '../../services/discover_state';
|
import { DiscoverStateContainer } from '../../services/discover_state';
|
||||||
import { VIEW_MODE } from '../../../../../common/constants';
|
import { VIEW_MODE } from '../../../../../common/constants';
|
||||||
|
@ -52,6 +53,7 @@ import { SavedSearchURLConflictCallout } from '../../../../components/saved_sear
|
||||||
import { DiscoverHistogramLayout } from './discover_histogram_layout';
|
import { DiscoverHistogramLayout } from './discover_histogram_layout';
|
||||||
import { ErrorCallout } from '../../../../components/common/error_callout';
|
import { ErrorCallout } from '../../../../components/common/error_callout';
|
||||||
import { addLog } from '../../../../utils/add_log';
|
import { addLog } from '../../../../utils/add_log';
|
||||||
|
import { DiscoverResizableLayout } from './discover_resizable_layout';
|
||||||
|
|
||||||
const SidebarMemoized = React.memo(DiscoverSidebarResponsive);
|
const SidebarMemoized = React.memo(DiscoverSidebarResponsive);
|
||||||
const TopNavMemoized = React.memo(DiscoverTopNav);
|
const TopNavMemoized = React.memo(DiscoverTopNav);
|
||||||
|
@ -182,7 +184,8 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
|
||||||
}
|
}
|
||||||
}, [dataState.error, isPlainRecord]);
|
}, [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 [{ dragging }] = useDragDropContext();
|
||||||
const draggingFieldName = dragging?.id;
|
const draggingFieldName = dragging?.id;
|
||||||
|
@ -211,7 +214,7 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
|
||||||
viewMode={viewMode}
|
viewMode={viewMode}
|
||||||
onAddFilter={onAddFilter as DocViewFilterFn}
|
onAddFilter={onAddFilter as DocViewFilterFn}
|
||||||
onFieldEdited={onFieldEdited}
|
onFieldEdited={onFieldEdited}
|
||||||
resizeRef={resizeRef}
|
container={mainContainer}
|
||||||
onDropFieldToTable={onDropFieldToTable}
|
onDropFieldToTable={onDropFieldToTable}
|
||||||
/>
|
/>
|
||||||
{resultState === 'loading' && <LoadingSpinner />}
|
{resultState === 'loading' && <LoadingSpinner />}
|
||||||
|
@ -221,14 +224,18 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
|
||||||
currentColumns,
|
currentColumns,
|
||||||
dataView,
|
dataView,
|
||||||
isPlainRecord,
|
isPlainRecord,
|
||||||
|
mainContainer,
|
||||||
onAddFilter,
|
onAddFilter,
|
||||||
|
onDropFieldToTable,
|
||||||
onFieldEdited,
|
onFieldEdited,
|
||||||
resultState,
|
resultState,
|
||||||
stateContainer,
|
stateContainer,
|
||||||
viewMode,
|
viewMode,
|
||||||
onDropFieldToTable,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const [unifiedFieldListSidebarContainerApi, setUnifiedFieldListSidebarContainerApi] =
|
||||||
|
useState<UnifiedFieldListSidebarContainerApi | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiPage
|
<EuiPage
|
||||||
className="dscPage"
|
className="dscPage"
|
||||||
|
@ -265,73 +272,99 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
|
||||||
onFieldEdited={onFieldEdited}
|
onFieldEdited={onFieldEdited}
|
||||||
/>
|
/>
|
||||||
<EuiPageBody className="dscPageBody" aria-describedby="savedSearchTitle">
|
<EuiPageBody className="dscPageBody" aria-describedby="savedSearchTitle">
|
||||||
<SavedSearchURLConflictCallout
|
<div
|
||||||
savedSearch={savedSearch}
|
ref={setSidebarContainer}
|
||||||
spaces={spaces}
|
css={css`
|
||||||
history={history}
|
width: 100%;
|
||||||
/>
|
height: 100%;
|
||||||
<EuiFlexGroup className="dscPageBody__contents" gutterSize="none">
|
`}
|
||||||
<EuiFlexItem grow={false}>
|
>
|
||||||
<SidebarMemoized
|
<SavedSearchURLConflictCallout
|
||||||
documents$={stateContainer.dataState.data$.documents$}
|
savedSearch={savedSearch}
|
||||||
onAddField={onAddColumn}
|
spaces={spaces}
|
||||||
columns={currentColumns}
|
history={history}
|
||||||
onAddFilter={!isPlainRecord ? onAddFilter : undefined}
|
/>
|
||||||
onRemoveField={onRemoveColumn}
|
<DiscoverResizableLayout
|
||||||
onChangeDataView={stateContainer.actions.onChangeDataView}
|
container={sidebarContainer}
|
||||||
selectedDataView={dataView}
|
unifiedFieldListSidebarContainerApi={unifiedFieldListSidebarContainerApi}
|
||||||
trackUiMetric={trackUiMetric}
|
sidebarPanel={
|
||||||
onFieldEdited={onFieldEdited}
|
<EuiFlexGroup
|
||||||
onDataViewCreated={stateContainer.actions.onDataViewCreated}
|
gutterSize="none"
|
||||||
availableFields$={stateContainer.dataState.data$.availableFields$}
|
css={css`
|
||||||
/>
|
height: 100%;
|
||||||
</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,
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{mainDisplay}
|
<EuiFlexItem>
|
||||||
</EuiPanel>
|
<SidebarMemoized
|
||||||
)}
|
documents$={stateContainer.dataState.data$.documents$}
|
||||||
</EuiFlexItem>
|
onAddField={onAddColumn}
|
||||||
</EuiFlexGroup>
|
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>
|
</EuiPageBody>
|
||||||
</EuiPage>
|
</EuiPage>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -13,7 +13,7 @@ import { EuiProgress } from '@elastic/eui';
|
||||||
import { getDataTableRecords, realHits } from '../../../../__fixtures__/real_hits';
|
import { getDataTableRecords, realHits } from '../../../../__fixtures__/real_hits';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
DiscoverSidebarResponsive,
|
DiscoverSidebarResponsive,
|
||||||
DiscoverSidebarResponsiveProps,
|
DiscoverSidebarResponsiveProps,
|
||||||
|
@ -37,6 +37,7 @@ import { buildDataTableRecord } from '@kbn/discover-utils';
|
||||||
import type { DataTableRecord } from '@kbn/discover-utils/types';
|
import type { DataTableRecord } from '@kbn/discover-utils/types';
|
||||||
import type { DiscoverCustomizationId } from '../../../../customizations/customization_service';
|
import type { DiscoverCustomizationId } from '../../../../customizations/customization_service';
|
||||||
import type { SearchBarCustomization } from '../../../../customizations';
|
import type { SearchBarCustomization } from '../../../../customizations';
|
||||||
|
import type { UnifiedFieldListSidebarContainerApi } from '@kbn/unified-field-list';
|
||||||
|
|
||||||
const mockSearchBarCustomization: SearchBarCustomization = {
|
const mockSearchBarCustomization: SearchBarCustomization = {
|
||||||
id: 'search_bar',
|
id: 'search_bar',
|
||||||
|
@ -168,6 +169,8 @@ function getCompProps(options?: { hits?: DataTableRecord[] }): DiscoverSidebarRe
|
||||||
trackUiMetric: jest.fn(),
|
trackUiMetric: jest.fn(),
|
||||||
onFieldEdited: jest.fn(),
|
onFieldEdited: jest.fn(),
|
||||||
onDataViewCreated: 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());
|
mockedServices.data.query.getState = jest.fn().mockImplementation(() => appState.getState());
|
||||||
|
|
||||||
await act(async () => {
|
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}>
|
<KibanaContextProvider services={mockedServices}>
|
||||||
<DiscoverAppStateProvider value={appState}>
|
<DiscoverAppStateProvider value={appState}>
|
||||||
<DiscoverSidebarResponsive {...props} />
|
<SidebarWrapper />
|
||||||
</DiscoverAppStateProvider>
|
</DiscoverAppStateProvider>
|
||||||
</KibanaContextProvider>
|
</KibanaContextProvider>
|
||||||
);
|
);
|
||||||
// wait for lazy modules
|
// wait for lazy modules
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
await comp.update();
|
comp.update();
|
||||||
});
|
});
|
||||||
|
|
||||||
await comp!.update();
|
comp!.update();
|
||||||
|
|
||||||
return comp!;
|
return comp!;
|
||||||
}
|
}
|
||||||
|
@ -251,7 +265,7 @@ describe('discover responsive sidebar', function () {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
// wait for lazy modules
|
// wait for lazy modules
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
await compLoadingExistence.update();
|
compLoadingExistence.update();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
@ -273,11 +287,11 @@ describe('discover responsive sidebar', function () {
|
||||||
indexPatternTitle: 'test-loaded',
|
indexPatternTitle: 'test-loaded',
|
||||||
existingFieldNames: Object.keys(mockfieldCounts),
|
existingFieldNames: Object.keys(mockfieldCounts),
|
||||||
});
|
});
|
||||||
await compLoadingExistence.update();
|
compLoadingExistence.update();
|
||||||
});
|
});
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await compLoadingExistence.update();
|
compLoadingExistence.update();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
@ -419,11 +433,11 @@ describe('discover responsive sidebar', function () {
|
||||||
const availableFields = findTestSubject(comp, 'fieldListGroupedAvailableFields');
|
const availableFields = findTestSubject(comp, 'fieldListGroupedAvailableFields');
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
const button = findTestSubject(availableFields, 'field-extension-showDetails');
|
const button = findTestSubject(availableFields, 'field-extension-showDetails');
|
||||||
await button.simulate('click');
|
button.simulate('click');
|
||||||
await comp.update();
|
comp.update();
|
||||||
});
|
});
|
||||||
|
|
||||||
await comp.update();
|
comp.update();
|
||||||
findTestSubject(comp, 'plus-extension-gif').simulate('click');
|
findTestSubject(comp, 'plus-extension-gif').simulate('click');
|
||||||
expect(props.onAddFilter).toHaveBeenCalled();
|
expect(props.onAddFilter).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -432,11 +446,11 @@ describe('discover responsive sidebar', function () {
|
||||||
const availableFields = findTestSubject(comp, 'fieldListGroupedAvailableFields');
|
const availableFields = findTestSubject(comp, 'fieldListGroupedAvailableFields');
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
const button = findTestSubject(availableFields, 'field-extension-showDetails');
|
const button = findTestSubject(availableFields, 'field-extension-showDetails');
|
||||||
await button.simulate('click');
|
button.simulate('click');
|
||||||
await comp.update();
|
comp.update();
|
||||||
});
|
});
|
||||||
|
|
||||||
await comp.update();
|
comp.update();
|
||||||
findTestSubject(comp, 'discoverFieldListPanelAddExistFilter-extension').simulate('click');
|
findTestSubject(comp, 'discoverFieldListPanelAddExistFilter-extension').simulate('click');
|
||||||
expect(props.onAddFilter).toHaveBeenCalledWith('_exists_', 'extension', '+');
|
expect(props.onAddFilter).toHaveBeenCalledWith('_exists_', 'extension', '+');
|
||||||
});
|
});
|
||||||
|
@ -450,7 +464,7 @@ describe('discover responsive sidebar', function () {
|
||||||
);
|
);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await findTestSubject(comp, 'fieldListFiltersFieldSearch').simulate('change', {
|
findTestSubject(comp, 'fieldListFiltersFieldSearch').simulate('change', {
|
||||||
target: { value: 'bytes' },
|
target: { value: 'bytes' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -471,16 +485,16 @@ describe('discover responsive sidebar', function () {
|
||||||
);
|
);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await findTestSubject(comp, 'fieldListFiltersFieldTypeFilterToggle').simulate('click');
|
findTestSubject(comp, 'fieldListFiltersFieldTypeFilterToggle').simulate('click');
|
||||||
});
|
});
|
||||||
|
|
||||||
await comp.update();
|
comp.update();
|
||||||
|
|
||||||
await act(async () => {
|
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, 'fieldListGroupedAvailableFields-count').text()).toBe('2');
|
||||||
expect(findTestSubject(comp, 'fieldListGrouped__ariaDescription').text()).toBe(
|
expect(findTestSubject(comp, 'fieldListGrouped__ariaDescription').text()).toBe(
|
||||||
|
@ -519,7 +533,7 @@ describe('discover responsive sidebar', function () {
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
await compInTextBasedMode.update();
|
compInTextBasedMode.update();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(findTestSubject(compInTextBasedMode, 'indexPattern-add-field_btn').length).toBe(0);
|
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');
|
const addFieldButton = findTestSubject(comp, 'dataView-add-field_btn');
|
||||||
expect(addFieldButton.length).toBe(1);
|
expect(addFieldButton.length).toBe(1);
|
||||||
await addFieldButton.simulate('click');
|
addFieldButton.simulate('click');
|
||||||
expect(services.dataViewFieldEditor.openEditor).toHaveBeenCalledTimes(1);
|
expect(services.dataViewFieldEditor.openEditor).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -630,10 +644,10 @@ describe('discover responsive sidebar', function () {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
findTestSubject(availableFields, 'field-bytes').simulate('click');
|
findTestSubject(availableFields, 'field-bytes').simulate('click');
|
||||||
});
|
});
|
||||||
await comp.update();
|
comp.update();
|
||||||
const editFieldButton = findTestSubject(comp, 'discoverFieldListPanelEdit-bytes');
|
const editFieldButton = findTestSubject(comp, 'discoverFieldListPanelEdit-bytes');
|
||||||
expect(editFieldButton.length).toBe(1);
|
expect(editFieldButton.length).toBe(1);
|
||||||
await editFieldButton.simulate('click');
|
editFieldButton.simulate('click');
|
||||||
expect(services.dataViewFieldEditor.openEditor).toHaveBeenCalledTimes(1);
|
expect(services.dataViewFieldEditor.openEditor).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -662,12 +676,12 @@ describe('discover responsive sidebar', function () {
|
||||||
// open flyout
|
// open flyout
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
compWithPicker.find('.unifiedFieldListSidebar__mobileButton').last().simulate('click');
|
compWithPicker.find('.unifiedFieldListSidebar__mobileButton').last().simulate('click');
|
||||||
await compWithPicker.update();
|
compWithPicker.update();
|
||||||
});
|
});
|
||||||
|
|
||||||
await compWithPicker.update();
|
compWithPicker.update();
|
||||||
// open data view picker
|
// 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);
|
expect(findTestSubject(compWithPicker, 'changeDataViewPopover').length).toBe(1);
|
||||||
// check "Add a field"
|
// check "Add a field"
|
||||||
const addFieldButtonInDataViewPicker = findTestSubject(
|
const addFieldButtonInDataViewPicker = findTestSubject(
|
||||||
|
@ -678,7 +692,7 @@ describe('discover responsive sidebar', function () {
|
||||||
// click "Create a data view"
|
// click "Create a data view"
|
||||||
const createDataViewButton = findTestSubject(compWithPicker, 'dataview-create-new');
|
const createDataViewButton = findTestSubject(compWithPicker, 'dataview-create-new');
|
||||||
expect(createDataViewButton.length).toBe(1);
|
expect(createDataViewButton.length).toBe(1);
|
||||||
await createDataViewButton.simulate('click');
|
createDataViewButton.simulate('click');
|
||||||
expect(services.dataViewEditor.openEditor).toHaveBeenCalled();
|
expect(services.dataViewEditor.openEditor).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -697,10 +711,10 @@ describe('discover responsive sidebar', function () {
|
||||||
.find('.unifiedFieldListSidebar__mobileButton')
|
.find('.unifiedFieldListSidebar__mobileButton')
|
||||||
.last()
|
.last()
|
||||||
.simulate('click');
|
.simulate('click');
|
||||||
await compWithPickerInViewerMode.update();
|
compWithPickerInViewerMode.update();
|
||||||
});
|
});
|
||||||
|
|
||||||
await compWithPickerInViewerMode.update();
|
compWithPickerInViewerMode.update();
|
||||||
// open data view picker
|
// open data view picker
|
||||||
findTestSubject(compWithPickerInViewerMode, 'dataView-switch-link').simulate('click');
|
findTestSubject(compWithPickerInViewerMode, 'dataView-switch-link').simulate('click');
|
||||||
expect(findTestSubject(compWithPickerInViewerMode, 'changeDataViewPopover').length).toBe(1);
|
expect(findTestSubject(compWithPickerInViewerMode, 'changeDataViewPopover').length).toBe(1);
|
||||||
|
@ -724,10 +738,10 @@ describe('discover responsive sidebar', function () {
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
comp.find('.unifiedFieldListSidebar__mobileButton').last().simulate('click');
|
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);
|
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 () => {
|
await act(async () => {
|
||||||
comp.find('.unifiedFieldListSidebar__mobileButton').last().simulate('click');
|
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);
|
expect(comp.find('[data-test-subj="custom-data-view-picker"]').exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* Side Public License, v 1.
|
* 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 { UiCounterMetricType } from '@kbn/analytics';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||||
|
@ -133,6 +133,9 @@ export interface DiscoverSidebarResponsiveProps {
|
||||||
* For customization and testing purposes
|
* For customization and testing purposes
|
||||||
*/
|
*/
|
||||||
fieldListVariant?: UnifiedFieldListSidebarContainerProps['variant'];
|
fieldListVariant?: UnifiedFieldListSidebarContainerProps['variant'];
|
||||||
|
|
||||||
|
unifiedFieldListSidebarContainerApi: UnifiedFieldListSidebarContainerApi | null;
|
||||||
|
setUnifiedFieldListSidebarContainerApi: (api: UnifiedFieldListSidebarContainerApi) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -153,6 +156,8 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
|
||||||
onChangeDataView,
|
onChangeDataView,
|
||||||
onAddField,
|
onAddField,
|
||||||
onRemoveField,
|
onRemoveField,
|
||||||
|
unifiedFieldListSidebarContainerApi,
|
||||||
|
setUnifiedFieldListSidebarContainerApi,
|
||||||
} = props;
|
} = props;
|
||||||
const [sidebarState, dispatchSidebarStateAction] = useReducer(
|
const [sidebarState, dispatchSidebarStateAction] = useReducer(
|
||||||
discoverSidebarReducer,
|
discoverSidebarReducer,
|
||||||
|
@ -161,8 +166,6 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
|
||||||
);
|
);
|
||||||
const selectedDataViewRef = useRef<DataView | null | undefined>(selectedDataView);
|
const selectedDataViewRef = useRef<DataView | null | undefined>(selectedDataView);
|
||||||
const showFieldList = sidebarState.status !== DiscoverSidebarReducerStatus.INITIAL;
|
const showFieldList = sidebarState.status !== DiscoverSidebarReducerStatus.INITIAL;
|
||||||
const [unifiedFieldListSidebarContainerApi, setUnifiedFieldListSidebarContainerApi] =
|
|
||||||
useState<UnifiedFieldListSidebarContainerApi | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscription = props.documents$.subscribe((documentState) => {
|
const subscription = props.documents$.subscribe((documentState) => {
|
||||||
|
@ -385,6 +388,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
|
||||||
allFields={sidebarState.allFields}
|
allFields={sidebarState.allFields}
|
||||||
showFieldList={showFieldList}
|
showFieldList={showFieldList}
|
||||||
workspaceSelectedFieldNames={columns}
|
workspaceSelectedFieldNames={columns}
|
||||||
|
fullWidth
|
||||||
onAddFieldToWorkspace={onAddFieldToWorkspace}
|
onAddFieldToWorkspace={onAddFieldToWorkspace}
|
||||||
onRemoveFieldFromWorkspace={onRemoveFieldFromWorkspace}
|
onRemoveFieldFromWorkspace={onRemoveFieldFromWorkspace}
|
||||||
onAddFilter={onAddFilter}
|
onAddFilter={onAddFilter}
|
||||||
|
|
|
@ -73,7 +73,8 @@
|
||||||
"@kbn/unified-data-table",
|
"@kbn/unified-data-table",
|
||||||
"@kbn/no-data-page-plugin",
|
"@kbn/no-data-page-plugin",
|
||||||
"@kbn/rule-data-utils",
|
"@kbn/rule-data-utils",
|
||||||
"@kbn/global-search-plugin"
|
"@kbn/global-search-plugin",
|
||||||
|
"@kbn/resizable-layout"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"target/**/*"
|
"target/**/*"
|
||||||
|
|
|
@ -33,7 +33,7 @@ describe('UnifiedHistogramContainer', () => {
|
||||||
requestAdapter={new RequestAdapter()}
|
requestAdapter={new RequestAdapter()}
|
||||||
searchSessionId={'123'}
|
searchSessionId={'123'}
|
||||||
timeRange={{ from: 'now-15m', to: 'now' }}
|
timeRange={{ from: 'now-15m', to: 'now' }}
|
||||||
resizeRef={{ current: null }}
|
container={null}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
expect(component.update().isEmptyRender()).toBe(true);
|
expect(component.update().isEmptyRender()).toBe(true);
|
||||||
|
@ -62,7 +62,7 @@ describe('UnifiedHistogramContainer', () => {
|
||||||
requestAdapter={new RequestAdapter()}
|
requestAdapter={new RequestAdapter()}
|
||||||
searchSessionId={'123'}
|
searchSessionId={'123'}
|
||||||
timeRange={{ from: 'now-15m', to: 'now' }}
|
timeRange={{ from: 'now-15m', to: 'now' }}
|
||||||
resizeRef={{ current: null }}
|
container={null}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
|
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
|
||||||
|
|
|
@ -53,7 +53,7 @@ export type UnifiedHistogramContainerProps = {
|
||||||
| 'timeRange'
|
| 'timeRange'
|
||||||
| 'relativeTimeRange'
|
| 'relativeTimeRange'
|
||||||
| 'columns'
|
| 'columns'
|
||||||
| 'resizeRef'
|
| 'container'
|
||||||
| 'appendHitsCounter'
|
| 'appendHitsCounter'
|
||||||
| 'children'
|
| 'children'
|
||||||
| 'onBrushEnd'
|
| 'onBrushEnd'
|
||||||
|
|
|
@ -13,7 +13,6 @@ import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { Chart } from '../chart';
|
import { Chart } from '../chart';
|
||||||
import { Panels, PANELS_MODE } from '../panels';
|
|
||||||
import {
|
import {
|
||||||
UnifiedHistogramChartContext,
|
UnifiedHistogramChartContext,
|
||||||
UnifiedHistogramFetchStatus,
|
UnifiedHistogramFetchStatus,
|
||||||
|
@ -22,6 +21,7 @@ import {
|
||||||
import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield';
|
import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield';
|
||||||
import { unifiedHistogramServicesMock } from '../__mocks__/services';
|
import { unifiedHistogramServicesMock } from '../__mocks__/services';
|
||||||
import { UnifiedHistogramLayout, UnifiedHistogramLayoutProps } from './layout';
|
import { UnifiedHistogramLayout, UnifiedHistogramLayoutProps } from './layout';
|
||||||
|
import { ResizableLayout, ResizableLayoutMode } from '@kbn/resizable-layout';
|
||||||
|
|
||||||
let mockBreakpoint = 'l';
|
let mockBreakpoint = 'l';
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ describe('Layout', () => {
|
||||||
services = unifiedHistogramServicesMock,
|
services = unifiedHistogramServicesMock,
|
||||||
hits = createHits(),
|
hits = createHits(),
|
||||||
chart = createChart(),
|
chart = createChart(),
|
||||||
resizeRef = { current: null },
|
container = null,
|
||||||
...rest
|
...rest
|
||||||
}: Partial<Omit<UnifiedHistogramLayoutProps, 'hits' | 'chart'>> & {
|
}: Partial<Omit<UnifiedHistogramLayoutProps, 'hits' | 'chart'>> & {
|
||||||
hits?: UnifiedHistogramHitsContext | null;
|
hits?: UnifiedHistogramHitsContext | null;
|
||||||
|
@ -65,7 +65,7 @@ describe('Layout', () => {
|
||||||
services={services}
|
services={services}
|
||||||
hits={hits ?? undefined}
|
hits={hits ?? undefined}
|
||||||
chart={chart ?? undefined}
|
chart={chart ?? undefined}
|
||||||
resizeRef={resizeRef}
|
container={container}
|
||||||
dataView={dataViewWithTimefieldMock}
|
dataView={dataViewWithTimefieldMock}
|
||||||
query={{
|
query={{
|
||||||
language: 'kuery',
|
language: 'kuery',
|
||||||
|
@ -95,66 +95,66 @@ describe('Layout', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PANELS_MODE', () => {
|
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();
|
const component = await mountComponent();
|
||||||
setBreakpoint(component, 'm');
|
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();
|
const component = await mountComponent();
|
||||||
setBreakpoint(component, 's');
|
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({
|
const component = await mountComponent({
|
||||||
chart: {
|
chart: {
|
||||||
...createChart(),
|
...createChart(),
|
||||||
hidden: true,
|
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 });
|
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 });
|
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();
|
const component = await mountComponent();
|
||||||
setBreakpoint(component, 's');
|
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({
|
expect(component.find(Chart).find('div.euiFlexGroup').first().getDOMNode()).toHaveStyle({
|
||||||
height: `${expectedHeight}px`,
|
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 } });
|
const component = await mountComponent({ chart: { ...createChart(), hidden: true } });
|
||||||
setBreakpoint(component, 's');
|
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({
|
expect(component.find(Chart).find('div.euiFlexGroup').first().getDOMNode()).not.toHaveStyle({
|
||||||
height: `${expectedHeight}px`,
|
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 });
|
const component = await mountComponent({ chart: null });
|
||||||
setBreakpoint(component, 's');
|
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({
|
expect(component.find(Chart).find('div.euiFlexGroup').first().getDOMNode()).not.toHaveStyle({
|
||||||
height: `${expectedHeight}px`,
|
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 });
|
const component = await mountComponent({ topPanelHeight: 123 });
|
||||||
expect(component.find(Chart).prop('onResetChartHeight')).toBeDefined();
|
expect(component.find(Chart).prop('onResetChartHeight')).toBeDefined();
|
||||||
setBreakpoint(component, 's');
|
setBreakpoint(component, 's');
|
||||||
|
@ -163,28 +163,28 @@ describe('Layout', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('topPanelHeight', () => {
|
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 });
|
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({
|
const component: ReactWrapper = await mountComponent({
|
||||||
onTopPanelHeightChange: jest.fn((topPanelHeight) => {
|
onTopPanelHeightChange: jest.fn((topPanelHeight) => {
|
||||||
component.setProps({ topPanelHeight });
|
component.setProps({ topPanelHeight });
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const defaultTopPanelHeight = component.find(Panels).prop('topPanelHeight');
|
const defaultTopPanelHeight = component.find(ResizableLayout).prop('fixedPanelSize');
|
||||||
const newTopPanelHeight = 123;
|
const newTopPanelHeight = 123;
|
||||||
expect(component.find(Panels).prop('topPanelHeight')).not.toBe(newTopPanelHeight);
|
expect(component.find(ResizableLayout).prop('fixedPanelSize')).not.toBe(newTopPanelHeight);
|
||||||
act(() => {
|
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(() => {
|
act(() => {
|
||||||
component.find(Chart).prop('onResetChartHeight')!();
|
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 () => {
|
it('should pass undefined for onResetChartHeight to Chart when the chart is the default height', async () => {
|
||||||
|
|
|
@ -7,8 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EuiSpacer, useEuiTheme, useIsWithinBreakpoints } from '@elastic/eui';
|
import { EuiSpacer, useEuiTheme, useIsWithinBreakpoints } from '@elastic/eui';
|
||||||
import { PropsWithChildren, ReactElement, RefObject } from 'react';
|
import React, { PropsWithChildren, ReactElement, useMemo, useState } from 'react';
|
||||||
import React, { useMemo } from 'react';
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
|
import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
@ -21,9 +20,13 @@ import type {
|
||||||
LensSuggestionsApi,
|
LensSuggestionsApi,
|
||||||
Suggestion,
|
Suggestion,
|
||||||
} from '@kbn/lens-plugin/public';
|
} 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 { Chart } from '../chart';
|
||||||
import { Panels, PANELS_MODE } from '../panels';
|
|
||||||
import type {
|
import type {
|
||||||
UnifiedHistogramChartContext,
|
UnifiedHistogramChartContext,
|
||||||
UnifiedHistogramServices,
|
UnifiedHistogramServices,
|
||||||
|
@ -96,9 +99,9 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren<unknown>
|
||||||
*/
|
*/
|
||||||
breakdown?: UnifiedHistogramBreakdownContext;
|
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
|
* Current top panel height -- leave undefined to use the default
|
||||||
*/
|
*/
|
||||||
|
@ -192,7 +195,7 @@ export const UnifiedHistogramLayout = ({
|
||||||
lensEmbeddableOutput$,
|
lensEmbeddableOutput$,
|
||||||
chart: originalChart,
|
chart: originalChart,
|
||||||
breakdown,
|
breakdown,
|
||||||
resizeRef,
|
container,
|
||||||
topPanelHeight,
|
topPanelHeight,
|
||||||
appendHitsCounter,
|
appendHitsCounter,
|
||||||
disableAutoFetching,
|
disableAutoFetching,
|
||||||
|
@ -226,14 +229,11 @@ export const UnifiedHistogramLayout = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const chart = suggestionUnsupported ? undefined : originalChart;
|
const chart = suggestionUnsupported ? undefined : originalChart;
|
||||||
const topPanelNode = useMemo(
|
const [topPanelNode] = useState(() =>
|
||||||
() => createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } }),
|
createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } })
|
||||||
[]
|
|
||||||
);
|
);
|
||||||
|
const [mainPanelNode] = useState(() =>
|
||||||
const mainPanelNode = useMemo(
|
createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } })
|
||||||
() => createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } }),
|
|
||||||
[]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const isMobile = useIsWithinBreakpoints(['xs', 's']);
|
const isMobile = useIsWithinBreakpoints(['xs', 's']);
|
||||||
|
@ -252,14 +252,15 @@ export const UnifiedHistogramLayout = ({
|
||||||
const panelsMode =
|
const panelsMode =
|
||||||
chart || hits
|
chart || hits
|
||||||
? showFixedPanels
|
? showFixedPanels
|
||||||
? PANELS_MODE.FIXED
|
? ResizableLayoutMode.Static
|
||||||
: PANELS_MODE.RESIZABLE
|
: ResizableLayoutMode.Resizable
|
||||||
: PANELS_MODE.SINGLE;
|
: ResizableLayoutMode.Single;
|
||||||
|
|
||||||
const currentTopPanelHeight = topPanelHeight ?? defaultTopPanelHeight;
|
const currentTopPanelHeight = topPanelHeight ?? defaultTopPanelHeight;
|
||||||
|
|
||||||
const onResetChartHeight = useMemo(() => {
|
const onResetChartHeight = useMemo(() => {
|
||||||
return currentTopPanelHeight !== defaultTopPanelHeight && panelsMode === PANELS_MODE.RESIZABLE
|
return currentTopPanelHeight !== defaultTopPanelHeight &&
|
||||||
|
panelsMode === ResizableLayoutMode.Resizable
|
||||||
? () => onTopPanelHeightChange?.(undefined)
|
? () => onTopPanelHeightChange?.(undefined)
|
||||||
: undefined;
|
: undefined;
|
||||||
}, [currentTopPanelHeight, defaultTopPanelHeight, onTopPanelHeightChange, panelsMode]);
|
}, [currentTopPanelHeight, defaultTopPanelHeight, onTopPanelHeightChange, panelsMode]);
|
||||||
|
@ -305,16 +306,18 @@ export const UnifiedHistogramLayout = ({
|
||||||
/>
|
/>
|
||||||
</InPortal>
|
</InPortal>
|
||||||
<InPortal node={mainPanelNode}>{children}</InPortal>
|
<InPortal node={mainPanelNode}>{children}</InPortal>
|
||||||
<Panels
|
<ResizableLayout
|
||||||
className={className}
|
className={className}
|
||||||
mode={panelsMode}
|
mode={panelsMode}
|
||||||
resizeRef={resizeRef}
|
direction={ResizableLayoutDirection.Vertical}
|
||||||
topPanelHeight={currentTopPanelHeight}
|
container={container}
|
||||||
minTopPanelHeight={defaultTopPanelHeight}
|
fixedPanelSize={currentTopPanelHeight}
|
||||||
minMainPanelHeight={minMainPanelHeight}
|
minFixedPanelSize={defaultTopPanelHeight}
|
||||||
topPanel={<OutPortal node={topPanelNode} />}
|
minFlexPanelSize={minMainPanelHeight}
|
||||||
mainPanel={<OutPortal node={mainPanelNode} />}
|
fixedPanel={<OutPortal node={topPanelNode} />}
|
||||||
onTopPanelHeightChange={onTopPanelHeightChange}
|
flexPanel={<OutPortal node={mainPanelNode} />}
|
||||||
|
data-test-subj="unifiedHistogram"
|
||||||
|
onFixedPanelSizeChange={onTopPanelHeightChange}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -25,6 +25,7 @@
|
||||||
"@kbn/kibana-utils-plugin",
|
"@kbn/kibana-utils-plugin",
|
||||||
"@kbn/visualizations-plugin",
|
"@kbn/visualizations-plugin",
|
||||||
"@kbn/discover-utils",
|
"@kbn/discover-utils",
|
||||||
|
"@kbn/resizable-layout",
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"target/**/*",
|
"target/**/*",
|
||||||
|
|
|
@ -31,8 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
defaultIndex: 'logstash-*',
|
defaultIndex: 'logstash-*',
|
||||||
};
|
};
|
||||||
|
|
||||||
// FLAKY: https://github.com/elastic/kibana/issues/146223
|
describe('discover test', function describeIndexTests() {
|
||||||
describe.skip('discover test', function describeIndexTests() {
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
log.debug('load kibana index with default index pattern');
|
log.debug('load kibana index with default index pattern');
|
||||||
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover');
|
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.timePicker.setDefaultAbsoluteRange();
|
||||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||||
const actualInterval = await PageObjects.discover.getChartInterval();
|
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 () {
|
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
|
// apply query some changes
|
||||||
await queryBar.setQuery('test');
|
await queryBar.setQuery('test');
|
||||||
await queryBar.submitQuery();
|
await queryBar.submitQuery();
|
||||||
|
@ -298,10 +300,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('resizable layout panels', () => {
|
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 resizeDistance = 100;
|
||||||
const topPanel = await testSubjects.find('unifiedHistogramResizablePanelTop');
|
const topPanel = await testSubjects.find('unifiedHistogramResizablePanelFixed');
|
||||||
const mainPanel = await testSubjects.find('unifiedHistogramResizablePanelMain');
|
const mainPanel = await testSubjects.find('unifiedHistogramResizablePanelFlex');
|
||||||
const resizeButton = await testSubjects.find('unifiedHistogramResizableButton');
|
const resizeButton = await testSubjects.find('unifiedHistogramResizableButton');
|
||||||
const topPanelSize = (await topPanel.getPosition()).height;
|
const topPanelSize = (await topPanel.getPosition()).height;
|
||||||
const mainPanelSize = (await mainPanel.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(newTopPanelSize).to.be(topPanelSize + resizeDistance);
|
||||||
expect(newMainPanelSize).to.be(mainPanelSize - 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', () => {
|
describe('URL state', () => {
|
||||||
|
|
|
@ -155,7 +155,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
it('should see scripted field value in Discover', async function () {
|
it('should see scripted field value in Discover', async function () {
|
||||||
await PageObjects.common.navigateToApp('discover');
|
await PageObjects.common.navigateToApp('discover');
|
||||||
|
|
||||||
await PageObjects.unifiedFieldList.clickFieldListItem(scriptedPainlessFieldName);
|
|
||||||
await retry.try(async function () {
|
await retry.try(async function () {
|
||||||
await PageObjects.unifiedFieldList.clickFieldListItemAdd(scriptedPainlessFieldName);
|
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 () {
|
it('should see scripted field value in Discover', async function () {
|
||||||
await PageObjects.common.navigateToApp('discover');
|
await PageObjects.common.navigateToApp('discover');
|
||||||
|
|
||||||
await PageObjects.unifiedFieldList.clickFieldListItem(scriptedPainlessFieldName2);
|
|
||||||
await retry.try(async function () {
|
await retry.try(async function () {
|
||||||
await PageObjects.unifiedFieldList.clickFieldListItemAdd(scriptedPainlessFieldName2);
|
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 () {
|
it('should see scripted field value in Discover', async function () {
|
||||||
await PageObjects.common.navigateToApp('discover');
|
await PageObjects.common.navigateToApp('discover');
|
||||||
|
|
||||||
await PageObjects.unifiedFieldList.clickFieldListItem(scriptedPainlessFieldName2);
|
|
||||||
await retry.try(async function () {
|
await retry.try(async function () {
|
||||||
await PageObjects.unifiedFieldList.clickFieldListItemAdd(scriptedPainlessFieldName2);
|
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 () {
|
it('should see scripted field value in Discover', async function () {
|
||||||
await PageObjects.common.navigateToApp('discover');
|
await PageObjects.common.navigateToApp('discover');
|
||||||
|
|
||||||
await PageObjects.unifiedFieldList.clickFieldListItem(scriptedPainlessFieldName2);
|
|
||||||
await retry.try(async function () {
|
await retry.try(async function () {
|
||||||
await PageObjects.unifiedFieldList.clickFieldListItemAdd(scriptedPainlessFieldName2);
|
await PageObjects.unifiedFieldList.clickFieldListItemAdd(scriptedPainlessFieldName2);
|
||||||
});
|
});
|
||||||
|
|
|
@ -143,7 +143,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
await PageObjects.common.navigateToApp('discover');
|
await PageObjects.common.navigateToApp('discover');
|
||||||
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
|
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
|
||||||
|
|
||||||
await PageObjects.unifiedFieldList.clickFieldListItem(scriptedPainlessFieldName);
|
|
||||||
await retry.try(async function () {
|
await retry.try(async function () {
|
||||||
await PageObjects.unifiedFieldList.clickFieldListItemAdd(scriptedPainlessFieldName);
|
await PageObjects.unifiedFieldList.clickFieldListItemAdd(scriptedPainlessFieldName);
|
||||||
});
|
});
|
||||||
|
@ -233,7 +232,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
await PageObjects.common.navigateToApp('discover');
|
await PageObjects.common.navigateToApp('discover');
|
||||||
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
|
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
|
||||||
|
|
||||||
await PageObjects.unifiedFieldList.clickFieldListItem(scriptedPainlessFieldName2);
|
|
||||||
await retry.try(async function () {
|
await retry.try(async function () {
|
||||||
await PageObjects.unifiedFieldList.clickFieldListItemAdd(scriptedPainlessFieldName2);
|
await PageObjects.unifiedFieldList.clickFieldListItemAdd(scriptedPainlessFieldName2);
|
||||||
});
|
});
|
||||||
|
@ -322,7 +320,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
await PageObjects.common.navigateToApp('discover');
|
await PageObjects.common.navigateToApp('discover');
|
||||||
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
|
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
|
||||||
|
|
||||||
await PageObjects.unifiedFieldList.clickFieldListItem(scriptedPainlessFieldName2);
|
|
||||||
await retry.try(async function () {
|
await retry.try(async function () {
|
||||||
await PageObjects.unifiedFieldList.clickFieldListItemAdd(scriptedPainlessFieldName2);
|
await PageObjects.unifiedFieldList.clickFieldListItemAdd(scriptedPainlessFieldName2);
|
||||||
});
|
});
|
||||||
|
@ -412,7 +409,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
await PageObjects.common.navigateToApp('discover');
|
await PageObjects.common.navigateToApp('discover');
|
||||||
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
|
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
|
||||||
|
|
||||||
await PageObjects.unifiedFieldList.clickFieldListItem(scriptedPainlessFieldName2);
|
|
||||||
await retry.try(async function () {
|
await retry.try(async function () {
|
||||||
await PageObjects.unifiedFieldList.clickFieldListItemAdd(scriptedPainlessFieldName2);
|
await PageObjects.unifiedFieldList.clickFieldListItemAdd(scriptedPainlessFieldName2);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1156,6 +1156,10 @@
|
||||||
"@kbn/reporting-example-plugin/*": ["x-pack/examples/reporting_example/*"],
|
"@kbn/reporting-example-plugin/*": ["x-pack/examples/reporting_example/*"],
|
||||||
"@kbn/reporting-plugin": ["x-pack/plugins/reporting"],
|
"@kbn/reporting-plugin": ["x-pack/plugins/reporting"],
|
||||||
"@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/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"],
|
"@kbn/response-stream-plugin": ["examples/response_stream"],
|
||||||
|
|
|
@ -5255,6 +5255,14 @@
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
uid ""
|
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":
|
"@kbn/resolver-test-plugin@link:x-pack/test/plugin_functional/plugins/resolver_test":
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
uid ""
|
uid ""
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue