mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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  #### After  ### 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:
parent
f439bdc2b3
commit
1f03570126
25 changed files with 283 additions and 152 deletions
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,12 @@
|
|||
@import 'src/core/public/mixins';
|
||||
|
||||
.dshAppWrapper {
|
||||
@include kibanaFullBodyHeight();
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dshUnsavedListingItem {
|
||||
margin-top: $euiSizeM;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
@import './component/panel/index';
|
||||
@import './component/viewport/index';
|
||||
|
||||
.dashboardViewport {
|
||||
flex: 1;
|
||||
.dashboardContainer, .dashboardViewport {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
|
|
@ -33,8 +33,6 @@
|
|||
*/
|
||||
.dshLayout-isMaximizedPanel {
|
||||
height: 100% !important; /* 1. */
|
||||
width: 100%;
|
||||
position: absolute !important; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
.dshDashboardViewport-controls {
|
||||
margin: 0 $euiSizeS 0 $euiSizeS;
|
||||
padding-top: $euiSizeS;
|
||||
padding-bottom: $euiSizeXS;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -131,6 +131,7 @@ export class PanelOptionsMenu extends React.Component<PanelOptionsMenuProps, Sta
|
|||
? 'embeddablePanelContextMenuOpen'
|
||||
: 'embeddablePanelContextMenuClosed'
|
||||
}
|
||||
repositionOnScroll
|
||||
>
|
||||
<EuiContextMenu
|
||||
initialPanelId="mainMenu"
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
.solutionToolbar {
|
||||
padding: $euiSize $euiSizeS;
|
||||
flex-grow: 0;
|
||||
|
||||
// Temporary fix for two tone icons to make them monochrome
|
||||
|
|
|
@ -36,6 +36,7 @@ export {
|
|||
withSuspense,
|
||||
LazyDataViewPicker,
|
||||
LazyFieldPicker,
|
||||
FloatingActions,
|
||||
} from './components';
|
||||
|
||||
export {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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 |
Binary file not shown.
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 166 KiB |
|
@ -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) {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue