[Canvas] Toolbar UI Updates (#113329)

This commit is contained in:
Catherine Liu 2021-10-12 11:53:40 -07:00 committed by GitHub
parent d50ec56ed1
commit 16c049a2d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 267 additions and 251 deletions

View file

@ -543,7 +543,6 @@ export function DashboardTopNav({
createType: title,
onClick: createNewVisType(visType as VisTypeAlias),
'data-test-subj': `dashboardQuickButton${name}`,
isDarkModeEnabled: IS_DARK_THEME,
};
} else {
const { name, icon, title, titleInWizard } = visType as BaseVisType;
@ -553,7 +552,6 @@ export function DashboardTopNav({
createType: titleInWizard || title,
onClick: createNewVisType(visType as BaseVisType),
'data-test-subj': `dashboardQuickButton${name}`,
isDarkModeEnabled: IS_DARK_THEME,
};
}
}

View file

@ -238,16 +238,18 @@ export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => {
panelPaddingSize="none"
data-test-subj="dashboardEditorMenuButton"
>
<EuiContextMenu
initialPanelId={0}
panels={editorMenuPanels}
className={`dshSolutionToolbar__editorContextMenu ${
IS_DARK_THEME
? 'dshSolutionToolbar__editorContextMenu--dark'
: 'dshSolutionToolbar__editorContextMenu--light'
}`}
data-test-subj="dashboardEditorContextMenu"
/>
{() => (
<EuiContextMenu
initialPanelId={0}
panels={editorMenuPanels}
className={`dshSolutionToolbar__editorContextMenu ${
IS_DARK_THEME
? 'dshSolutionToolbar__editorContextMenu--dark'
: 'dshSolutionToolbar__editorContextMenu--light'
}`}
data-test-subj="dashboardEditorContextMenu"
/>
)}
</SolutionToolbarPopover>
);
};

View file

@ -18,13 +18,17 @@ type AllowedPopoverProps = Omit<
'button' | 'isOpen' | 'closePopover' | 'anchorPosition'
>;
export type Props = AllowedButtonProps & AllowedPopoverProps;
export type Props = AllowedButtonProps &
AllowedPopoverProps & {
children: (arg: { closePopover: () => void }) => React.ReactNode;
};
export const SolutionToolbarPopover = ({
label,
iconType,
primary,
iconSide,
children,
...popover
}: Props) => {
const [isOpen, setIsOpen] = useState(false);
@ -33,10 +37,21 @@ export const SolutionToolbarPopover = ({
const closePopover = () => setIsOpen(false);
const button = (
<SolutionToolbarButton {...{ label, iconType, primary, iconSide }} onClick={onButtonClick} />
<SolutionToolbarButton
{...{ label, iconType, primary, iconSide }}
onClick={onButtonClick}
data-test-subj={popover['data-test-subj']}
/>
);
return (
<EuiPopover anchorPosition="downLeft" {...{ isOpen, button, closePopover }} {...popover} />
<EuiPopover
anchorPosition="downLeft"
panelPaddingSize="none"
{...{ isOpen, button, closePopover }}
{...popover}
>
{children({ closePopover })}
</EuiPopover>
);
};

View file

@ -8,17 +8,4 @@
border-color: $euiBorderColor !important;
}
}
// Temporary fix for two tone icons to make them monochrome
.quickButtonGroup__button--dark {
.euiIcon path {
fill: $euiColorGhost;
}
}
// Temporary fix for two tone icons to make them monochrome
.quickButtonGroup__button--light {
.euiIcon path {
fill: $euiColorInk;
}
}
}

View file

@ -17,27 +17,23 @@ import './quick_group.scss';
export interface QuickButtonProps extends Pick<EuiButtonGroupOptionProps, 'iconType'> {
createType: string;
onClick: () => void;
isDarkModeEnabled?: boolean;
}
export interface Props {
buttons: QuickButtonProps[];
}
type Option = EuiButtonGroupOptionProps &
Omit<QuickButtonProps, 'createType' | 'isDarkModeEnabled'>;
type Option = EuiButtonGroupOptionProps & Omit<QuickButtonProps, 'createType'>;
export const QuickButtonGroup = ({ buttons }: Props) => {
const buttonGroupOptions: Option[] = buttons.map((button: QuickButtonProps, index) => {
const { createType: label, isDarkModeEnabled, ...rest } = button;
const { createType: label, ...rest } = button;
const title = strings.getAriaButtonLabel(label);
return {
...rest,
'aria-label': title,
className: `quickButtonGroup__button ${
isDarkModeEnabled ? 'quickButtonGroup__button--dark' : 'quickButtonGroup__button--light'
}`,
className: `quickButtonGroup__button`,
id: `${htmlIdGenerator()()}${index}`,
label,
title,

View file

@ -54,29 +54,31 @@ const primaryButtonConfigs = {
panelPaddingSize="none"
primary={true}
>
<EuiContextMenu
initialPanelId={0}
panels={[
{
id: 0,
title: 'Open editor',
items: [
{
name: 'Lens',
icon: 'lensApp',
},
{
name: 'Maps',
icon: 'logoMaps',
},
{
name: 'TSVB',
icon: 'visVisualBuilder',
},
],
},
]}
/>
{() => (
<EuiContextMenu
initialPanelId={0}
panels={[
{
id: 0,
title: 'Open editor',
items: [
{
name: 'Lens',
icon: 'lensApp',
},
{
name: 'Maps',
icon: 'logoMaps',
},
{
name: 'TSVB',
icon: 'visVisualBuilder',
},
],
},
]}
/>
)}
</SolutionToolbarPopover>
),
Dashboard: (
@ -93,29 +95,31 @@ const extraButtonConfigs = {
Canvas: undefined,
Dashboard: [
<SolutionToolbarPopover iconType="visualizeApp" label="All editors" panelPaddingSize="none">
<EuiContextMenu
initialPanelId={0}
panels={[
{
id: 0,
title: 'Open editor',
items: [
{
name: 'Lens',
icon: 'lensApp',
},
{
name: 'Maps',
icon: 'logoMaps',
},
{
name: 'TSVB',
icon: 'visVisualBuilder',
},
],
},
]}
/>
{() => (
<EuiContextMenu
initialPanelId={0}
panels={[
{
id: 0,
title: 'Open editor',
items: [
{
name: 'Lens',
icon: 'lensApp',
},
{
name: 'Maps',
icon: 'logoMaps',
},
{
name: 'TSVB',
icon: 'visVisualBuilder',
},
],
},
]}
/>
)}
</SolutionToolbarPopover>,
],
};

View file

@ -31,7 +31,7 @@ $canvasLayoutFontSize: $euiFontSizeS;
.canvasLayout__stageHeader {
flex-grow: 0;
flex-basis: auto;
padding: 1px $euiSize 0;
padding: $euiSizeS;
font-size: $canvasLayoutFontSize;
border-bottom: $euiBorderThin;
background: $euiColorLightestShade;

View file

@ -3,13 +3,13 @@
exports[`Storyshots components/WorkpadHeader/ElementMenu default 1`] = `
<div
className="euiPopover euiPopover--anchorDownLeft"
data-test-subj="add-element-button"
>
<div
className="euiPopover__anchor"
>
<button
aria-label="Add an element"
className="euiButton euiButton--primary euiButton--small euiButton--fill canvasElementMenu__popoverButton"
className="euiButton euiButton--primary euiButton--fill solutionToolbarButton undefined"
data-test-subj="add-element-button"
disabled={false}
onClick={[Function]}

View file

@ -129,12 +129,6 @@ You can use standard Markdown in here, but you can also access your piped-in dat
},
};
const mockRenderEmbedPanel = () => <div id="embeddablePanel" />;
storiesOf('components/WorkpadHeader/ElementMenu', module).add('default', () => (
<ElementMenu
elements={testElements}
addElement={action('addElement')}
renderEmbedPanel={mockRenderEmbedPanel}
/>
<ElementMenu elements={testElements} addElement={action('addElement')} />
));

View file

@ -6,17 +6,13 @@
*/
import { sortBy } from 'lodash';
import React, { Fragment, FunctionComponent, useState } from 'react';
import React, { FunctionComponent, useState } from 'react';
import PropTypes from 'prop-types';
import {
EuiButton,
EuiContextMenu,
EuiIcon,
EuiContextMenuPanelItemDescriptor,
} from '@elastic/eui';
import { EuiContextMenu, EuiIcon, EuiContextMenuPanelItemDescriptor } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { PrimaryActionPopover } from '../../../../../../../src/plugins/presentation_util/public';
import { getId } from '../../../lib/get_id';
import { Popover, ClosePopoverFn } from '../../popover';
import { ClosePopoverFn } from '../../popover';
import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib';
import { ElementSpec } from '../../../../types';
import { flattenPanelTree } from '../../../lib/flatten_panel_tree';
@ -116,7 +112,7 @@ const categorizeElementsByType = (elements: ElementSpec[]): { [key: string]: Ele
return categories;
};
export interface Props {
interface Props {
/**
* Dictionary of elements from elements registry
*/
@ -125,25 +121,14 @@ export interface Props {
* Handler for adding a selected element to the workpad
*/
addElement: (element: ElementSpec) => void;
/**
* Renders embeddable flyout
*/
renderEmbedPanel: (onClose: () => void) => JSX.Element;
}
export const ElementMenu: FunctionComponent<Props> = ({
elements,
addElement,
renderEmbedPanel,
}) => {
export const ElementMenu: FunctionComponent<Props> = ({ elements, addElement }) => {
const [isAssetModalVisible, setAssetModalVisible] = useState(false);
const [isEmbedPanelVisible, setEmbedPanelVisible] = useState(false);
const [isSavedElementsModalVisible, setSavedElementsModalVisible] = useState(false);
const hideAssetModal = () => setAssetModalVisible(false);
const showAssetModal = () => setAssetModalVisible(true);
const hideEmbedPanel = () => setEmbedPanelVisible(false);
const showEmbedPanel = () => setEmbedPanelVisible(true);
const hideSavedElementsModal = () => setSavedElementsModalVisible(false);
const showSavedElementsModal = () => setSavedElementsModalVisible(true);
@ -214,47 +199,28 @@ export const ElementMenu: FunctionComponent<Props> = ({
closePopover();
},
},
{
name: strings.getEmbedObjectMenuItemLabel(),
className: CONTEXT_MENU_TOP_BORDER_CLASSNAME,
icon: <EuiIcon type="logoKibana" size="m" />,
onClick: () => {
showEmbedPanel();
closePopover();
},
},
],
};
};
const exportControl = (togglePopover: React.MouseEventHandler<any>) => (
<EuiButton
fill
iconType="plusInCircle"
size="s"
aria-label={strings.getElementMenuLabel()}
onClick={togglePopover}
className="canvasElementMenu__popoverButton"
data-test-subj="add-element-button"
>
{strings.getElementMenuButtonLabel()}
</EuiButton>
);
return (
<Fragment>
<Popover button={exportControl} panelPaddingSize="none" anchorPosition="downLeft">
<>
<PrimaryActionPopover
panelPaddingSize="none"
label={strings.getElementMenuButtonLabel()}
iconType="plusInCircle"
data-test-subj="add-element-button"
>
{({ closePopover }: { closePopover: ClosePopoverFn }) => (
<EuiContextMenu
initialPanelId={0}
panels={flattenPanelTree(getPanelTree(closePopover))}
/>
)}
</Popover>
</PrimaryActionPopover>
{isAssetModalVisible ? <AssetManager onClose={hideAssetModal} /> : null}
{isEmbedPanelVisible ? renderEmbedPanel(hideEmbedPanel) : null}
{isSavedElementsModalVisible ? <SavedElementsModal onClose={hideSavedElementsModal} /> : null}
</Fragment>
</>
);
};

View file

@ -1,3 +0,0 @@
.canvasElementMenu__popoverButton {
margin-right: $euiSizeS;
}

View file

@ -5,44 +5,4 @@
* 2.0.
*/
import React from 'react';
import { connect } from 'react-redux';
import { compose, withProps } from 'recompose';
import { Dispatch } from 'redux';
import { State, ElementSpec } from '../../../../types';
// @ts-expect-error untyped local
import { elementsRegistry } from '../../../lib/elements_registry';
import { ElementMenu as Component, Props as ComponentProps } from './element_menu.component';
// @ts-expect-error untyped local
import { addElement } from '../../../state/actions/elements';
import { getSelectedPage } from '../../../state/selectors/workpad';
import { AddEmbeddablePanel } from '../../embeddable_flyout';
interface StateProps {
pageId: string;
}
interface DispatchProps {
addElement: (pageId: string) => (partialElement: ElementSpec) => void;
}
const mapStateToProps = (state: State) => ({
pageId: getSelectedPage(state),
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
addElement: (pageId: string) => (element: ElementSpec) => dispatch(addElement(pageId, element)),
});
const mergeProps = (stateProps: StateProps, dispatchProps: DispatchProps) => ({
...stateProps,
...dispatchProps,
addElement: dispatchProps.addElement(stateProps.pageId),
// Moved this section out of the main component to enable stories
renderEmbedPanel: (onClose: () => void) => <AddEmbeddablePanel onClose={onClose} />,
});
export const ElementMenu = compose<ComponentProps, {}>(
connect(mapStateToProps, mapDispatchToProps, mergeProps),
withProps(() => ({ elements: elementsRegistry.toJS() }))
)(Component);
export * from './element_menu.component';

View file

@ -5,13 +5,19 @@
* 2.0.
*/
import React, { FunctionComponent } from 'react';
import React, { FC, useCallback, useState } from 'react';
import PropTypes from 'prop-types';
// @ts-expect-error no @types definition
import { Shortcuts } from 'react-shortcuts';
import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
AddFromLibraryButton,
QuickButtonGroup,
SolutionToolbar,
} from '../../../../../../src/plugins/presentation_util/public';
import { getElementStrings } from '../../../i18n';
import { CommitFn, ElementSpec } from '../../../types';
import { ToolTipShortcut } from '../tool_tip_shortcut/';
import { RefreshControl } from './refresh_control';
// @ts-expect-error untyped local
@ -21,7 +27,6 @@ import { ElementMenu } from './element_menu';
import { ShareMenu } from './share_menu';
import { ViewMenu } from './view_menu';
import { LabsControl } from './labs_control';
import { CommitFn } from '../../../types';
const strings = {
getFullScreenButtonAriaLabel: () =>
@ -46,19 +51,30 @@ const strings = {
}),
};
const elementStrings = getElementStrings();
export interface Props {
isWriteable: boolean;
canUserWrite: boolean;
commit: CommitFn;
onSetWriteable?: (writeable: boolean) => void;
renderEmbedPanel: (onClick: () => void) => JSX.Element;
elements: { [key: string]: ElementSpec };
addElement: (element: Partial<ElementSpec>) => void;
}
export const WorkpadHeader: FunctionComponent<Props> = ({
export const WorkpadHeader: FC<Props> = ({
isWriteable,
canUserWrite,
commit,
onSetWriteable = () => {},
renderEmbedPanel,
elements,
addElement,
}) => {
const [isEmbedPanelVisible, setEmbedPanelVisible] = useState(false);
const hideEmbedPanel = () => setEmbedPanelVisible(false);
const showEmbedPanel = () => setEmbedPanelVisible(true);
const toggleWriteable = () => onSetWriteable(!isWriteable);
const keyHandler = (action: string) => {
@ -111,65 +127,104 @@ export const WorkpadHeader: FunctionComponent<Props> = ({
);
};
const createElement = useCallback(
(elementName: string) => () => {
const elementSpec = elements[elementName];
if (elementSpec) {
addElement(elements[elementName]);
}
},
[addElement, elements]
);
const quickButtons = [
{
iconType: 'visText',
createType: elementStrings.markdown.displayName,
onClick: createElement('markdown'),
},
{
iconType: 'node',
createType: elementStrings.shape.displayName,
onClick: createElement('shape'),
},
{
iconType: 'image',
createType: elementStrings.image.displayName,
onClick: createElement('image'),
},
];
return (
<EuiFlexGroup
gutterSize="none"
alignItems="center"
justifyContent="spaceBetween"
className="canvasLayout__stageHeaderInner"
>
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="none">
{isWriteable && (
<EuiFlexItem grow={false}>
<ElementMenu />
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<ViewMenu />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EditMenu commit={commit} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ShareMenu />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LabsControl />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
{canUserWrite && (
<Shortcuts
name="EDITOR"
handler={keyHandler}
targetNodeSelector="body"
global
isolate
/>
<>
<EuiFlexGroup
gutterSize="none"
alignItems="center"
justifyContent="spaceBetween"
className="canvasLayout__stageHeaderInner"
>
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="none">
{isWriteable && (
<EuiFlexItem>
<SolutionToolbar>
{{
primaryActionButton: (
<ElementMenu addElement={addElement} elements={elements} />
),
quickButtonGroup: <QuickButtonGroup buttons={quickButtons} />,
addFromLibraryButton: <AddFromLibraryButton onClick={showEmbedPanel} />,
}}
</SolutionToolbar>
</EuiFlexItem>
)}
<EuiToolTip position="bottom" content={getEditToggleToolTip()}>
<EuiButtonIcon
iconType={isWriteable ? 'eyeClosed' : 'eye'}
onClick={toggleWriteable}
size="s"
aria-label={getEditToggleToolTipText()}
isDisabled={!canUserWrite}
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<RefreshControl />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FullscreenControl>{fullscreenButton}</FullscreenControl>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexItem grow={false}>
<ViewMenu />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EditMenu commit={commit} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ShareMenu />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LabsControl />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
{canUserWrite && (
<Shortcuts
name="EDITOR"
handler={keyHandler}
targetNodeSelector="body"
global
isolate
/>
)}
<EuiToolTip position="bottom" content={getEditToggleToolTip()}>
<EuiButtonIcon
iconType={isWriteable ? 'eyeClosed' : 'eye'}
onClick={toggleWriteable}
size="s"
aria-label={getEditToggleToolTipText()}
isDisabled={!canUserWrite}
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<RefreshControl />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FullscreenControl>{fullscreenButton}</FullscreenControl>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
{isEmbedPanelVisible ? renderEmbedPanel(hideEmbedPanel) : null}
</>
);
};
@ -178,4 +233,7 @@ WorkpadHeader.propTypes = {
commit: PropTypes.func.isRequired,
onSetWriteable: PropTypes.func,
canUserWrite: PropTypes.bool,
renderEmbedPanel: PropTypes.func.isRequired,
elements: PropTypes.object.isRequired,
addElement: PropTypes.func.isRequired,
};

View file

@ -5,22 +5,61 @@
* 2.0.
*/
import React from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { Action } from 'redux-actions';
// @ts-expect-error untyped local
import { elementsRegistry } from '../../lib/elements_registry';
import { canUserWrite } from '../../state/selectors/app';
import { getSelectedPage, isWriteable } from '../../state/selectors/workpad';
import { setWriteable } from '../../state/actions/workpad';
import { State } from '../../../types';
import { WorkpadHeader as Component } from './workpad_header.component';
// @ts-expect-error untyped local
import { addElement } from '../../state/actions/elements';
import { CommitFn, ElementSpec, State } from '../../../types';
import { WorkpadHeader as Component, Props as ComponentProps } from './workpad_header.component';
import { AddEmbeddablePanel } from '../embeddable_flyout';
const mapStateToProps = (state: State) => ({
interface Props {
commit: CommitFn;
}
interface StateProps {
isWriteable: boolean;
canUserWrite: boolean;
selectedPage: string;
pageId: string;
}
interface DispatchProps {
onSetWriteable: (isWorkpadWriteable: boolean) => Action<boolean>;
addElement: (pageId: string) => (partialElement: Partial<ElementSpec>) => void;
}
const mapStateToProps = (state: State): StateProps => ({
isWriteable: isWriteable(state) && canUserWrite(state),
canUserWrite: canUserWrite(state),
selectedPage: getSelectedPage(state),
pageId: getSelectedPage(state),
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
onSetWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)),
addElement: (pageId: string) => (element: Partial<ElementSpec>) =>
dispatch(addElement(pageId, element)),
});
export const WorkpadHeader = connect(mapStateToProps, mapDispatchToProps)(Component);
const mergeProps = (
stateProps: StateProps,
dispatchProps: DispatchProps,
ownProps: Props
): ComponentProps => ({
...stateProps,
...dispatchProps,
...ownProps,
renderEmbedPanel: (onClose: () => void) => <AddEmbeddablePanel onClose={onClose} />,
addElement: dispatchProps.addElement(stateProps.pageId),
elements: elementsRegistry.toJS(),
});
export const WorkpadHeader = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component);

View file

@ -36,7 +36,6 @@
@import '../components/toolbar/toolbar';
@import '../components/toolbar/tray/tray';
@import '../components/workpad/workpad';
@import '../components/workpad_header/element_menu/element_menu';
@import '../components/workpad_header/share_menu/share_menu';
@import '../components/workpad_header/view_menu/view_menu';
@import '../components/workpad_page/workpad_page';

View file

@ -42,6 +42,7 @@
{ "path": "../../../src/plugins/kibana_legacy/tsconfig.json" },
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
{ "path": "../../../src/plugins/kibana_utils/tsconfig.json" },
{ "path": "../../../src/plugins/presentation_util/tsconfig.json" },
{ "path": "../../../src/plugins/saved_objects/tsconfig.json" },
{ "path": "../../../src/plugins/ui_actions/tsconfig.json" },
{ "path": "../../../src/plugins/usage_collection/tsconfig.json" },