mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Controls] Improve controls empty state (#125728)
* Add controls button to toolbar * Add dismiss button * Add style to toolbar controls button * Clean up unnecessary isControlsEnabled check * Make toolbar controls button conditional once callout dismissed * Move add and edit controls to toolbar dropdown * Remove icon buttons * Add each control seperately to toolbar dropdown * Remove unused code * Fix close popover on click * Remove unnecessary dark theme check * Make closePopover optional for creating controls * Fix control group strings * Fix alignment of toolbar popover items * Functional tests - create controls from new menu button * Hide controls callout for empty dashboards * Add tooltips to control types + i18n support. * Move callout render logic to dashboard viewport * Add controls callout functional tests * Fix bundle size by lazy importing controls callout * Get create control button in callout via passed function * Fix mobile view of callout * Add documentation and cleaned code based on Devon's feedback * Moved the 'add to library' and 'controls' buttons in to extra
This commit is contained in:
parent
ca447c66ce
commit
b2cd94df7b
22 changed files with 619 additions and 395 deletions
|
@ -8,14 +8,7 @@
|
|||
|
||||
import '../control_group.scss';
|
||||
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiToolTip,
|
||||
EuiPanel,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
|
@ -35,24 +28,13 @@ import {
|
|||
useSensors,
|
||||
LayoutMeasuringStrategy,
|
||||
} from '@dnd-kit/core';
|
||||
|
||||
import { ControlGroupInput } from '../types';
|
||||
import { pluginServices } from '../../services';
|
||||
import { ViewMode } from '../../../../embeddable/public';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { CreateControlButton } from '../editor/create_control';
|
||||
import { EditControlGroup } from '../editor/edit_control_group';
|
||||
import { forwardAllContext } from '../editor/forward_all_context';
|
||||
import { controlGroupReducers } from '../state/control_group_reducers';
|
||||
import { ControlClone, SortableControl } from './control_group_sortable_item';
|
||||
import { useReduxContainerContext } from '../../../../presentation_util/public';
|
||||
import { ControlsIllustration } from './controls_illustration';
|
||||
|
||||
export const ControlGroup = () => {
|
||||
// Controls Services Context
|
||||
const { overlays } = pluginServices.getHooks();
|
||||
const { openFlyout } = overlays.useService();
|
||||
|
||||
// Redux embeddable container Context
|
||||
const reduxContainerContext = useReduxContainerContext<
|
||||
ControlGroupInput,
|
||||
|
@ -114,115 +96,69 @@ export const ControlGroup = () => {
|
|||
if (draggingId) panelBg = 'success';
|
||||
|
||||
return (
|
||||
<EuiPanel
|
||||
borderRadius="m"
|
||||
color={panelBg}
|
||||
paddingSize={emptyState ? 's' : 'none'}
|
||||
className={classNames('controlsWrapper', {
|
||||
'controlsWrapper--empty': emptyState,
|
||||
'controlsWrapper--twoLine': controlStyle === 'twoLine',
|
||||
})}
|
||||
>
|
||||
<>
|
||||
{idsInOrder.length > 0 ? (
|
||||
<EuiFlexGroup
|
||||
wrap={false}
|
||||
gutterSize="m"
|
||||
direction="row"
|
||||
responsive={false}
|
||||
alignItems="center"
|
||||
data-test-subj="controls-group"
|
||||
data-shared-items-count={idsInOrder.length}
|
||||
<EuiPanel
|
||||
borderRadius="m"
|
||||
color={panelBg}
|
||||
paddingSize={emptyState ? 's' : 'none'}
|
||||
className={classNames('controlsWrapper', {
|
||||
'controlsWrapper--empty': emptyState,
|
||||
'controlsWrapper--twoLine': controlStyle === 'twoLine',
|
||||
})}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<DndContext
|
||||
onDragStart={({ active }) => setDraggingId(active.id)}
|
||||
onDragEnd={onDragEnd}
|
||||
onDragCancel={() => setDraggingId(null)}
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
layoutMeasuring={{
|
||||
strategy: LayoutMeasuringStrategy.Always,
|
||||
}}
|
||||
>
|
||||
<SortableContext items={idsInOrder} strategy={rectSortingStrategy}>
|
||||
<EuiFlexGroup
|
||||
className={classNames('controlGroup', { 'controlGroup-isDragging': draggingId })}
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
wrap={true}
|
||||
>
|
||||
{idsInOrder.map(
|
||||
(controlId, index) =>
|
||||
panels[controlId] && (
|
||||
<SortableControl
|
||||
isEditable={isEditable}
|
||||
dragInfo={{ index, draggingIndex }}
|
||||
embeddableId={controlId}
|
||||
key={controlId}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</SortableContext>
|
||||
<DragOverlay>
|
||||
{draggingId ? <ControlClone draggingId={draggingId} /> : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</EuiFlexItem>
|
||||
{isEditable && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup responsive={false} className="groupEditActions" gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
<EuiToolTip content={ControlGroupStrings.management.getManageButtonTitle()}>
|
||||
<EuiButtonIcon
|
||||
aria-label={ControlGroupStrings.management.getManageButtonTitle()}
|
||||
iconType="gear"
|
||||
color="text"
|
||||
data-test-subj="controls-sorting-button"
|
||||
onClick={() => {
|
||||
const flyoutInstance = openFlyout(
|
||||
forwardAllContext(
|
||||
<EditControlGroup closeFlyout={() => flyoutInstance.close()} />,
|
||||
reduxContainerContext
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiToolTip content={ControlGroupStrings.management.getAddControlTitle()}>
|
||||
<CreateControlButton isIconButton={true} />
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs" data-test-subj="controls-empty">
|
||||
<EuiFlexItem grow={1} className="controlsIllustration__container">
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ControlsIllustration />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{' '}
|
||||
<EuiText className="emptyStateText" size="s">
|
||||
<p>{ControlGroupStrings.emptyState.getCallToAction()}</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<div className="addControlButton">
|
||||
<CreateControlButton isIconButton={false} />
|
||||
</div>
|
||||
<EuiFlexGroup
|
||||
wrap={false}
|
||||
gutterSize="m"
|
||||
direction="row"
|
||||
responsive={false}
|
||||
alignItems="center"
|
||||
data-test-subj="controls-group"
|
||||
data-shared-items-count={idsInOrder.length}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<DndContext
|
||||
onDragStart={({ active }) => setDraggingId(active.id)}
|
||||
onDragEnd={onDragEnd}
|
||||
onDragCancel={() => setDraggingId(null)}
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
layoutMeasuring={{
|
||||
strategy: LayoutMeasuringStrategy.Always,
|
||||
}}
|
||||
>
|
||||
<SortableContext items={idsInOrder} strategy={rectSortingStrategy}>
|
||||
<EuiFlexGroup
|
||||
className={classNames('controlGroup', {
|
||||
'controlGroup-isDragging': draggingId,
|
||||
})}
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
wrap={true}
|
||||
>
|
||||
{idsInOrder.map(
|
||||
(controlId, index) =>
|
||||
panels[controlId] && (
|
||||
<SortableControl
|
||||
isEditable={isEditable}
|
||||
dragInfo={{ index, draggingIndex }}
|
||||
embeddableId={controlId}
|
||||
key={controlId}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</SortableContext>
|
||||
<DragOverlay>
|
||||
{draggingId ? <ControlClone draggingId={draggingId} /> : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
</EuiPanel>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -13,6 +13,10 @@ export const ControlGroupStrings = {
|
|||
i18n.translate('controls.controlGroup.title', {
|
||||
defaultMessage: 'Control group',
|
||||
}),
|
||||
getControlButtonTitle: () =>
|
||||
i18n.translate('controls.controlGroup.toolbarButtonTitle', {
|
||||
defaultMessage: 'Controls',
|
||||
}),
|
||||
emptyState: {
|
||||
getCallToAction: () =>
|
||||
i18n.translate('controls.controlGroup.emptyState.callToAction', {
|
||||
|
@ -26,6 +30,10 @@ export const ControlGroupStrings = {
|
|||
i18n.translate('controls.controlGroup.emptyState.twoLineLoadingTitle', {
|
||||
defaultMessage: '...',
|
||||
}),
|
||||
getDismissButton: () =>
|
||||
i18n.translate('controls.controlGroup.emptyState.dismissButton', {
|
||||
defaultMessage: 'Dismiss',
|
||||
}),
|
||||
},
|
||||
manageControl: {
|
||||
getFlyoutCreateTitle: () =>
|
||||
|
@ -60,11 +68,11 @@ export const ControlGroupStrings = {
|
|||
}),
|
||||
getManageButtonTitle: () =>
|
||||
i18n.translate('controls.controlGroup.management.buttonTitle', {
|
||||
defaultMessage: 'Configure controls',
|
||||
defaultMessage: 'Settings',
|
||||
}),
|
||||
getFlyoutTitle: () =>
|
||||
i18n.translate('controls.controlGroup.management.flyoutTitle', {
|
||||
defaultMessage: 'Configure controls',
|
||||
defaultMessage: 'Control settings',
|
||||
}),
|
||||
getDefaultWidthTitle: () =>
|
||||
i18n.translate('controls.controlGroup.management.defaultWidthTitle', {
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import {
|
||||
EuiFlyoutHeader,
|
||||
EuiButtonGroup,
|
||||
EuiFlyoutBody,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTitle,
|
||||
EuiFlyoutFooter,
|
||||
EuiButton,
|
||||
EuiFormRow,
|
||||
EuiButtonEmpty,
|
||||
EuiSpacer,
|
||||
EuiCheckbox,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { ControlStyle, ControlWidth } from '../../types';
|
||||
import { CONTROL_LAYOUT_OPTIONS, CONTROL_WIDTH_OPTIONS } from './editor_constants';
|
||||
|
||||
interface EditControlGroupProps {
|
||||
width: ControlWidth;
|
||||
controlStyle: ControlStyle;
|
||||
setAllWidths: boolean;
|
||||
updateControlStyle: (controlStyle: ControlStyle) => void;
|
||||
updateWidth: (newWidth: ControlWidth) => void;
|
||||
updateAllControlWidths: (newWidth: ControlWidth) => void;
|
||||
onCancel: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const ControlGroupEditor = ({
|
||||
width,
|
||||
controlStyle,
|
||||
setAllWidths,
|
||||
updateControlStyle,
|
||||
updateWidth,
|
||||
updateAllControlWidths,
|
||||
onCancel,
|
||||
onClose,
|
||||
}: EditControlGroupProps) => {
|
||||
const [currentControlStyle, setCurrentControlStyle] = useState(controlStyle);
|
||||
const [currentWidth, setCurrentWidth] = useState(width);
|
||||
const [applyToAll, setApplyToAll] = useState(setAllWidths);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>{ControlGroupStrings.management.getFlyoutTitle()}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiFormRow label={ControlGroupStrings.management.getLayoutTitle()}>
|
||||
<EuiButtonGroup
|
||||
color="primary"
|
||||
idSelected={currentControlStyle}
|
||||
legend={ControlGroupStrings.management.controlStyle.getDesignSwitchLegend()}
|
||||
options={CONTROL_LAYOUT_OPTIONS}
|
||||
onChange={(newControlStyle: string) => {
|
||||
setCurrentControlStyle(newControlStyle as ControlStyle);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow label={ControlGroupStrings.management.getDefaultWidthTitle()}>
|
||||
<EuiButtonGroup
|
||||
color="primary"
|
||||
idSelected={currentWidth}
|
||||
legend={ControlGroupStrings.management.controlWidth.getWidthSwitchLegend()}
|
||||
options={CONTROL_WIDTH_OPTIONS}
|
||||
onChange={(newWidth: string) => {
|
||||
setCurrentWidth(newWidth as ControlWidth);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiCheckbox
|
||||
id="editControls_setAllSizesCheckbox"
|
||||
label={ControlGroupStrings.management.getSetAllWidthsToDefaultTitle()}
|
||||
checked={applyToAll}
|
||||
onChange={(e) => {
|
||||
setApplyToAll(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiButtonEmpty
|
||||
onClick={onCancel}
|
||||
aria-label={'delete-all'}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
flush="left"
|
||||
size="s"
|
||||
>
|
||||
{ControlGroupStrings.management.getDeleteAllButtonTitle()}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup responsive={false} justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
aria-label={`cancel-editing-group`}
|
||||
iconType="cross"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{ControlGroupStrings.manageControl.getCancelTitle()}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
aria-label={`save-group`}
|
||||
iconType="check"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
if (currentControlStyle && currentControlStyle !== controlStyle) {
|
||||
updateControlStyle(currentControlStyle);
|
||||
}
|
||||
if (currentWidth && currentWidth !== width) {
|
||||
updateWidth(currentWidth);
|
||||
}
|
||||
if (applyToAll) {
|
||||
updateAllControlWidths(currentWidth);
|
||||
}
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{ControlGroupStrings.manageControl.getSaveChangesTitle()}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiButtonIconColor,
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
|
@ -16,42 +15,39 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import React, { useState, ReactElement } from 'react';
|
||||
|
||||
import { ControlGroupInput } from '../types';
|
||||
import { pluginServices } from '../../services';
|
||||
import { ControlEditor } from './control_editor';
|
||||
import { OverlayRef } from '../../../../../core/public';
|
||||
import { forwardAllContext } from './forward_all_context';
|
||||
import { DEFAULT_CONTROL_WIDTH } from './editor_constants';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { controlGroupReducers } from '../state/control_group_reducers';
|
||||
import { EmbeddableFactoryNotFoundError } from '../../../../embeddable/public';
|
||||
import { useReduxContainerContext } from '../../../../presentation_util/public';
|
||||
import { ControlWidth, IEditableControlFactory, ControlInput } from '../../types';
|
||||
import { toMountPoint } from '../../../../kibana_react/public';
|
||||
|
||||
export const CreateControlButton = ({ isIconButton }: { isIconButton: boolean }) => {
|
||||
export type CreateControlButtonTypes = 'toolbar' | 'callout';
|
||||
export interface CreateControlButtonProps {
|
||||
defaultControlWidth?: ControlWidth;
|
||||
updateDefaultWidth: (defaultControlWidth: ControlWidth) => void;
|
||||
addNewEmbeddable: (type: string, input: Omit<ControlInput, 'id'>) => void;
|
||||
buttonType: CreateControlButtonTypes;
|
||||
closePopover?: () => void;
|
||||
}
|
||||
|
||||
export const CreateControlButton = ({
|
||||
defaultControlWidth,
|
||||
updateDefaultWidth,
|
||||
addNewEmbeddable,
|
||||
buttonType,
|
||||
closePopover,
|
||||
}: CreateControlButtonProps) => {
|
||||
// Controls Services Context
|
||||
const { overlays, controls } = pluginServices.getHooks();
|
||||
const { getControlTypes, getControlFactory } = controls.useService();
|
||||
const { openFlyout, openConfirm } = overlays.useService();
|
||||
|
||||
// Redux embeddable container Context
|
||||
const reduxContainerContext = useReduxContainerContext<
|
||||
ControlGroupInput,
|
||||
typeof controlGroupReducers
|
||||
>();
|
||||
const {
|
||||
containerActions: { addNewEmbeddable },
|
||||
actions: { setDefaultControlWidth },
|
||||
useEmbeddableSelector,
|
||||
useEmbeddableDispatch,
|
||||
} = reduxContainerContext;
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
|
||||
// current state
|
||||
const { defaultControlWidth } = useEmbeddableSelector((state) => state);
|
||||
const { overlays, controls } = pluginServices.getServices();
|
||||
const { getControlTypes, getControlFactory } = controls;
|
||||
const { openFlyout, openConfirm } = overlays;
|
||||
const [isControlTypePopoverOpen, setIsControlTypePopoverOpen] = useState(false);
|
||||
|
||||
const createNewControl = async (type: string) => {
|
||||
const PresentationUtilProvider = pluginServices.getContextProvider();
|
||||
const factory = getControlFactory(type);
|
||||
if (!factory) throw new EmbeddableFactoryNotFoundError(type);
|
||||
|
||||
|
@ -80,26 +76,27 @@ export const CreateControlButton = ({ isIconButton }: { isIconButton: boolean })
|
|||
const editableFactory = factory as IEditableControlFactory;
|
||||
|
||||
const flyoutInstance = openFlyout(
|
||||
forwardAllContext(
|
||||
<ControlEditor
|
||||
isCreate={true}
|
||||
factory={editableFactory}
|
||||
width={defaultControlWidth ?? DEFAULT_CONTROL_WIDTH}
|
||||
updateTitle={(newTitle) => (inputToReturn.title = newTitle)}
|
||||
updateWidth={(newWidth) => dispatch(setDefaultControlWidth(newWidth as ControlWidth))}
|
||||
onTypeEditorChange={(partialInput) =>
|
||||
(inputToReturn = { ...inputToReturn, ...partialInput })
|
||||
}
|
||||
onSave={() => {
|
||||
if (editableFactory.presaveTransformFunction) {
|
||||
inputToReturn = editableFactory.presaveTransformFunction(inputToReturn);
|
||||
toMountPoint(
|
||||
<PresentationUtilProvider>
|
||||
<ControlEditor
|
||||
isCreate={true}
|
||||
factory={editableFactory}
|
||||
width={defaultControlWidth ?? DEFAULT_CONTROL_WIDTH}
|
||||
updateTitle={(newTitle) => (inputToReturn.title = newTitle)}
|
||||
updateWidth={updateDefaultWidth}
|
||||
onTypeEditorChange={(partialInput) =>
|
||||
(inputToReturn = { ...inputToReturn, ...partialInput })
|
||||
}
|
||||
resolve(inputToReturn);
|
||||
flyoutInstance.close();
|
||||
}}
|
||||
onCancel={() => onCancel(flyoutInstance)}
|
||||
/>,
|
||||
reduxContainerContext
|
||||
onSave={() => {
|
||||
if (editableFactory.presaveTransformFunction) {
|
||||
inputToReturn = editableFactory.presaveTransformFunction(inputToReturn);
|
||||
}
|
||||
resolve(inputToReturn);
|
||||
flyoutInstance.close();
|
||||
}}
|
||||
onCancel={() => onCancel(flyoutInstance)}
|
||||
/>
|
||||
</PresentationUtilProvider>
|
||||
),
|
||||
{
|
||||
onClose: (flyout) => onCancel(flyout),
|
||||
|
@ -117,48 +114,49 @@ export const CreateControlButton = ({ isIconButton }: { isIconButton: boolean })
|
|||
|
||||
if (getControlTypes().length === 0) return null;
|
||||
|
||||
const onCreateButtonClick = () => {
|
||||
if (getControlTypes().length > 1) {
|
||||
setIsControlTypePopoverOpen(!isControlTypePopoverOpen);
|
||||
return;
|
||||
}
|
||||
createNewControl(getControlTypes()[0]);
|
||||
};
|
||||
|
||||
const commonButtonProps = {
|
||||
onClick: onCreateButtonClick,
|
||||
color: 'primary' as EuiButtonIconColor,
|
||||
'data-test-subj': 'controls-create-button',
|
||||
'aria-label': ControlGroupStrings.management.getManageButtonTitle(),
|
||||
};
|
||||
|
||||
const createControlButton = isIconButton ? (
|
||||
<EuiButtonIcon {...commonButtonProps} iconType={'plusInCircle'} />
|
||||
) : (
|
||||
<EuiButton {...commonButtonProps} size="s">
|
||||
{ControlGroupStrings.emptyState.getAddControlButtonTitle()}
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
if (getControlTypes().length > 1) {
|
||||
const items: ReactElement[] = [];
|
||||
getControlTypes().forEach((type) => {
|
||||
const factory = getControlFactory(type);
|
||||
items.push(
|
||||
<EuiContextMenuItem
|
||||
key={type}
|
||||
icon={factory.getIconType?.()}
|
||||
data-test-subj={`create-${type}-control`}
|
||||
onClick={() => {
|
||||
const items: ReactElement[] = [];
|
||||
getControlTypes().forEach((type) => {
|
||||
const factory = getControlFactory(type);
|
||||
items.push(
|
||||
<EuiContextMenuItem
|
||||
key={type}
|
||||
icon={factory.getIconType?.()}
|
||||
data-test-subj={`create-${type}-control`}
|
||||
onClick={() => {
|
||||
if (buttonType === 'callout' && isControlTypePopoverOpen) {
|
||||
setIsControlTypePopoverOpen(false);
|
||||
createNewControl(type);
|
||||
}}
|
||||
>
|
||||
{factory.getDisplayName()}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
});
|
||||
} else if (closePopover) {
|
||||
closePopover();
|
||||
}
|
||||
createNewControl(type);
|
||||
}}
|
||||
toolTipContent={factory.getDescription()}
|
||||
>
|
||||
{factory.getDisplayName()}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
if (buttonType === 'callout') {
|
||||
const onCreateButtonClick = () => {
|
||||
if (getControlTypes().length > 1) {
|
||||
setIsControlTypePopoverOpen(!isControlTypePopoverOpen);
|
||||
return;
|
||||
}
|
||||
createNewControl(getControlTypes()[0]);
|
||||
};
|
||||
|
||||
const createControlButton = (
|
||||
<EuiButton {...commonButtonProps} onClick={onCreateButtonClick} size="s">
|
||||
{ControlGroupStrings.emptyState.getAddControlButtonTitle()}
|
||||
</EuiButton>
|
||||
);
|
||||
return (
|
||||
<EuiPopover
|
||||
button={createControlButton}
|
||||
|
@ -168,9 +166,9 @@ export const CreateControlButton = ({ isIconButton }: { isIconButton: boolean })
|
|||
data-test-subj="control-type-picker"
|
||||
closePopover={() => setIsControlTypePopoverOpen(false)}
|
||||
>
|
||||
<EuiContextMenuPanel size="s" items={items} />
|
||||
<EuiContextMenuPanel items={items} />
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
return createControlButton;
|
||||
return <EuiContextMenuPanel items={items} />;
|
||||
};
|
||||
|
|
|
@ -6,165 +6,93 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
EuiFormRow,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiFlyoutBody,
|
||||
EuiButtonGroup,
|
||||
EuiButtonEmpty,
|
||||
EuiFlyoutHeader,
|
||||
EuiCheckbox,
|
||||
EuiFlyoutFooter,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { EuiContextMenuItem } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
CONTROL_LAYOUT_OPTIONS,
|
||||
CONTROL_WIDTH_OPTIONS,
|
||||
DEFAULT_CONTROL_WIDTH,
|
||||
} from './editor_constants';
|
||||
import { ControlGroupInput } from '../types';
|
||||
import { DEFAULT_CONTROL_WIDTH, DEFAULT_CONTROL_STYLE } from './editor_constants';
|
||||
import { ControlsPanels } from '../types';
|
||||
import { pluginServices } from '../../services';
|
||||
import { ControlStyle, ControlWidth } from '../../types';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { controlGroupReducers } from '../state/control_group_reducers';
|
||||
import { useReduxContainerContext } from '../../../../presentation_util/public';
|
||||
import { toMountPoint } from '../../../../kibana_react/public';
|
||||
import { OverlayRef } from '../../../../../core/public';
|
||||
import { ControlGroupEditor } from './control_group_editor';
|
||||
|
||||
interface EditControlGroupState {
|
||||
newControlStyle: ControlGroupInput['controlStyle'];
|
||||
newDefaultWidth: ControlGroupInput['defaultControlWidth'];
|
||||
setAllWidths: boolean;
|
||||
export interface EditControlGroupButtonProps {
|
||||
controlStyle: ControlStyle;
|
||||
panels?: ControlsPanels;
|
||||
defaultControlWidth?: ControlWidth;
|
||||
setControlStyle: (setControlStyle: ControlStyle) => void;
|
||||
setDefaultControlWidth: (defaultControlWidth: ControlWidth) => void;
|
||||
setAllControlWidths: (defaultControlWidth: ControlWidth) => void;
|
||||
removeEmbeddable?: (panelId: string) => void;
|
||||
closePopover: () => void;
|
||||
}
|
||||
|
||||
export const EditControlGroup = ({ closeFlyout }: { closeFlyout: () => void }) => {
|
||||
const { overlays } = pluginServices.getHooks();
|
||||
const { openConfirm } = overlays.useService();
|
||||
export const EditControlGroup = ({
|
||||
panels,
|
||||
defaultControlWidth,
|
||||
controlStyle,
|
||||
setControlStyle,
|
||||
setDefaultControlWidth,
|
||||
setAllControlWidths,
|
||||
removeEmbeddable,
|
||||
closePopover,
|
||||
}: EditControlGroupButtonProps) => {
|
||||
const { overlays } = pluginServices.getServices();
|
||||
const { openConfirm, openFlyout } = overlays;
|
||||
|
||||
const {
|
||||
containerActions,
|
||||
useEmbeddableSelector,
|
||||
useEmbeddableDispatch,
|
||||
actions: { setControlStyle, setAllControlWidths, setDefaultControlWidth },
|
||||
} = useReduxContainerContext<ControlGroupInput, typeof controlGroupReducers>();
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const { panels, controlStyle, defaultControlWidth } = useEmbeddableSelector((state) => state);
|
||||
const editControlGroup = () => {
|
||||
const PresentationUtilProvider = pluginServices.getContextProvider();
|
||||
|
||||
const [state, setState] = useState<EditControlGroupState>({
|
||||
newControlStyle: controlStyle,
|
||||
newDefaultWidth: defaultControlWidth,
|
||||
setAllWidths: false,
|
||||
});
|
||||
const onCancel = (ref: OverlayRef) => {
|
||||
if (!removeEmbeddable || !panels) return;
|
||||
openConfirm(ControlGroupStrings.management.deleteControls.getSubtitle(), {
|
||||
confirmButtonText: ControlGroupStrings.management.deleteControls.getConfirm(),
|
||||
cancelButtonText: ControlGroupStrings.management.deleteControls.getCancel(),
|
||||
title: ControlGroupStrings.management.deleteControls.getDeleteAllTitle(),
|
||||
buttonColor: 'danger',
|
||||
}).then((confirmed) => {
|
||||
if (confirmed) Object.keys(panels).forEach((panelId) => removeEmbeddable(panelId));
|
||||
ref.close();
|
||||
});
|
||||
};
|
||||
|
||||
const onSave = () => {
|
||||
const { newControlStyle, newDefaultWidth, setAllWidths } = state;
|
||||
if (newControlStyle && newControlStyle !== controlStyle) {
|
||||
dispatch(setControlStyle(newControlStyle));
|
||||
}
|
||||
if (newDefaultWidth && newDefaultWidth !== defaultControlWidth) {
|
||||
dispatch(setDefaultControlWidth(newDefaultWidth));
|
||||
}
|
||||
if (setAllWidths && newDefaultWidth) {
|
||||
dispatch(setAllControlWidths(newDefaultWidth));
|
||||
}
|
||||
closeFlyout();
|
||||
const flyoutInstance = openFlyout(
|
||||
toMountPoint(
|
||||
<PresentationUtilProvider>
|
||||
<ControlGroupEditor
|
||||
width={defaultControlWidth ?? DEFAULT_CONTROL_WIDTH}
|
||||
controlStyle={controlStyle ?? DEFAULT_CONTROL_STYLE}
|
||||
setAllWidths={false}
|
||||
updateControlStyle={setControlStyle}
|
||||
updateWidth={setDefaultControlWidth}
|
||||
updateAllControlWidths={setAllControlWidths}
|
||||
onCancel={() => onCancel(flyoutInstance)}
|
||||
onClose={() => flyoutInstance.close()}
|
||||
/>
|
||||
</PresentationUtilProvider>
|
||||
),
|
||||
{
|
||||
onClose: () => flyoutInstance.close(),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const commonButtonProps = {
|
||||
key: 'manageControls',
|
||||
onClick: () => {
|
||||
editControlGroup();
|
||||
closePopover();
|
||||
},
|
||||
icon: 'gear',
|
||||
'data-test-subj': 'controls-sorting-button',
|
||||
'aria-label': ControlGroupStrings.management.getManageButtonTitle(),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>{ControlGroupStrings.management.getFlyoutTitle()}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiFormRow label={ControlGroupStrings.management.getLayoutTitle()}>
|
||||
<EuiButtonGroup
|
||||
color="primary"
|
||||
legend={ControlGroupStrings.management.controlStyle.getDesignSwitchLegend()}
|
||||
options={CONTROL_LAYOUT_OPTIONS}
|
||||
idSelected={state.newControlStyle}
|
||||
onChange={(newControlStyle) =>
|
||||
setState((s) => ({ ...s, newControlStyle: newControlStyle as ControlStyle }))
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow label={ControlGroupStrings.management.getDefaultWidthTitle()}>
|
||||
<EuiButtonGroup
|
||||
color="primary"
|
||||
idSelected={state.newDefaultWidth ?? DEFAULT_CONTROL_WIDTH}
|
||||
legend={ControlGroupStrings.management.controlWidth.getWidthSwitchLegend()}
|
||||
options={CONTROL_WIDTH_OPTIONS}
|
||||
onChange={(newDefaultWidth: string) =>
|
||||
setState((s) => ({ ...s, newDefaultWidth: newDefaultWidth as ControlWidth }))
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiCheckbox
|
||||
id="editControls_setAllSizesCheckbox"
|
||||
label={ControlGroupStrings.management.getSetAllWidthsToDefaultTitle()}
|
||||
checked={state.setAllWidths}
|
||||
onChange={(e) => setState((s) => ({ ...s, setAllWidths: e.target.checked }))}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiButtonEmpty
|
||||
onClick={() => {
|
||||
if (!containerActions?.removeEmbeddable) return;
|
||||
closeFlyout();
|
||||
openConfirm(ControlGroupStrings.management.deleteControls.getSubtitle(), {
|
||||
confirmButtonText: ControlGroupStrings.management.deleteControls.getConfirm(),
|
||||
cancelButtonText: ControlGroupStrings.management.deleteControls.getCancel(),
|
||||
title: ControlGroupStrings.management.deleteControls.getDeleteAllTitle(),
|
||||
buttonColor: 'danger',
|
||||
}).then((confirmed) => {
|
||||
if (confirmed)
|
||||
Object.keys(panels).forEach((panelId) =>
|
||||
containerActions.removeEmbeddable(panelId)
|
||||
);
|
||||
});
|
||||
}}
|
||||
aria-label={'delete-all'}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
flush="left"
|
||||
size="s"
|
||||
>
|
||||
{ControlGroupStrings.management.getDeleteAllButtonTitle()}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup responsive={false} justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
aria-label={`cancel-editing-group`}
|
||||
iconType="cross"
|
||||
onClick={() => {
|
||||
closeFlyout();
|
||||
}}
|
||||
>
|
||||
{ControlGroupStrings.manageControl.getCancelTitle()}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
aria-label={`save-group`}
|
||||
iconType="check"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
onSave();
|
||||
}}
|
||||
>
|
||||
{ControlGroupStrings.manageControl.getSaveChangesTitle()}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</>
|
||||
<EuiContextMenuItem {...commonButtonProps}>
|
||||
{ControlGroupStrings.management.getManageButtonTitle()}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,10 +6,11 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ControlWidth } from '../../types';
|
||||
import { ControlStyle, ControlWidth } from '../../types';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
|
||||
export const DEFAULT_CONTROL_WIDTH: ControlWidth = 'auto';
|
||||
export const DEFAULT_CONTROL_STYLE: ControlStyle = 'oneLine';
|
||||
|
||||
export const CONTROL_WIDTH_OPTIONS = [
|
||||
{
|
||||
|
|
|
@ -13,6 +13,7 @@ import deepEqual from 'fast-deep-equal';
|
|||
import { Filter, uniqFilters } from '@kbn/es-query';
|
||||
import { EMPTY, merge, pipe, Subscription } from 'rxjs';
|
||||
import { distinctUntilChanged, debounceTime, catchError, switchMap, map } from 'rxjs/operators';
|
||||
import { EuiContextMenuPanel, EuiHorizontalRule } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
ControlGroupInput,
|
||||
|
@ -24,6 +25,7 @@ import {
|
|||
withSuspense,
|
||||
LazyReduxEmbeddableWrapper,
|
||||
ReduxEmbeddableWrapperPropsWithChildren,
|
||||
SolutionToolbarPopover,
|
||||
} from '../../../../presentation_util/public';
|
||||
import { pluginServices } from '../../services';
|
||||
import { DataView } from '../../../../data_views/public';
|
||||
|
@ -32,6 +34,9 @@ import { ControlGroup } from '../component/control_group_component';
|
|||
import { controlGroupReducers } from '../state/control_group_reducers';
|
||||
import { ControlEmbeddable, ControlInput, ControlOutput } from '../../types';
|
||||
import { Container, EmbeddableFactory } from '../../../../embeddable/public';
|
||||
import { CreateControlButton, CreateControlButtonTypes } from '../editor/create_control';
|
||||
import { EditControlGroup } from '../editor/edit_control_group';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
|
||||
const ControlGroupReduxWrapper = withSuspense<
|
||||
ReduxEmbeddableWrapperPropsWithChildren<ControlGroupInput>
|
||||
|
@ -63,6 +68,74 @@ export class ControlGroupContainer extends Container<
|
|||
return Promise.resolve();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a button that allows controls to be created externally using the embeddable
|
||||
* @param buttonType Controls the button styling
|
||||
* @param closePopover Closes the create control menu popover when flyout opens - only necessary if `buttonType === 'toolbar'`
|
||||
* @return If `buttonType == 'toolbar'`, returns `EuiContextMenuPanel` with input control types as items.
|
||||
* Otherwise, if `buttonType == 'callout'` returns `EuiButton` with popover containing input control types.
|
||||
*/
|
||||
public getCreateControlButton = (
|
||||
buttonType: CreateControlButtonTypes,
|
||||
closePopover?: () => void
|
||||
) => {
|
||||
return (
|
||||
<CreateControlButton
|
||||
buttonType={buttonType}
|
||||
defaultControlWidth={this.getInput().defaultControlWidth}
|
||||
updateDefaultWidth={(defaultControlWidth) => this.updateInput({ defaultControlWidth })}
|
||||
addNewEmbeddable={(type, input) => this.addNewEmbeddable(type, input)}
|
||||
closePopover={closePopover}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
private getEditControlGroupButton = (closePopover: () => void) => {
|
||||
return (
|
||||
<EditControlGroup
|
||||
controlStyle={this.getInput().controlStyle}
|
||||
panels={this.getInput().panels}
|
||||
defaultControlWidth={this.getInput().defaultControlWidth}
|
||||
setControlStyle={(controlStyle) => this.updateInput({ controlStyle })}
|
||||
setDefaultControlWidth={(defaultControlWidth) => this.updateInput({ defaultControlWidth })}
|
||||
setAllControlWidths={(defaultControlWidth) => {
|
||||
Object.keys(this.getInput().panels).forEach(
|
||||
(panelId) => (this.getInput().panels[panelId].width = defaultControlWidth)
|
||||
);
|
||||
}}
|
||||
removeEmbeddable={(id) => this.removeEmbeddable(id)}
|
||||
closePopover={closePopover}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the toolbar button that is used for creating controls and managing control settings
|
||||
* @return `SolutionToolbarPopover` button for input controls
|
||||
*/
|
||||
public getToolbarButtons = () => {
|
||||
return (
|
||||
<SolutionToolbarPopover
|
||||
ownFocus
|
||||
label={ControlGroupStrings.getControlButtonTitle()}
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
panelPaddingSize="none"
|
||||
data-test-subj="dashboardControlsMenuButton"
|
||||
>
|
||||
{({ closePopover }: { closePopover: () => void }) => (
|
||||
<EuiContextMenuPanel
|
||||
items={[
|
||||
this.getCreateControlButton('toolbar', closePopover),
|
||||
<EuiHorizontalRule margin="none" />,
|
||||
this.getEditControlGroupButton(closePopover),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</SolutionToolbarPopover>
|
||||
);
|
||||
};
|
||||
|
||||
constructor(initialInput: ControlGroupInput, parent?: Container) {
|
||||
super(
|
||||
initialInput,
|
||||
|
@ -100,6 +173,10 @@ export class ControlGroupContainer extends Container<
|
|||
});
|
||||
}
|
||||
|
||||
public getPanelCount = () => {
|
||||
return Object.keys(this.getInput().panels).length;
|
||||
};
|
||||
|
||||
private recalculateOutput = () => {
|
||||
const allFilters: Filter[] = [];
|
||||
const allDataViews: DataView[] = [];
|
||||
|
|
|
@ -25,12 +25,6 @@ export const controlGroupReducers = {
|
|||
) => {
|
||||
state.defaultControlWidth = action.payload;
|
||||
},
|
||||
setAllControlWidths: (
|
||||
state: WritableDraft<ControlGroupInput>,
|
||||
action: PayloadAction<ControlWidth>
|
||||
) => {
|
||||
Object.keys(state.panels).forEach((panelId) => (state.panels[panelId].width = action.payload));
|
||||
},
|
||||
setControlWidth: (
|
||||
state: WritableDraft<ControlGroupInput>,
|
||||
action: PayloadAction<{ width: ControlWidth; embeddableId: string }>
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
createOptionsListExtract,
|
||||
createOptionsListInject,
|
||||
} from '../../../common/control_types/options_list/options_list_persistable_state';
|
||||
import { OptionsListStrings } from './options_list_strings';
|
||||
|
||||
export class OptionsListEmbeddableFactory
|
||||
implements EmbeddableFactoryDefinition, IEditableControlFactory<OptionsListEmbeddableInput>
|
||||
|
@ -49,7 +50,9 @@ export class OptionsListEmbeddableFactory
|
|||
|
||||
public isEditable = () => Promise.resolve(false);
|
||||
|
||||
public getDisplayName = () => 'Options List Control';
|
||||
public getDisplayName = () => OptionsListStrings.getDisplayName();
|
||||
public getIconType = () => 'list';
|
||||
public getDescription = () => OptionsListStrings.getDescription();
|
||||
|
||||
public inject = createOptionsListInject();
|
||||
public extract = createOptionsListExtract();
|
||||
|
|
|
@ -9,6 +9,14 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const OptionsListStrings = {
|
||||
getDisplayName: () =>
|
||||
i18n.translate('controls.optionsList.displayName', {
|
||||
defaultMessage: 'Options list',
|
||||
}),
|
||||
getDescription: () =>
|
||||
i18n.translate('controls.optionsList.description', {
|
||||
defaultMessage: 'Add control that allows options to be selected from a dropdown.',
|
||||
}),
|
||||
summary: {
|
||||
getSeparator: () =>
|
||||
i18n.translate('controls.optionsList.summary.separator', {
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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, EuiFlexItem, EuiText, EuiButtonEmpty, EuiPanel } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { ControlGroupStrings } from '../control_group/control_group_strings';
|
||||
import { ControlsIllustration } from './controls_illustration';
|
||||
|
||||
const CONTROLS_CALLOUT_STATE_KEY = 'dashboard:controlsCalloutDismissed';
|
||||
|
||||
export interface CalloutProps {
|
||||
getCreateControlButton?: () => JSX.Element;
|
||||
}
|
||||
|
||||
export const ControlsCallout = ({ getCreateControlButton }: CalloutProps) => {
|
||||
const [controlsCalloutDismissed, setControlsCalloutDismissed] = useLocalStorage(
|
||||
CONTROLS_CALLOUT_STATE_KEY,
|
||||
false
|
||||
);
|
||||
const dismissControls = () => {
|
||||
setControlsCalloutDismissed(true);
|
||||
};
|
||||
|
||||
if (controlsCalloutDismissed) return null;
|
||||
|
||||
return (
|
||||
<EuiPanel
|
||||
borderRadius="m"
|
||||
color="plain"
|
||||
paddingSize={'s'}
|
||||
className={classNames('controlsWrapper--empty', 'dshDashboardViewport-controls')}
|
||||
>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs" data-test-subj="controls-empty">
|
||||
<EuiFlexItem grow={1} className="controlsIllustration__container">
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ControlsIllustration />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText className="emptyStateText" size="s">
|
||||
<p>{ControlGroupStrings.emptyState.getCallToAction()}</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{getCreateControlButton ? (
|
||||
<EuiFlexItem grow={false}>{getCreateControlButton()}</EuiFlexItem>
|
||||
) : null}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty size="s" onClick={dismissControls}>
|
||||
{ControlGroupStrings.emptyState.getDismissButton()}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
// required for dynamic import using React.lazy()
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ControlsCallout;
|
11
src/plugins/controls/public/controls_callout/index.ts
Normal file
11
src/plugins/controls/public/controls_callout/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
export const LazyControlsCallout = React.lazy(() => import('./controls_callout'));
|
||||
export type { CalloutProps } from './controls_callout';
|
|
@ -39,6 +39,8 @@ export {
|
|||
type OptionsListEmbeddableInput,
|
||||
} from './control_types';
|
||||
|
||||
export { LazyControlsCallout, type CalloutProps } from './controls_callout';
|
||||
|
||||
export function plugin() {
|
||||
return new ControlsPlugin();
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.dshDashboardViewport-controlGroup {
|
||||
margin: 0 $euiSizeS 0 $euiSizeS;
|
||||
.dshDashboardViewport-controls {
|
||||
margin: 0 $euiSizeS 0 $euiSizeS;
|
||||
padding-bottom: $euiSizeXS;
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,12 @@ import { DashboardContainer, DashboardReactContextValue } from '../dashboard_con
|
|||
import { DashboardGrid } from '../grid';
|
||||
import { context } from '../../../services/kibana_react';
|
||||
import { DashboardEmptyScreen } from '../empty_screen/dashboard_empty_screen';
|
||||
import { ControlGroupContainer } from '../../../../../controls/public';
|
||||
import {
|
||||
CalloutProps,
|
||||
ControlGroupContainer,
|
||||
LazyControlsCallout,
|
||||
} from '../../../../../controls/public';
|
||||
import { withSuspense } from '../../../services/presentation_util';
|
||||
|
||||
export interface DashboardViewportProps {
|
||||
container: DashboardContainer;
|
||||
|
@ -31,6 +36,8 @@ interface State {
|
|||
isEmbeddedExternally?: boolean;
|
||||
}
|
||||
|
||||
const ControlsCallout = withSuspense<CalloutProps>(LazyControlsCallout);
|
||||
|
||||
export class DashboardViewport extends React.Component<DashboardViewportProps, State> {
|
||||
static contextType = context;
|
||||
public declare readonly context: DashboardReactContextValue;
|
||||
|
@ -94,14 +101,24 @@ export class DashboardViewport extends React.Component<DashboardViewportProps, S
|
|||
};
|
||||
|
||||
public render() {
|
||||
const { container, controlsEnabled } = this.props;
|
||||
const { container, controlsEnabled, controlGroup } = this.props;
|
||||
const isEditMode = container.getInput().viewMode !== ViewMode.VIEW;
|
||||
const { isEmbeddedExternally, isFullScreenMode, panelCount, title, description, useMargins } =
|
||||
this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
{controlsEnabled ? (
|
||||
<div className="dshDashboardViewport-controlGroup" ref={this.controlsRoot} />
|
||||
<>
|
||||
{isEditMode && panelCount !== 0 && controlGroup?.getPanelCount() === 0 ? (
|
||||
<ControlsCallout
|
||||
getCreateControlButton={() => {
|
||||
return controlGroup?.getCreateControlButton('callout');
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div className="dshDashboardViewport-controls" ref={this.controlsRoot} />
|
||||
</>
|
||||
) : null}
|
||||
<div
|
||||
data-shared-items-count={panelCount}
|
||||
|
|
|
@ -598,17 +598,16 @@ export function DashboardTopNav({
|
|||
/>
|
||||
),
|
||||
quickButtonGroup: <QuickButtonGroup buttons={quickButtons} />,
|
||||
addFromLibraryButton: (
|
||||
<AddFromLibraryButton
|
||||
onClick={addFromLibrary}
|
||||
data-test-subj="dashboardAddPanelButton"
|
||||
/>
|
||||
),
|
||||
extraButtons: [
|
||||
<EditorMenu
|
||||
createNewVisType={createNewVisType}
|
||||
dashboardContainer={dashboardAppState.dashboardContainer}
|
||||
/>,
|
||||
<AddFromLibraryButton
|
||||
onClick={addFromLibrary}
|
||||
data-test-subj="dashboardAddPanelButton"
|
||||
/>,
|
||||
dashboardAppState.dashboardContainer.controlGroup?.getToolbarButtons(),
|
||||
],
|
||||
}}
|
||||
</SolutionToolbar>
|
||||
|
|
|
@ -10,7 +10,6 @@ import React, { ReactElement } from 'react';
|
|||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
AddFromLibraryButton,
|
||||
QuickButtonGroup,
|
||||
PrimaryActionButton,
|
||||
SolutionToolbarButton,
|
||||
|
@ -23,8 +22,9 @@ import './solution_toolbar.scss';
|
|||
interface NamedSlots {
|
||||
primaryActionButton: ReactElement<typeof PrimaryActionButton | typeof PrimaryActionPopover>;
|
||||
quickButtonGroup?: ReactElement<typeof QuickButtonGroup>;
|
||||
addFromLibraryButton?: ReactElement<typeof AddFromLibraryButton>;
|
||||
extraButtons?: Array<ReactElement<typeof SolutionToolbarButton | typeof SolutionToolbarPopover>>;
|
||||
extraButtons?: Array<
|
||||
ReactElement<typeof SolutionToolbarButton | typeof SolutionToolbarPopover> | undefined
|
||||
>;
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
|
@ -33,12 +33,7 @@ export interface Props {
|
|||
}
|
||||
|
||||
export const SolutionToolbar = ({ isDarkModeEnabled, children }: Props) => {
|
||||
const {
|
||||
primaryActionButton,
|
||||
quickButtonGroup,
|
||||
addFromLibraryButton,
|
||||
extraButtons = [],
|
||||
} = children;
|
||||
const { primaryActionButton, quickButtonGroup, extraButtons = [] } = children;
|
||||
|
||||
const extra = extraButtons.map((button, index) =>
|
||||
button ? (
|
||||
|
@ -61,9 +56,6 @@ export const SolutionToolbar = ({ isDarkModeEnabled, children }: Props) => {
|
|||
<EuiFlexGroup responsive={false} alignItems="center" gutterSize="xs">
|
||||
{quickButtonGroup ? <EuiFlexItem grow={false}>{quickButtonGroup}</EuiFlexItem> : null}
|
||||
{extra}
|
||||
{addFromLibraryButton ? (
|
||||
<EuiFlexItem grow={false}>{addFromLibraryButton}</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -51,12 +51,43 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
it('shows the empty control callout on a new dashboard', async () => {
|
||||
await testSubjects.existOrFail('controls-empty');
|
||||
describe('Controls callout visibility', async () => {
|
||||
describe('does not show the empty control callout on an empty dashboard', async () => {
|
||||
it('in view mode', async () => {
|
||||
await dashboard.saveDashboard('Test Controls Callout');
|
||||
await dashboard.clickCancelOutOfEditMode();
|
||||
await testSubjects.missingOrFail('controls-empty');
|
||||
});
|
||||
|
||||
it('in edit mode', async () => {
|
||||
await dashboard.switchToEditMode();
|
||||
await testSubjects.missingOrFail('controls-empty');
|
||||
});
|
||||
});
|
||||
|
||||
it('show the empty control callout on a dashboard with panels', async () => {
|
||||
await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie');
|
||||
await testSubjects.existOrFail('controls-empty');
|
||||
});
|
||||
|
||||
it('adding control hides the empty control callout', async () => {
|
||||
await dashboardControls.createOptionsListControl({
|
||||
dataViewTitle: 'animals-*',
|
||||
fieldName: 'sound.keyword',
|
||||
});
|
||||
await testSubjects.missingOrFail('controls-empty');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await dashboard.clickCancelOutOfEditMode();
|
||||
await dashboard.gotoDashboardLandingPage();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Options List Control creation and editing experience', async () => {
|
||||
it('can add a new options list control from a blank state', async () => {
|
||||
await dashboard.clickNewDashboard();
|
||||
await timePicker.setDefaultDataRange();
|
||||
await dashboardControls.createOptionsListControl({ fieldName: 'machine.os.raw' });
|
||||
expect(await dashboardControls.getControlsCount()).to.be(1);
|
||||
});
|
||||
|
@ -115,7 +146,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
let controlId: string;
|
||||
before(async () => {
|
||||
await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie');
|
||||
|
||||
await dashboardControls.createOptionsListControl({
|
||||
dataViewTitle: 'animals-*',
|
||||
fieldName: 'sound.keyword',
|
||||
|
|
|
@ -64,10 +64,8 @@ export class DashboardPageControls extends FtrService {
|
|||
|
||||
public async openCreateControlFlyout(type: string) {
|
||||
this.log.debug(`Opening flyout for ${type} control`);
|
||||
await this.testSubjects.click('controls-create-button');
|
||||
if (await this.testSubjects.exists('control-type-picker')) {
|
||||
await this.testSubjects.click(`create-${type}-control`);
|
||||
}
|
||||
await this.testSubjects.click('dashboardControlsMenuButton');
|
||||
await this.testSubjects.click(`create-${type}-control`);
|
||||
await this.retry.try(async () => {
|
||||
await this.testSubjects.existOrFail('control-editor-flyout');
|
||||
});
|
||||
|
|
|
@ -169,13 +169,13 @@ export const WorkpadHeader: FC<Props> = ({
|
|||
{{
|
||||
primaryActionButton: <ElementMenu addElement={addElement} elements={elements} />,
|
||||
quickButtonGroup: <QuickButtonGroup buttons={quickButtons} />,
|
||||
addFromLibraryButton: (
|
||||
extraButtons: [
|
||||
<AddFromLibraryButton
|
||||
onClick={showEmbedPanel}
|
||||
data-test-subj="canvas-add-from-library-button"
|
||||
/>
|
||||
),
|
||||
extraButtons: [<EditorMenu addElement={addElement} />],
|
||||
/>,
|
||||
<EditorMenu addElement={addElement} />,
|
||||
],
|
||||
}}
|
||||
</SolutionToolbar>
|
||||
</EuiFlexItem>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue