[Dashboard Usability] Moves scrollbar to panel section (#145628)

## Summary

Closes https://github.com/elastic/kibana/issues/145404.
Closes #134257.

Cloud deployment for testing:
https://kibana-pr-145628.kb.us-west2.gcp.elastic-cloud.com:9243
User: `elastic`
Password: `zuIno5Tuy4lVmhMwbt2C6NyY`

This moves the scrollbar from the entire app to only the panel section
of the dashboard app. The search/filter bar and editor toolbar will
remain at the top while controls and panels scroll. The controls
floating actions were extracted out into their own component for future
use for panel actions.

#### Before
![Nov-17-2022
12-53-43](https://user-images.githubusercontent.com/1697105/202557972-6963b9d3-4eb0-40a4-963d-19e16e652d95.gif)


#### After
![Nov-17-2022
12-41-29](https://user-images.githubusercontent.com/1697105/202557993-251a984e-8bce-4b50-94c7-a7a39410a624.gif)


### Checklist

Delete any items that are not applicable to this PR.

- [ ] 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
- [ ] [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
- [ ] 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)
- [ ] 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))
- [ ] 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:
Catherine Liu 2023-02-09 10:10:54 -08:00 committed by GitHub
parent f439bdc2b3
commit 1f03570126
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 283 additions and 152 deletions

View file

@ -21,7 +21,7 @@ import {
import { FormattedMessage } from '@kbn/i18n-react';
import { Markdown } from '@kbn/kibana-react-plugin/public';
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
import { useReduxEmbeddableContext, FloatingActions } from '@kbn/presentation-util-plugin/public';
import { ControlGroupReduxState } from '../types';
import { pluginServices } from '../../services';
import { EditControlButton } from '../editor/edit_control';
@ -127,12 +127,7 @@ export const ControlFrame = ({
}, [embeddable, embeddableRoot]);
const floatingActions = (
<div
className={classNames('controlFrameFloatingActions', {
'controlFrameFloatingActions--twoLine': usingTwoLineLayout,
'controlFrameFloatingActions--oneLine': !usingTwoLineLayout,
})}
>
<>
{!fatalError && embeddableType !== TIME_SLIDER_CONTROL && (
<EuiToolTip content={ControlGroupStrings.floatingActions.getEditButtonTitle()}>
<EditControlButton embeddableId={embeddableId} />
@ -158,7 +153,7 @@ export const ControlFrame = ({
color="danger"
/>
</EuiToolTip>
</div>
</>
);
const embeddableParentClassNames = classNames('controlFrame__control', {
@ -220,18 +215,27 @@ export const ControlFrame = ({
return (
<>
{embeddable && enableActions && floatingActions}
<EuiFormRow
data-test-subj="control-frame-title"
fullWidth
label={
usingTwoLineLayout
? title || ControlGroupStrings.emptyState.getTwoLineLoadingTitle()
: undefined
}
<FloatingActions
className={classNames('controlFrameFloatingActions', {
'controlFrameFloatingActions--twoLine': usingTwoLineLayout,
'controlFrameFloatingActions--oneLine': !usingTwoLineLayout,
})}
usingTwoLineLayout={usingTwoLineLayout}
actions={floatingActions}
isEnabled={embeddable && enableActions}
>
{form}
</EuiFormRow>
<EuiFormRow
data-test-subj="control-frame-title"
fullWidth
label={
usingTwoLineLayout
? title || ControlGroupStrings.emptyState.getTwoLineLoadingTitle()
: undefined
}
>
{form}
</EuiFormRow>
</FloatingActions>
</>
);
};

View file

@ -181,48 +181,6 @@ $controlMinWidth: $euiSize * 14;
}
}
.controlFrameFloatingActions {
visibility: hidden;
opacity: 0;
// slower transition on hover leave in case the user accidentally stops hover
transition: visibility .3s, opacity .3s;
z-index: 1;
position: absolute;
&--oneLine {
right: $euiSizeXS;
top: -$euiSizeL;
padding: $euiSizeXS;
border-radius: $euiBorderRadius;
background-color: $euiColorEmptyShade;
box-shadow: 0 0 0 1px $euiColorLightShade;
}
&--fatalError {
right: $euiSizeXS;
top: -$euiSizeL;
padding: $euiSizeXS;
border-radius: $euiBorderRadius;
background-color: $euiColorEmptyShade;
box-shadow: 0 0 0 1px $euiColorLightShade;
}
&--twoLine {
right: $euiSizeXS;
top: -$euiSizeXS;
}
}
&:hover {
.controlFrameFloatingActions {
transition: visibility .1s, opacity .1s;
visibility: visible;
opacity: 1;
}
}
&-isDragging {
.euiFormRow__labelWrapper {
opacity: 0;
@ -232,3 +190,22 @@ $controlMinWidth: $euiSize * 14;
}
}
}
.controlFrameFloatingActions {
z-index: 1;
position: absolute;
&--oneLine {
padding: $euiSizeXS;
border-radius: $euiBorderRadius;
background-color: $euiColorEmptyShade;
box-shadow: 0 0 0 1px $euiColorLightShade;
}
&--fatalError {
padding: $euiSizeXS;
border-radius: $euiBorderRadius;
background-color: $euiColorEmptyShade;
box-shadow: 0 0 0 1px $euiColorLightShade;
}
}

View file

@ -1,3 +1,12 @@
@import 'src/core/public/mixins';
.dshAppWrapper {
@include kibanaFullBodyHeight();
display: flex;
flex-direction: column;
}
.dshUnsavedListingItem {
margin-top: $euiSizeM;
}

View file

@ -184,7 +184,7 @@ export function DashboardApp({
}, [dashboardContainer, kbnUrlStateStorage]);
return (
<>
<div className={'dshAppWrapper'}>
{showNoDataPage && (
<DashboardAppNoDataPage onDataViewCreated={() => setShowNoDataPage(false)} />
)}
@ -204,6 +204,6 @@ export function DashboardApp({
/>
</>
)}
</>
</div>
);
}

View file

@ -6,7 +6,6 @@
* Side Public License, v 1.
*/
import { EuiHorizontalRule } from '@elastic/eui';
import { METRIC_TYPE } from '@kbn/analytics';
import { EmbeddableFactory } from '@kbn/embeddable-plugin/public';
import {
@ -19,6 +18,8 @@ import {
import { BaseVisType, VisTypeAlias } from '@kbn/visualizations-plugin/public';
import React from 'react';
import { useCallback } from 'react';
import { useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { dashboardReplacePanelActionStrings } from '../../dashboard_actions/_dashboard_actions_strings';
import { DASHBOARD_APP_ID, DASHBOARD_UI_METRIC_ID } from '../../dashboard_constants';
import { useDashboardContainerContext } from '../../dashboard_container/dashboard_container_renderer';
@ -36,6 +37,7 @@ export function DashboardEditingToolbar() {
embeddable: { getStateTransfer, getEmbeddableFactory },
visualizations: { get: getVisualization, getAliases: getVisTypeAliases },
} = pluginServices.getServices();
const { euiTheme } = useEuiTheme();
const { embeddableInstance: dashboardContainer } = useDashboardContainerContext();
@ -178,8 +180,11 @@ export function DashboardEditingToolbar() {
}
return (
<>
<EuiHorizontalRule margin="none" />
<div
css={css`
padding: 0 ${euiTheme.size.s} ${euiTheme.size.s} ${euiTheme.size.s};
`}
>
<SolutionToolbar isDarkModeEnabled={IS_DARK_THEME}>
{{
primaryActionButton: (
@ -195,6 +200,6 @@ export function DashboardEditingToolbar() {
extraButtons,
}}
</SolutionToolbar>
</>
</div>
);
}

View file

@ -18,6 +18,7 @@ import { ViewMode } from '@kbn/embeddable-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { TopNavMenuProps } from '@kbn/navigation-plugin/public';
import { EuiHorizontalRule } from '@elastic/eui';
import {
getDashboardTitle,
leaveConfirmStrings,
@ -261,6 +262,7 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
</PresentationUtilContextProvider>
) : null}
{viewMode === ViewMode.EDIT ? <DashboardEditingToolbar /> : null}
<EuiHorizontalRule margin="none" />
</>
);
}

View file

@ -4,9 +4,9 @@
@import './component/panel/index';
@import './component/viewport/index';
.dashboardViewport {
flex: 1;
.dashboardContainer, .dashboardViewport {
display: flex;
flex: 1;
flex-direction: column;
}

View file

@ -33,8 +33,6 @@
*/
.dshLayout-isMaximizedPanel {
height: 100% !important; /* 1. */
width: 100%;
position: absolute !important; /* 1 */
}
/**

View file

@ -8,6 +8,7 @@
.dshDashboardViewport-controls {
margin: 0 $euiSizeS 0 $euiSizeS;
padding-top: $euiSizeS;
padding-bottom: $euiSizeXS;
}

View file

@ -11,24 +11,21 @@ import React, { useEffect, useRef } from 'react';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { ExitFullScreenButton } from '@kbn/shared-ux-button-exit-full-screen';
import { css } from '@emotion/react';
import { EuiPortal } from '@elastic/eui';
import { DashboardGrid } from '../grid';
import { pluginServices } from '../../../services/plugin_services';
import { DashboardEmptyScreen } from '../empty_screen/dashboard_empty_screen';
import { useDashboardContainerContext } from '../../dashboard_container_renderer';
export const DashboardViewport = () => {
export const DashboardViewportComponent = () => {
const {
settings: { isProjectEnabledInLabs },
} = pluginServices.getServices();
const controlsRoot = useRef(null);
const {
useEmbeddableDispatch,
useEmbeddableSelector: select,
actions: { setFullScreenMode },
embeddableInstance: dashboardContainer,
} = useDashboardContainerContext();
const dispatch = useEmbeddableDispatch();
const { useEmbeddableSelector: select, embeddableInstance: dashboardContainer } =
useDashboardContainerContext();
/**
* Render Control group
@ -47,9 +44,10 @@ export const DashboardViewport = () => {
const dashboardTitle = select((state) => state.explicitInput.title);
const useMargins = select((state) => state.explicitInput.useMargins);
const description = select((state) => state.explicitInput.description);
const isFullScreenMode = select((state) => state.componentState.fullScreenMode);
const isEmbeddedExternally = select((state) => state.componentState.isEmbeddedExternally);
const expandedPanelId = select((state) => state.componentState.expandedPanelId);
const expandedPanelStyles = css`
flex: 1;
`;
const controlsEnabled = isProjectEnabledInLabs('labs:dashboard:dashboardControls');
return (
@ -66,13 +64,8 @@ export const DashboardViewport = () => {
data-title={dashboardTitle}
data-description={description}
className={useMargins ? 'dshDashboardViewport-withMargins' : 'dshDashboardViewport'}
css={expandedPanelId ? expandedPanelStyles : undefined}
>
{isFullScreenMode && (
<ExitFullScreenButton
onExit={() => dispatch(setFullScreenMode(false))}
toggleChrome={!isEmbeddedExternally}
/>
)}
{panelCount === 0 && (
<div className="dshDashboardEmptyScreen">
<DashboardEmptyScreen isEditMode={viewMode === ViewMode.EDIT} />
@ -83,3 +76,38 @@ export const DashboardViewport = () => {
</>
);
};
// This fullscreen button HOC separates fullscreen button and dashboard content to reduce rerenders
// because ExitFullScreenButton sets isFullscreenMode to false on unmount while rerendering.
// This specifically fixed maximizing/minimizing panels without exiting fullscreen mode.
const WithFullScreenButton = ({ children }: { children: JSX.Element }) => {
const {
useEmbeddableDispatch,
useEmbeddableSelector: select,
actions: { setFullScreenMode },
} = useDashboardContainerContext();
const dispatch = useEmbeddableDispatch();
const isFullScreenMode = select((state) => state.componentState.fullScreenMode);
const isEmbeddedExternally = select((state) => state.componentState.isEmbeddedExternally);
return (
<>
{children}
{isFullScreenMode && (
<EuiPortal>
<ExitFullScreenButton
onExit={() => dispatch(setFullScreenMode(false))}
toggleChrome={!isEmbeddedExternally}
/>
</EuiPortal>
)}
</>
);
};
export const DashboardViewport = () => (
<WithFullScreenButton>
<DashboardViewportComponent />
</WithFullScreenButton>
);

View file

@ -10,10 +10,11 @@ import './_dashboard_container.scss';
import { v4 as uuidv4 } from 'uuid';
import classNames from 'classnames';
import { EuiLoadingElastic, EuiLoadingSpinner } from '@elastic/eui';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { EuiLoadingElastic, EuiLoadingSpinner, useEuiOverflowScroll } from '@elastic/eui';
import { css } from '@emotion/react';
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
import {
@ -110,13 +111,20 @@ export const DashboardContainerRenderer = ({
{ 'dashboardViewport--screenshotMode': isScreenshotMode() },
{ 'dashboardViewport--loading': loading }
);
const viewportStyles = css`
${useEuiOverflowScroll('y', false)}
`;
const loadingSpinner = showPlainSpinner ? (
<EuiLoadingSpinner size="xxl" />
) : (
<EuiLoadingElastic size="xxl" />
);
return (
<div className={viewportClasses}>{loading ? loadingSpinner : <div ref={dashboardRoot} />}</div>
<div className={viewportClasses} css={viewportStyles}>
{loading ? loadingSpinner : <div ref={dashboardRoot} />}
</div>
);
};

View file

@ -417,6 +417,8 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
}
this.domNode = dom;
this.domNode.className = 'dashboardContainer';
const { Wrapper: DashboardReduxWrapper } = this.reduxEmbeddableTools;
ReactDOM.render(
<I18nProvider>

View file

@ -131,6 +131,7 @@ export class PanelOptionsMenu extends React.Component<PanelOptionsMenuProps, Sta
? 'embeddablePanelContextMenuOpen'
: 'embeddablePanelContextMenuClosed'
}
repositionOnScroll
>
<EuiContextMenu
initialPanelId="mainMenu"

View file

@ -0,0 +1,98 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { FC, ReactElement, useCallback, useRef, useState } from 'react';
import { EuiPortal, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
export interface FloatingActionsProps {
className?: string;
actions?: JSX.Element;
children: ReactElement;
isEnabled?: boolean;
usingTwoLineLayout?: boolean;
}
export const FloatingActions: FC<FloatingActionsProps> = ({
className = '',
actions,
isEnabled,
usingTwoLineLayout,
children,
}) => {
const { euiTheme } = useEuiTheme();
const anchorRef = useRef<HTMLSpanElement>(null);
const actionsRef = useRef<HTMLDivElement>(null);
const [areFloatingActionsVisible, setFloatingActionsVisible] = useState<boolean>(false);
const showFloatingActions = useCallback(
() => isEnabled && setFloatingActionsVisible(true),
[isEnabled, setFloatingActionsVisible]
);
const hideFloatingActions = useCallback(
() => setFloatingActionsVisible(false),
[setFloatingActionsVisible]
);
const anchorBoundingRect = anchorRef.current?.getBoundingClientRect();
const actionsBoundingRect = actionsRef.current?.getBoundingClientRect();
const hiddenActionsStyles = `
visibility: hidden;
opacity: 0;
// slower transition on hover leave in case the user accidentally stops hover
transition: visibility 0.3s, opacity 0.3s;
`;
const visibleActionsStyles = `
transition: visibility 0.1s, opacity 0.1s;
visibility: visible;
opacity: 1;
`;
const floatingActionStyles =
anchorBoundingRect && actionsBoundingRect
? css`
top: ${anchorBoundingRect.top -
(usingTwoLineLayout ? parseInt(euiTheme.size.xs, 10) : parseInt(euiTheme.size.l, 10))}px;
left: ${anchorBoundingRect.right -
actionsBoundingRect.width -
parseInt(euiTheme.size.xs, 10)}px;
${areFloatingActionsVisible ? visibleActionsStyles : hiddenActionsStyles}
`
: undefined;
return (
<>
<span
className="floatingActions__anchor"
ref={anchorRef}
onMouseOver={showFloatingActions}
onFocus={showFloatingActions}
onMouseLeave={hideFloatingActions}
onBlur={hideFloatingActions}
>
{children}
</span>
<EuiPortal>
<div
ref={actionsRef}
className={className}
css={floatingActionStyles}
onMouseOver={showFloatingActions}
onFocus={showFloatingActions}
onMouseLeave={hideFloatingActions}
onBlur={hideFloatingActions}
>
{actions}
</div>
</EuiPortal>
</>
);
};

View file

@ -42,6 +42,8 @@ export const LazyDataViewPicker = React.lazy(() => import('./data_view_picker/da
export const LazyFieldPicker = React.lazy(() => import('./field_picker/field_picker'));
export { FloatingActions } from './floating_actions/floating_actions';
/**
* A lazily-loaded ExpressionInput component.
*/

View file

@ -1,5 +1,4 @@
.solutionToolbar {
padding: $euiSize $euiSizeS;
flex-grow: 0;
// Temporary fix for two tone icons to make them monochrome

View file

@ -36,6 +36,7 @@ export {
withSuspense,
LazyDataViewPicker,
LazyFieldPicker,
FloatingActions,
} from './components';
export {

View file

@ -85,6 +85,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dashboard.clearUnsavedChanges();
});
it('can make selections', async () => {
const firstId = (await dashboardControls.getAllControlIds())[0];
await dashboardControls.optionsListOpenPopover(firstId);
await dashboardControls.optionsListPopoverSelectOption('win xp');
await dashboardControls.optionsListPopoverSelectOption('osx');
await dashboardControls.optionsListEnsurePopoverIsClosed(firstId);
const selectionString = await dashboardControls.optionsListGetSelectionsString(firstId);
expect(selectionString).to.be('win xp, osx');
});
it('can add a second options list control with a non-default data view', async () => {
await dashboardControls.createControl({
controlType: OPTIONS_LIST_CONTROL,
@ -98,18 +109,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dashboard.clearUnsavedChanges();
});
it('renames an existing control', async () => {
const secondId = (await dashboardControls.getAllControlIds())[1];
const newTitle = 'wow! Animal sounds?';
await dashboardControls.editExistingControl(secondId);
await dashboardControls.controlEditorSetTitle(newTitle);
await dashboardControls.controlEditorSave();
expect(await dashboardControls.doesControlTitleExist(newTitle)).to.be(true);
await dashboard.clearUnsavedChanges();
});
it('can change the data view and field of an existing options list', async () => {
it('can change the data view and field of an existing options list and clears selections', async () => {
const firstId = (await dashboardControls.getAllControlIds())[0];
await dashboardControls.editExistingControl(firstId);
@ -120,6 +120,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dashboardControls.controlsEditorSetfield('animal.keyword', OPTIONS_LIST_CONTROL);
await dashboardControls.controlEditorSave();
const selectionString = await dashboardControls.optionsListGetSelectionsString(firstId);
expect(selectionString).to.be('Any');
// when creating a new filter, the ability to select a data view should be removed, because the dashboard now only has one data view
await retry.try(async () => {
await testSubjects.click('addFilter');
@ -127,37 +130,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await filterBar.ensureFieldEditorModalIsClosed();
expect(indexPatternSelectExists).to.be(false);
});
await dashboard.clearUnsavedChanges();
});
it('editing field clears selections', async () => {
it('renames an existing control and retains selection', async () => {
const secondId = (await dashboardControls.getAllControlIds())[1];
await dashboardControls.optionsListOpenPopover(secondId);
await dashboardControls.optionsListPopoverSelectOption('hiss');
await dashboardControls.optionsListEnsurePopoverIsClosed(secondId);
const newTitle = 'wow! Animal sounds?';
await testSubjects.click(`control-action-${secondId}-edit`);
await dashboardControls.editExistingControl(secondId);
await dashboardControls.controlsEditorSetfield('animal.keyword', OPTIONS_LIST_CONTROL);
await dashboardControls.controlEditorSetTitle(newTitle);
await dashboardControls.controlEditorSetWidth('small');
await dashboardControls.controlEditorSave();
expect(await dashboardControls.doesControlTitleExist(newTitle)).to.be(true);
const selectionString = await dashboardControls.optionsListGetSelectionsString(secondId);
expect(selectionString).to.be('Any');
});
expect(selectionString).to.be('hiss');
it('editing other control settings keeps selections', async () => {
const secondId = (await dashboardControls.getAllControlIds())[1];
await dashboardControls.optionsListOpenPopover(secondId);
await dashboardControls.optionsListPopoverSelectOption('dog');
await dashboardControls.optionsListPopoverSelectOption('cat');
await dashboardControls.optionsListEnsurePopoverIsClosed(secondId);
await dashboardControls.editExistingControl(secondId);
await dashboardControls.controlEditorSetTitle('Animal');
await dashboardControls.controlEditorSetWidth('large');
await dashboardControls.controlEditorSave();
const selectionString = await dashboardControls.optionsListGetSelectionsString(secondId);
expect(selectionString).to.be('dog, cat');
await dashboard.clearUnsavedChanges();
});
it('deletes an existing control', async () => {

View file

@ -80,6 +80,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dashboard.clearUnsavedChanges();
});
it('can select a range', async () => {
const firstId = (await dashboardControls.getAllControlIds())[0];
await dashboardControls.rangeSliderSetLowerBound(firstId, '50');
await dashboardControls.rangeSliderSetUpperBound(firstId, '100');
await dashboardControls.validateRange('value', firstId, '50', '100');
await dashboard.clearUnsavedChanges();
});
it('can add a second range list control with a non-default data view', async () => {
await dashboardControls.createControl({
controlType: RANGE_SLIDER_CONTROL,
@ -90,22 +99,29 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await dashboardControls.getControlsCount()).to.be(2);
const secondId = (await dashboardControls.getAllControlIds())[1];
await dashboardControls.validateRange('placeholder', secondId, '100', '1200');
await dashboardControls.rangeSliderSetLowerBound(secondId, '200');
await dashboardControls.rangeSliderSetUpperBound(secondId, '1000');
await dashboardControls.validateRange('value', secondId, '200', '1000');
// data views should be properly propagated from the control group to the dashboard
expect(await filterBar.getIndexPatterns()).to.be('logstash-*,kibana_sample_data_flights');
await dashboard.clearUnsavedChanges();
});
it('renames an existing control', async () => {
it('edits title and size of an existing control and retains existing range selection', async () => {
const secondId = (await dashboardControls.getAllControlIds())[1];
const newTitle = 'Average ticket price';
await dashboardControls.editExistingControl(secondId);
await dashboardControls.controlEditorSetTitle(newTitle);
await dashboardControls.controlEditorSetWidth('large');
await dashboardControls.controlEditorSave();
expect(await dashboardControls.doesControlTitleExist(newTitle)).to.be(true);
await dashboardControls.validateRange('value', secondId, '200', '1000');
await dashboard.clearUnsavedChanges();
});
it('can edit range slider control', async () => {
it('can change data view and field of range slider control and clears existing selection', async () => {
const firstId = (await dashboardControls.getAllControlIds())[0];
await dashboardControls.editExistingControl(firstId);
@ -117,6 +133,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dashboardControls.controlEditorSave();
await dashboardControls.rangeSliderWaitForLoading();
await dashboardControls.validateRange('placeholder', firstId, '0', '6');
await dashboardControls.validateRange('value', firstId, '', '');
// when creating a new filter, the ability to select a data view should be removed, because the dashboard now only has one data view
await retry.try(async () => {
@ -152,37 +169,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dashboardControls.rangeSliderWaitForLoading();
const secondId = (await dashboardControls.getAllControlIds())[1];
await dashboardControls.validateRange('placeholder', secondId, '100', '1000');
});
it('editing field clears selections', async () => {
const secondId = (await dashboardControls.getAllControlIds())[1];
await dashboardControls.editExistingControl(secondId);
await dashboardControls.controlsEditorSetfield('FlightDelayMin', RANGE_SLIDER_CONTROL);
await dashboardControls.controlEditorSave();
await dashboardControls.rangeSliderWaitForLoading();
await dashboardControls.validateRange('value', secondId, '', '');
});
it('editing other control settings keeps selections', async () => {
const secondId = (await dashboardControls.getAllControlIds())[1];
await dashboardControls.rangeSliderSetLowerBound(secondId, '50');
await dashboardControls.rangeSliderSetUpperBound(secondId, '100');
await dashboardControls.rangeSliderWaitForLoading();
await dashboardControls.editExistingControl(secondId);
await dashboardControls.controlEditorSetTitle('Minimum Flight Delay');
await dashboardControls.controlEditorSetWidth('large');
await dashboardControls.controlEditorSave();
await dashboardControls.rangeSliderWaitForLoading();
await dashboardControls.validateRange('value', secondId, '50', '100');
await dashboard.clearUnsavedChanges();
});
it('can clear out selections by clicking the reset button', async () => {
const firstId = (await dashboardControls.getAllControlIds())[0];
await dashboardControls.rangeSliderClearSelection(firstId);
await dashboardControls.validateRange('value', firstId, '', '');
await dashboardControls.rangeSliderEnsurePopoverIsClosed(firstId);
await dashboard.clearUnsavedChanges();
});

View file

@ -325,6 +325,7 @@ export class DashboardPageObject extends FtrService {
await this.testSubjects.existOrFail('dashboardUnsavedChangesBadge');
await this.clickQuickSave();
await this.testSubjects.missingOrFail('dashboardUnsavedChangesBadge');
await this.testSubjects.click('toastCloseButton');
});
if (switchMode) {
await this.clickCancelOutOfEditMode();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Before After
Before After

View file

@ -117,12 +117,17 @@ export class DashboardAddPanelService extends FtrService {
// Clear all toasts that could hide pagination controls
await this.common.clearAllToasts();
const isNext = await this.testSubjects.exists('pagination-button-next');
const addPanel = await this.testSubjects.find('dashboardAddPanel');
const isNext = await this.testSubjects.descendantExists('pagination-button-next', addPanel);
if (!isNext) {
return false;
}
const pagerNextButton = await this.testSubjects.find('pagination-button-next');
const pagerNextButton = await this.testSubjects.findDescendant(
'pagination-button-next',
addPanel
);
const isDisabled = await pagerNextButton.getAttribute('disabled');
if (isDisabled != null) {

View file

@ -21,6 +21,7 @@ exports[`TooltipPopover render should render tooltip popover 1`] = `
isOpen={true}
ownFocus={true}
panelPaddingSize="m"
repositionOnScroll={true}
style={
Object {
"pointerEvents": "none",
@ -80,6 +81,7 @@ exports[`TooltipPopover render should render tooltip popover with custom tooltip
isOpen={true}
ownFocus={true}
panelPaddingSize="m"
repositionOnScroll={true}
style={
Object {
"pointerEvents": "none",

View file

@ -157,6 +157,7 @@ export class TooltipPopover extends Component<Props, State> {
pointerEvents: 'none',
transform: `translate(${this.state.x - 13 - offset}px, ${this.state.y - 13}px)`,
}}
repositionOnScroll
>
{this._renderTooltipContent()}
</EuiPopover>