mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Controls] Control Group Embeddable and Management Experience (#111065)
* built control group embeddable featuring inline control creation and editing, and DndKit based drag and drop. Co-authored-by: andreadelrio <delrio.andre@gmail.com>
This commit is contained in:
parent
b26968dbe8
commit
ac39f94b75
46 changed files with 2137 additions and 227 deletions
|
@ -11,6 +11,12 @@ const aliases = require('../../src/dev/storybook/aliases.ts').storybookAliases;
|
|||
|
||||
config.refs = {};
|
||||
|
||||
// Required due to https://github.com/storybookjs/storybook/issues/13834
|
||||
config.babel = async (options) => ({
|
||||
...options,
|
||||
plugins: ['@babel/plugin-transform-typescript', ...options.plugins],
|
||||
});
|
||||
|
||||
for (const alias of Object.keys(aliases).filter((a) => a !== 'ci_composite')) {
|
||||
// snake_case -> Title Case
|
||||
const title = alias
|
||||
|
|
|
@ -92,6 +92,9 @@
|
|||
"yarn": "^1.21.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^3.1.1",
|
||||
"@dnd-kit/sortable": "^4.0.0",
|
||||
"@dnd-kit/utilities": "^2.0.0",
|
||||
"@babel/runtime": "^7.15.4",
|
||||
"@elastic/apm-rum": "^5.9.1",
|
||||
"@elastic/apm-rum-react": "^1.3.1",
|
||||
|
|
|
@ -46,6 +46,7 @@ export abstract class Container<
|
|||
parent?: Container
|
||||
) {
|
||||
super(input, output, parent);
|
||||
this.getFactory = getFactory; // Currently required for using in storybook due to https://github.com/storybookjs/storybook/issues/13834
|
||||
this.subscription = this.getInput$()
|
||||
// At each update event, get both the previous and current state
|
||||
.pipe(startWith(input), pairwise())
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { InputControlFactory } from '../types';
|
||||
import { ControlsService } from '../controls_service';
|
||||
import { flightFields, getEuiSelectableOptions } from './flights';
|
||||
import { OptionsListEmbeddableFactory } from '../control_types/options_list';
|
||||
|
||||
export const getControlsServiceStub = () => {
|
||||
const controlsServiceStub = new ControlsService();
|
||||
|
||||
const optionsListFactoryStub = new OptionsListEmbeddableFactory(
|
||||
({ field, search }) =>
|
||||
new Promise((r) => setTimeout(() => r(getEuiSelectableOptions(field, search)), 500)),
|
||||
() => Promise.resolve(['demo data flights']),
|
||||
() => Promise.resolve(flightFields)
|
||||
);
|
||||
|
||||
// cast to unknown because the stub cannot use the embeddable start contract to transform the EmbeddableFactoryDefinition into an EmbeddableFactory
|
||||
const optionsListControlFactory = optionsListFactoryStub as unknown as InputControlFactory;
|
||||
optionsListControlFactory.getDefaultInput = () => ({});
|
||||
controlsServiceStub.registerInputControlType(optionsListControlFactory);
|
||||
return controlsServiceStub;
|
||||
};
|
|
@ -23,7 +23,7 @@ const panelStyle = {
|
|||
|
||||
const kqlBarStyle = { background: bar, padding: 16, minHeight, fontStyle: 'italic' };
|
||||
|
||||
const inputBarStyle = { background: '#fff', padding: 4, minHeight };
|
||||
const inputBarStyle = { background: '#fff', padding: 4 };
|
||||
|
||||
const layout = (OptionStory: Story) => (
|
||||
<EuiFlexGroup style={{ background }} direction="column">
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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, { useEffect, useMemo } from 'react';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { decorators } from './decorators';
|
||||
import { providers } from '../../../services/storybook';
|
||||
import { getControlsServiceStub } from './controls_service_stub';
|
||||
import { ControlGroupContainerFactory } from '../control_group/control_group_container_factory';
|
||||
|
||||
export default {
|
||||
title: 'Controls',
|
||||
description: '',
|
||||
decorators,
|
||||
};
|
||||
|
||||
const ControlGroupStoryComponent = () => {
|
||||
const embeddableRoot: React.RefObject<HTMLDivElement> = useMemo(() => React.createRef(), []);
|
||||
|
||||
providers.overlays.start({});
|
||||
const overlays = providers.overlays.getService();
|
||||
|
||||
const controlsServiceStub = getControlsServiceStub();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const factory = new ControlGroupContainerFactory(controlsServiceStub, overlays);
|
||||
const controlGroupContainerEmbeddable = await factory.create({
|
||||
inheritParentState: {
|
||||
useQuery: false,
|
||||
useFilters: false,
|
||||
useTimerange: false,
|
||||
},
|
||||
controlStyle: 'oneLine',
|
||||
id: uuid.v4(),
|
||||
panels: {},
|
||||
});
|
||||
if (controlGroupContainerEmbeddable && embeddableRoot.current) {
|
||||
controlGroupContainerEmbeddable.render(embeddableRoot.current);
|
||||
}
|
||||
})();
|
||||
}, [embeddableRoot, controlsServiceStub, overlays]);
|
||||
|
||||
return <div ref={embeddableRoot} />;
|
||||
};
|
||||
|
||||
export const ControlGroupStory = () => <ControlGroupStoryComponent />;
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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, { useEffect, useMemo, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiFormControlLayout,
|
||||
EuiFormLabel,
|
||||
EuiFormRow,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { ControlGroupContainer } from '../control_group/control_group_container';
|
||||
import { useChildEmbeddable } from '../hooks/use_child_embeddable';
|
||||
import { ControlStyle } from '../types';
|
||||
import { ControlFrameStrings } from './control_frame_strings';
|
||||
|
||||
export interface ControlFrameProps {
|
||||
container: ControlGroupContainer;
|
||||
customPrepend?: JSX.Element;
|
||||
controlStyle: ControlStyle;
|
||||
enableActions?: boolean;
|
||||
onRemove?: () => void;
|
||||
embeddableId: string;
|
||||
onEdit?: () => void;
|
||||
}
|
||||
|
||||
export const ControlFrame = ({
|
||||
customPrepend,
|
||||
enableActions,
|
||||
embeddableId,
|
||||
controlStyle,
|
||||
container,
|
||||
onRemove,
|
||||
onEdit,
|
||||
}: ControlFrameProps) => {
|
||||
const embeddableRoot: React.RefObject<HTMLDivElement> = useMemo(() => React.createRef(), []);
|
||||
const embeddable = useChildEmbeddable({ container, embeddableId });
|
||||
|
||||
const [title, setTitle] = useState<string>();
|
||||
|
||||
const usingTwoLineLayout = controlStyle === 'twoLine';
|
||||
|
||||
useEffect(() => {
|
||||
if (embeddableRoot.current && embeddable) {
|
||||
embeddable.render(embeddableRoot.current);
|
||||
}
|
||||
const subscription = embeddable?.getInput$().subscribe((newInput) => setTitle(newInput.title));
|
||||
return () => subscription?.unsubscribe();
|
||||
}, [embeddable, embeddableRoot]);
|
||||
|
||||
const floatingActions = (
|
||||
<div
|
||||
className={classNames('controlFrame--floatingActions', {
|
||||
'controlFrame--floatingActions-twoLine': usingTwoLineLayout,
|
||||
'controlFrame--floatingActions-oneLine': !usingTwoLineLayout,
|
||||
})}
|
||||
>
|
||||
<EuiToolTip content={ControlFrameStrings.floatingActions.getEditButtonTitle()}>
|
||||
<EuiButtonIcon
|
||||
aria-label={ControlFrameStrings.floatingActions.getEditButtonTitle()}
|
||||
iconType="pencil"
|
||||
onClick={onEdit}
|
||||
color="text"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
<EuiToolTip content={ControlFrameStrings.floatingActions.getRemoveButtonTitle()}>
|
||||
<EuiButtonIcon
|
||||
aria-label={ControlFrameStrings.floatingActions.getRemoveButtonTitle()}
|
||||
onClick={onRemove}
|
||||
iconType="cross"
|
||||
color="danger"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</div>
|
||||
);
|
||||
|
||||
const form = (
|
||||
<EuiFormControlLayout
|
||||
className={'controlFrame--formControlLayout'}
|
||||
fullWidth
|
||||
prepend={
|
||||
<>
|
||||
{customPrepend ?? null}
|
||||
{usingTwoLineLayout ? undefined : (
|
||||
<EuiFormLabel className="controlFrame--formControlLayout__label" htmlFor={embeddableId}>
|
||||
{title}
|
||||
</EuiFormLabel>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={classNames('controlFrame--control', {
|
||||
'controlFrame--twoLine': controlStyle === 'twoLine',
|
||||
'controlFrame--oneLine': controlStyle === 'oneLine',
|
||||
})}
|
||||
id={`controlFrame--${embeddableId}`}
|
||||
ref={embeddableRoot}
|
||||
/>
|
||||
</EuiFormControlLayout>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{enableActions && floatingActions}
|
||||
<EuiFormRow fullWidth label={usingTwoLineLayout ? title : undefined}>
|
||||
{form}
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ControlFrameStrings = {
|
||||
floatingActions: {
|
||||
getEditButtonTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.floatingActions.editTitle', {
|
||||
defaultMessage: 'Manage control',
|
||||
}),
|
||||
getRemoveButtonTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.floatingActions.removeTitle', {
|
||||
defaultMessage: 'Remove control',
|
||||
}),
|
||||
},
|
||||
};
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* 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 '../control_group.scss';
|
||||
|
||||
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
rectSortingStrategy,
|
||||
sortableKeyboardCoordinates,
|
||||
} from '@dnd-kit/sortable';
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragOverlay,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
LayoutMeasuringStrategy,
|
||||
} from '@dnd-kit/core';
|
||||
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { ControlGroupContainer } from '../control_group_container';
|
||||
import { ControlClone, SortableControl } from './control_group_sortable_item';
|
||||
import { OPTIONS_LIST_CONTROL } from '../../control_types/options_list/options_list_embeddable';
|
||||
|
||||
interface ControlGroupProps {
|
||||
controlGroupContainer: ControlGroupContainer;
|
||||
}
|
||||
|
||||
export const ControlGroup = ({ controlGroupContainer }: ControlGroupProps) => {
|
||||
const [controlIds, setControlIds] = useState<string[]>([]);
|
||||
|
||||
// sync controlIds every time input panels change
|
||||
useEffect(() => {
|
||||
const subscription = controlGroupContainer.getInput$().subscribe(() => {
|
||||
setControlIds((currentIds) => {
|
||||
// sync control Ids with panels from container input.
|
||||
const { panels } = controlGroupContainer.getInput();
|
||||
const newIds: string[] = [];
|
||||
const allIds = [...currentIds, ...Object.keys(panels)];
|
||||
allIds.forEach((id) => {
|
||||
const currentIndex = currentIds.indexOf(id);
|
||||
if (!panels[id] && currentIndex !== -1) {
|
||||
currentIds.splice(currentIndex, 1);
|
||||
}
|
||||
if (currentIndex === -1 && Boolean(panels[id])) {
|
||||
newIds.push(id);
|
||||
}
|
||||
});
|
||||
return [...currentIds, ...newIds];
|
||||
});
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [controlGroupContainer]);
|
||||
|
||||
const [draggingId, setDraggingId] = useState<string | null>(null);
|
||||
|
||||
const draggingIndex = useMemo(
|
||||
() => (draggingId ? controlIds.indexOf(draggingId) : -1),
|
||||
[controlIds, draggingId]
|
||||
);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
|
||||
);
|
||||
|
||||
const onDragEnd = ({ over }: DragEndEvent) => {
|
||||
if (over) {
|
||||
const overIndex = controlIds.indexOf(over.id);
|
||||
if (draggingIndex !== overIndex) {
|
||||
const newIndex = overIndex;
|
||||
setControlIds((currentControlIds) => arrayMove(currentControlIds, draggingIndex, newIndex));
|
||||
}
|
||||
}
|
||||
setDraggingId(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup wrap={false} direction="row" alignItems="center" className="superWrapper">
|
||||
<EuiFlexItem>
|
||||
<DndContext
|
||||
onDragStart={({ active }) => setDraggingId(active.id)}
|
||||
onDragEnd={onDragEnd}
|
||||
onDragCancel={() => setDraggingId(null)}
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
layoutMeasuring={{
|
||||
strategy: LayoutMeasuringStrategy.Always,
|
||||
}}
|
||||
>
|
||||
<SortableContext items={controlIds} strategy={rectSortingStrategy}>
|
||||
<EuiFlexGroup
|
||||
className={classNames('controlGroup', { 'controlGroup-isDragging': draggingId })}
|
||||
alignItems="center"
|
||||
gutterSize={'m'}
|
||||
wrap={true}
|
||||
>
|
||||
{controlIds.map((controlId, index) => (
|
||||
<SortableControl
|
||||
onEdit={() => controlGroupContainer.editControl(controlId)}
|
||||
onRemove={() => controlGroupContainer.removeEmbeddable(controlId)}
|
||||
dragInfo={{ index, draggingIndex }}
|
||||
container={controlGroupContainer}
|
||||
controlStyle={controlGroupContainer.getInput().controlStyle}
|
||||
embeddableId={controlId}
|
||||
width={controlGroupContainer.getInput().panels[controlId].width}
|
||||
key={controlId}
|
||||
/>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</SortableContext>
|
||||
<DragOverlay>
|
||||
{draggingId ? (
|
||||
<ControlClone
|
||||
width={controlGroupContainer.getInput().panels[draggingId].width}
|
||||
embeddableId={draggingId}
|
||||
container={controlGroupContainer}
|
||||
/>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center" direction="row" gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
<EuiToolTip content={ControlGroupStrings.management.getManageButtonTitle()}>
|
||||
<EuiButtonIcon
|
||||
aria-label={ControlGroupStrings.management.getManageButtonTitle()}
|
||||
iconType="gear"
|
||||
color="text"
|
||||
data-test-subj="inputControlsSortingButton"
|
||||
onClick={controlGroupContainer.editControlGroup}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiToolTip content={ControlGroupStrings.management.getAddControlTitle()}>
|
||||
<EuiButtonIcon
|
||||
aria-label={ControlGroupStrings.management.getManageButtonTitle()}
|
||||
iconType="plus"
|
||||
color="text"
|
||||
data-test-subj="inputControlsSortingButton"
|
||||
onClick={() => controlGroupContainer.createNewControl(OPTIONS_LIST_CONTROL)} // use popover when there are multiple types of control
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* 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 { EuiFlexItem, EuiFormLabel, EuiIcon, EuiFlexGroup } from '@elastic/eui';
|
||||
import React, { forwardRef, HTMLAttributes } from 'react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { ControlWidth } from '../../types';
|
||||
import { ControlGroupContainer } from '../control_group_container';
|
||||
import { useChildEmbeddable } from '../../hooks/use_child_embeddable';
|
||||
import { ControlFrame, ControlFrameProps } from '../../control_frame/control_frame_component';
|
||||
|
||||
interface DragInfo {
|
||||
isOver?: boolean;
|
||||
isDragging?: boolean;
|
||||
draggingIndex?: number;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
export type SortableControlProps = ControlFrameProps & {
|
||||
dragInfo: DragInfo;
|
||||
width: ControlWidth;
|
||||
};
|
||||
|
||||
/**
|
||||
* A sortable wrapper around the generic control frame.
|
||||
*/
|
||||
export const SortableControl = (frameProps: SortableControlProps) => {
|
||||
const { embeddableId } = frameProps;
|
||||
const { over, listeners, isSorting, transform, transition, attributes, isDragging, setNodeRef } =
|
||||
useSortable({
|
||||
id: embeddableId,
|
||||
animateLayoutChanges: () => true,
|
||||
});
|
||||
|
||||
frameProps.dragInfo = { ...frameProps.dragInfo, isOver: over?.id === embeddableId, isDragging };
|
||||
|
||||
return (
|
||||
<SortableControlInner
|
||||
key={embeddableId}
|
||||
ref={setNodeRef}
|
||||
{...frameProps}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
style={{
|
||||
transition: transition ?? undefined,
|
||||
transform: isSorting ? undefined : CSS.Translate.toString(transform),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const SortableControlInner = forwardRef<
|
||||
HTMLButtonElement,
|
||||
SortableControlProps & { style: HTMLAttributes<HTMLButtonElement>['style'] }
|
||||
>(
|
||||
(
|
||||
{
|
||||
embeddableId,
|
||||
controlStyle,
|
||||
container,
|
||||
dragInfo,
|
||||
onRemove,
|
||||
onEdit,
|
||||
style,
|
||||
width,
|
||||
...dragHandleProps
|
||||
},
|
||||
dragHandleRef
|
||||
) => {
|
||||
const { isOver, isDragging, draggingIndex, index } = dragInfo;
|
||||
|
||||
const dragHandle = (
|
||||
<button ref={dragHandleRef} {...dragHandleProps} className="controlFrame--dragHandle">
|
||||
<EuiIcon type="grabHorizontal" />
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexItem
|
||||
grow={width === 'auto'}
|
||||
className={classNames('controlFrame--wrapper', {
|
||||
'controlFrame--wrapper-isDragging': isDragging,
|
||||
'controlFrame--wrapper-small': width === 'small',
|
||||
'controlFrame--wrapper-medium': width === 'medium',
|
||||
'controlFrame--wrapper-large': width === 'large',
|
||||
'controlFrame--wrapper-insertBefore': isOver && (index ?? -1) < (draggingIndex ?? -1),
|
||||
'controlFrame--wrapper-insertAfter': isOver && (index ?? -1) > (draggingIndex ?? -1),
|
||||
})}
|
||||
style={style}
|
||||
>
|
||||
<ControlFrame
|
||||
enableActions={draggingIndex === -1}
|
||||
controlStyle={controlStyle}
|
||||
embeddableId={embeddableId}
|
||||
customPrepend={dragHandle}
|
||||
container={container}
|
||||
onRemove={onRemove}
|
||||
onEdit={onEdit}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* A simplified clone version of the control which is dragged. This version only shows
|
||||
* the title, because individual controls can be any size, and dragging a wide item
|
||||
* can be quite cumbersome.
|
||||
*/
|
||||
export const ControlClone = ({
|
||||
embeddableId,
|
||||
container,
|
||||
width,
|
||||
}: {
|
||||
embeddableId: string;
|
||||
container: ControlGroupContainer;
|
||||
width: ControlWidth;
|
||||
}) => {
|
||||
const embeddable = useChildEmbeddable({ embeddableId, container });
|
||||
const layout = container.getInput().controlStyle;
|
||||
return (
|
||||
<EuiFlexItem
|
||||
className={classNames('controlFrame--cloneWrapper', {
|
||||
'controlFrame--cloneWrapper-small': width === 'small',
|
||||
'controlFrame--cloneWrapper-medium': width === 'medium',
|
||||
'controlFrame--cloneWrapper-large': width === 'large',
|
||||
'controlFrame--cloneWrapper-twoLine': layout === 'twoLine',
|
||||
})}
|
||||
>
|
||||
{layout === 'twoLine' ? (
|
||||
<EuiFormLabel>{embeddable?.getInput().title}</EuiFormLabel>
|
||||
) : undefined}
|
||||
<EuiFlexGroup gutterSize="none" className={'controlFrame--draggable'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="grabHorizontal" className="controlFrame--dragHandle" />
|
||||
</EuiFlexItem>
|
||||
{container.getInput().controlStyle === 'oneLine' ? (
|
||||
<EuiFlexItem>{embeddable?.getInput().title}</EuiFlexItem>
|
||||
) : undefined}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,184 @@
|
|||
$smallControl: $euiSize * 14;
|
||||
$mediumControl: $euiSize * 25;
|
||||
$largeControl: $euiSize * 50;
|
||||
$controlMinWidth: $euiSize * 14;
|
||||
|
||||
.controlGroup {
|
||||
margin-left: $euiSizeXS;
|
||||
overflow-x: clip; // sometimes when using auto width, removing a control can cause a horizontal scrollbar to appear.
|
||||
min-height: $euiSize * 4;
|
||||
padding: $euiSize 0;
|
||||
}
|
||||
|
||||
.controlFrame--cloneWrapper {
|
||||
width: max-content;
|
||||
|
||||
.euiFormLabel {
|
||||
padding-bottom: $euiSizeXS;
|
||||
}
|
||||
|
||||
&-small {
|
||||
width: $smallControl;
|
||||
}
|
||||
|
||||
&-medium {
|
||||
width: $mediumControl;
|
||||
}
|
||||
|
||||
&-large {
|
||||
width: $largeControl;
|
||||
}
|
||||
|
||||
&-twoLine {
|
||||
margin-top: -$euiSize * 1.25;
|
||||
}
|
||||
|
||||
.euiFormLabel, div {
|
||||
cursor: grabbing !important; // prevents cursor flickering while dragging the clone
|
||||
}
|
||||
|
||||
.controlFrame--draggable {
|
||||
cursor: grabbing;
|
||||
height: $euiButtonHeight;
|
||||
align-items: center;
|
||||
border-radius: $euiBorderRadius;
|
||||
@include euiFontSizeS;
|
||||
font-weight: $euiFontWeightSemiBold;
|
||||
@include euiFormControlDefaultShadow;
|
||||
background-color: $euiFormInputGroupLabelBackground;
|
||||
min-width: $controlMinWidth;
|
||||
}
|
||||
|
||||
.controlFrame--formControlLayout, .controlFrame--draggable {
|
||||
&-clone {
|
||||
box-shadow: 0 0 0 1px $euiShadowColor,
|
||||
0 1px 6px 0 $euiShadowColor;
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
|
||||
.controlFrame--dragHandle {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.controlFrame--wrapper {
|
||||
flex-basis: auto;
|
||||
position: relative;
|
||||
display: block;
|
||||
|
||||
.controlFrame--formControlLayout {
|
||||
width: 100%;
|
||||
min-width: $controlMinWidth;
|
||||
transition:background-color .1s, color .1s;
|
||||
|
||||
&__label {
|
||||
@include euiTextTruncate;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
&:not(.controlFrame--formControlLayout-clone) {
|
||||
.controlFrame--dragHandle {
|
||||
cursor: grab;
|
||||
}
|
||||
}
|
||||
|
||||
.controlFrame--control {
|
||||
height: 100%;
|
||||
transition: opacity .1s;
|
||||
|
||||
&.controlFrame--twoLine {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-small {
|
||||
width: $smallControl;
|
||||
}
|
||||
|
||||
&-medium {
|
||||
width: $mediumControl;
|
||||
}
|
||||
|
||||
&-large {
|
||||
width: $largeControl;
|
||||
}
|
||||
|
||||
&-insertBefore,
|
||||
&-insertAfter {
|
||||
.controlFrame--formControlLayout:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-color: transparentize($euiColorPrimary, .5);
|
||||
border-radius: $euiBorderRadius;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&-insertBefore {
|
||||
.controlFrame--formControlLayout:after {
|
||||
left: -$euiSizeS;
|
||||
}
|
||||
}
|
||||
|
||||
&-insertAfter {
|
||||
.controlFrame--formControlLayout:after {
|
||||
right: -$euiSizeS;
|
||||
}
|
||||
}
|
||||
|
||||
.controlFrame--floatingActions {
|
||||
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 1pt $euiColorLightShade;
|
||||
}
|
||||
|
||||
&-twoLine {
|
||||
right:$euiSizeXS;
|
||||
top: -$euiSizeXS;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.controlFrame--floatingActions {
|
||||
transition:visibility .1s, opacity .1s;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&-isDragging {
|
||||
.euiFormRow__labelWrapper {
|
||||
opacity: 0;
|
||||
}
|
||||
.controlFrame--formControlLayout {
|
||||
background-color: $euiColorEmptyShade !important;
|
||||
color: transparent !important;
|
||||
box-shadow: none;
|
||||
|
||||
.euiFormLabel {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.controlFrame--control {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ControlWidth } from '../types';
|
||||
import { ControlGroupStrings } from './control_group_strings';
|
||||
|
||||
export const CONTROL_GROUP_TYPE = 'control_group';
|
||||
|
||||
export const DEFAULT_CONTROL_WIDTH: ControlWidth = 'auto';
|
||||
|
||||
export const CONTROL_WIDTH_OPTIONS = [
|
||||
{
|
||||
id: `auto`,
|
||||
label: ControlGroupStrings.management.controlWidth.getAutoWidthTitle(),
|
||||
},
|
||||
{
|
||||
id: `small`,
|
||||
label: ControlGroupStrings.management.controlWidth.getSmallWidthTitle(),
|
||||
},
|
||||
{
|
||||
id: `medium`,
|
||||
label: ControlGroupStrings.management.controlWidth.getMediumWidthTitle(),
|
||||
},
|
||||
{
|
||||
id: `large`,
|
||||
label: ControlGroupStrings.management.controlWidth.getLargeWidthTitle(),
|
||||
},
|
||||
];
|
||||
|
||||
export const CONTROL_LAYOUT_OPTIONS = [
|
||||
{
|
||||
id: `oneLine`,
|
||||
label: ControlGroupStrings.management.controlStyle.getSingleLineTitle(),
|
||||
},
|
||||
{
|
||||
id: `twoLine`,
|
||||
label: ControlGroupStrings.management.controlStyle.getTwoLineTitle(),
|
||||
},
|
||||
];
|
|
@ -0,0 +1,224 @@
|
|||
/*
|
||||
* 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';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import {
|
||||
Container,
|
||||
EmbeddableFactory,
|
||||
EmbeddableFactoryNotFoundError,
|
||||
} from '../../../../../embeddable/public';
|
||||
import {
|
||||
InputControlEmbeddable,
|
||||
InputControlInput,
|
||||
InputControlOutput,
|
||||
IEditableControlFactory,
|
||||
ControlWidth,
|
||||
} from '../types';
|
||||
import { ControlsService } from '../controls_service';
|
||||
import { ControlGroupInput, ControlPanelState } from './types';
|
||||
import { ManageControlComponent } from './editor/manage_control';
|
||||
import { toMountPoint } from '../../../../../kibana_react/public';
|
||||
import { ControlGroup } from './component/control_group_component';
|
||||
import { PresentationOverlaysService } from '../../../services/overlays';
|
||||
import { CONTROL_GROUP_TYPE, DEFAULT_CONTROL_WIDTH } from './control_group_constants';
|
||||
import { ManageControlGroup } from './editor/manage_control_group_component';
|
||||
import { OverlayRef } from '../../../../../../core/public';
|
||||
import { ControlGroupStrings } from './control_group_strings';
|
||||
|
||||
export class ControlGroupContainer extends Container<InputControlInput, ControlGroupInput> {
|
||||
public readonly type = CONTROL_GROUP_TYPE;
|
||||
|
||||
private nextControlWidth: ControlWidth = DEFAULT_CONTROL_WIDTH;
|
||||
|
||||
constructor(
|
||||
initialInput: ControlGroupInput,
|
||||
private readonly controlsService: ControlsService,
|
||||
private readonly overlays: PresentationOverlaysService,
|
||||
parent?: Container
|
||||
) {
|
||||
super(initialInput, { embeddableLoaded: {} }, controlsService.getControlFactory, parent);
|
||||
this.overlays = overlays;
|
||||
this.controlsService = controlsService;
|
||||
}
|
||||
|
||||
protected createNewPanelState<TEmbeddableInput extends InputControlInput = InputControlInput>(
|
||||
factory: EmbeddableFactory<InputControlInput, InputControlOutput, InputControlEmbeddable>,
|
||||
partial: Partial<TEmbeddableInput> = {}
|
||||
): ControlPanelState<TEmbeddableInput> {
|
||||
const panelState = super.createNewPanelState(factory, partial);
|
||||
return {
|
||||
order: 1,
|
||||
width: this.nextControlWidth,
|
||||
...panelState,
|
||||
} as ControlPanelState<TEmbeddableInput>;
|
||||
}
|
||||
|
||||
protected getInheritedInput(id: string): InputControlInput {
|
||||
const { filters, query, timeRange, inheritParentState } = this.getInput();
|
||||
return {
|
||||
filters: inheritParentState.useFilters ? filters : undefined,
|
||||
query: inheritParentState.useQuery ? query : undefined,
|
||||
timeRange: inheritParentState.useTimerange ? timeRange : undefined,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
public createNewControl = async (type: string) => {
|
||||
const factory = this.controlsService.getControlFactory(type);
|
||||
if (!factory) throw new EmbeddableFactoryNotFoundError(type);
|
||||
|
||||
const initialInputPromise = new Promise<Omit<InputControlInput, 'id'>>((resolve, reject) => {
|
||||
let inputToReturn: Partial<InputControlInput> = {};
|
||||
|
||||
const onCancel = (ref: OverlayRef) => {
|
||||
this.overlays
|
||||
.openConfirm(ControlGroupStrings.management.discardNewControl.getSubtitle(), {
|
||||
confirmButtonText: ControlGroupStrings.management.discardNewControl.getConfirm(),
|
||||
cancelButtonText: ControlGroupStrings.management.discardNewControl.getCancel(),
|
||||
title: ControlGroupStrings.management.discardNewControl.getTitle(),
|
||||
buttonColor: 'danger',
|
||||
})
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
reject();
|
||||
ref.close();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const flyoutInstance = this.overlays.openFlyout(
|
||||
toMountPoint(
|
||||
<ManageControlComponent
|
||||
width={this.nextControlWidth}
|
||||
updateTitle={(newTitle) => (inputToReturn.title = newTitle)}
|
||||
updateWidth={(newWidth) => (this.nextControlWidth = newWidth)}
|
||||
controlEditorComponent={(factory as IEditableControlFactory).getControlEditor?.({
|
||||
onChange: (partialInput) => {
|
||||
inputToReturn = { ...inputToReturn, ...partialInput };
|
||||
},
|
||||
})}
|
||||
onSave={() => {
|
||||
resolve(inputToReturn);
|
||||
flyoutInstance.close();
|
||||
}}
|
||||
onCancel={() => onCancel(flyoutInstance)}
|
||||
/>
|
||||
),
|
||||
{
|
||||
onClose: (flyout) => onCancel(flyout),
|
||||
}
|
||||
);
|
||||
});
|
||||
initialInputPromise.then(
|
||||
async (explicitInput) => {
|
||||
await this.addNewEmbeddable(type, explicitInput);
|
||||
},
|
||||
() => {} // swallow promise rejection because it can be part of normal flow
|
||||
);
|
||||
};
|
||||
|
||||
public editControl = async (embeddableId: string) => {
|
||||
const panel = this.getInput().panels[embeddableId];
|
||||
const factory = this.getFactory(panel.type);
|
||||
const embeddable = await this.untilEmbeddableLoaded(embeddableId);
|
||||
|
||||
if (!factory) throw new EmbeddableFactoryNotFoundError(panel.type);
|
||||
|
||||
const initialExplicitInput = cloneDeep(panel.explicitInput);
|
||||
const initialWidth = panel.width;
|
||||
|
||||
const onCancel = (ref: OverlayRef) => {
|
||||
this.overlays
|
||||
.openConfirm(ControlGroupStrings.management.discardChanges.getSubtitle(), {
|
||||
confirmButtonText: ControlGroupStrings.management.discardChanges.getConfirm(),
|
||||
cancelButtonText: ControlGroupStrings.management.discardChanges.getCancel(),
|
||||
title: ControlGroupStrings.management.discardChanges.getTitle(),
|
||||
buttonColor: 'danger',
|
||||
})
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
embeddable.updateInput(initialExplicitInput);
|
||||
this.updateInput({
|
||||
panels: {
|
||||
...this.getInput().panels,
|
||||
[embeddableId]: { ...this.getInput().panels[embeddableId], width: initialWidth },
|
||||
},
|
||||
});
|
||||
ref.close();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const flyoutInstance = this.overlays.openFlyout(
|
||||
toMountPoint(
|
||||
<ManageControlComponent
|
||||
width={panel.width}
|
||||
title={embeddable.getTitle()}
|
||||
removeControl={() => this.removeEmbeddable(embeddableId)}
|
||||
updateTitle={(newTitle) => embeddable.updateInput({ title: newTitle })}
|
||||
controlEditorComponent={(factory as IEditableControlFactory).getControlEditor?.({
|
||||
onChange: (partialInput) => embeddable.updateInput(partialInput),
|
||||
initialInput: embeddable.getInput(),
|
||||
})}
|
||||
onCancel={() => onCancel(flyoutInstance)}
|
||||
onSave={() => flyoutInstance.close()}
|
||||
updateWidth={(newWidth) =>
|
||||
this.updateInput({
|
||||
panels: {
|
||||
...this.getInput().panels,
|
||||
[embeddableId]: { ...this.getInput().panels[embeddableId], width: newWidth },
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
),
|
||||
{
|
||||
onClose: (flyout) => onCancel(flyout),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
public editControlGroup = () => {
|
||||
const flyoutInstance = this.overlays.openFlyout(
|
||||
toMountPoint(
|
||||
<ManageControlGroup
|
||||
controlStyle={this.getInput().controlStyle}
|
||||
setControlStyle={(newStyle) => this.updateInput({ controlStyle: newStyle })}
|
||||
deleteAllEmbeddables={() => {
|
||||
this.overlays
|
||||
.openConfirm(ControlGroupStrings.management.deleteAllControls.getSubtitle(), {
|
||||
confirmButtonText: ControlGroupStrings.management.deleteAllControls.getConfirm(),
|
||||
cancelButtonText: ControlGroupStrings.management.deleteAllControls.getCancel(),
|
||||
title: ControlGroupStrings.management.deleteAllControls.getTitle(),
|
||||
buttonColor: 'danger',
|
||||
})
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
Object.keys(this.getInput().panels).forEach((id) => this.removeEmbeddable(id));
|
||||
flyoutInstance.close();
|
||||
}
|
||||
});
|
||||
}}
|
||||
setAllPanelWidths={(newWidth) => {
|
||||
const newPanels = cloneDeep(this.getInput().panels);
|
||||
Object.values(newPanels).forEach((panel) => (panel.width = newWidth));
|
||||
this.updateInput({ panels: { ...newPanels, ...newPanels } });
|
||||
}}
|
||||
panels={this.getInput().panels}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
public render(dom: HTMLElement) {
|
||||
ReactDOM.render(<ControlGroup controlGroupContainer={this} />, dom);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 {
|
||||
Container,
|
||||
ContainerOutput,
|
||||
EmbeddableFactory,
|
||||
EmbeddableFactoryDefinition,
|
||||
ErrorEmbeddable,
|
||||
} from '../../../../../embeddable/public';
|
||||
import { ControlGroupInput } from './types';
|
||||
import { ControlsService } from '../controls_service';
|
||||
import { ControlGroupStrings } from './control_group_strings';
|
||||
import { CONTROL_GROUP_TYPE } from './control_group_constants';
|
||||
import { ControlGroupContainer } from './control_group_container';
|
||||
import { PresentationOverlaysService } from '../../../services/overlays';
|
||||
|
||||
export type DashboardContainerFactory = EmbeddableFactory<
|
||||
ControlGroupInput,
|
||||
ContainerOutput,
|
||||
ControlGroupContainer
|
||||
>;
|
||||
export class ControlGroupContainerFactory
|
||||
implements EmbeddableFactoryDefinition<ControlGroupInput, ContainerOutput, ControlGroupContainer>
|
||||
{
|
||||
public readonly isContainerType = true;
|
||||
public readonly type = CONTROL_GROUP_TYPE;
|
||||
public readonly controlsService: ControlsService;
|
||||
private readonly overlays: PresentationOverlaysService;
|
||||
|
||||
constructor(controlsService: ControlsService, overlays: PresentationOverlaysService) {
|
||||
this.overlays = overlays;
|
||||
this.controlsService = controlsService;
|
||||
}
|
||||
|
||||
public isEditable = async () => false;
|
||||
|
||||
public readonly getDisplayName = () => {
|
||||
return ControlGroupStrings.getEmbeddableTitle();
|
||||
};
|
||||
|
||||
public getDefaultInput(): Partial<ControlGroupInput> {
|
||||
return {
|
||||
panels: {},
|
||||
inheritParentState: {
|
||||
useFilters: true,
|
||||
useQuery: true,
|
||||
useTimerange: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public create = async (
|
||||
initialInput: ControlGroupInput,
|
||||
parent?: Container
|
||||
): Promise<ControlGroupContainer | ErrorEmbeddable> => {
|
||||
return new ControlGroupContainer(initialInput, this.controlsService, this.overlays, parent);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ControlGroupStrings = {
|
||||
getEmbeddableTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.title', {
|
||||
defaultMessage: 'Control group',
|
||||
}),
|
||||
manageControl: {
|
||||
getFlyoutTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.manageControl.flyoutTitle', {
|
||||
defaultMessage: 'Manage control',
|
||||
}),
|
||||
getTitleInputTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.manageControl.titleInputTitle', {
|
||||
defaultMessage: 'Title',
|
||||
}),
|
||||
getWidthInputTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.manageControl.widthInputTitle', {
|
||||
defaultMessage: 'Control width',
|
||||
}),
|
||||
getSaveChangesTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.manageControl.saveChangesTitle', {
|
||||
defaultMessage: 'Save and close',
|
||||
}),
|
||||
getCancelTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.manageControl.cancelTitle', {
|
||||
defaultMessage: 'Cancel',
|
||||
}),
|
||||
},
|
||||
management: {
|
||||
getAddControlTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.addControl', {
|
||||
defaultMessage: 'Add control',
|
||||
}),
|
||||
getManageButtonTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.buttonTitle', {
|
||||
defaultMessage: 'Manage controls',
|
||||
}),
|
||||
getFlyoutTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.flyoutTitle', {
|
||||
defaultMessage: 'Manage controls',
|
||||
}),
|
||||
getDesignTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.designTitle', {
|
||||
defaultMessage: 'Design',
|
||||
}),
|
||||
getWidthTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.widthTitle', {
|
||||
defaultMessage: 'Width',
|
||||
}),
|
||||
getLayoutTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.layoutTitle', {
|
||||
defaultMessage: 'Layout',
|
||||
}),
|
||||
getDeleteButtonTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.delete', {
|
||||
defaultMessage: 'Delete control',
|
||||
}),
|
||||
getDeleteAllButtonTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteAll', {
|
||||
defaultMessage: 'Delete all',
|
||||
}),
|
||||
controlWidth: {
|
||||
getChangeAllControlWidthsTitle: () =>
|
||||
i18n.translate(
|
||||
'presentationUtil.inputControls.controlGroup.management.layout.changeAllControlWidths',
|
||||
{
|
||||
defaultMessage: 'Set width for all controls',
|
||||
}
|
||||
),
|
||||
getWidthSwitchLegend: () =>
|
||||
i18n.translate(
|
||||
'presentationUtil.inputControls.controlGroup.management.layout.controlWidthLegend',
|
||||
{
|
||||
defaultMessage: 'Change individual control width',
|
||||
}
|
||||
),
|
||||
getAutoWidthTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.layout.auto', {
|
||||
defaultMessage: 'Auto',
|
||||
}),
|
||||
getSmallWidthTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.layout.small', {
|
||||
defaultMessage: 'Small',
|
||||
}),
|
||||
getMediumWidthTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.layout.medium', {
|
||||
defaultMessage: 'Medium',
|
||||
}),
|
||||
getLargeWidthTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.layout.large', {
|
||||
defaultMessage: 'Large',
|
||||
}),
|
||||
},
|
||||
controlStyle: {
|
||||
getDesignSwitchLegend: () =>
|
||||
i18n.translate(
|
||||
'presentationUtil.inputControls.controlGroup.management.layout.designSwitchLegend',
|
||||
{
|
||||
defaultMessage: 'Switch control designs',
|
||||
}
|
||||
),
|
||||
getSingleLineTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.layout.singleLine', {
|
||||
defaultMessage: 'Single line layout',
|
||||
}),
|
||||
getTwoLineTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.layout.twoLine', {
|
||||
defaultMessage: 'Two line layout',
|
||||
}),
|
||||
},
|
||||
deleteAllControls: {
|
||||
getTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteAll.title', {
|
||||
defaultMessage: 'Delete all?',
|
||||
}),
|
||||
getSubtitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteAll.sub', {
|
||||
defaultMessage: 'Controls are not recoverable once removed.',
|
||||
}),
|
||||
getConfirm: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteAll.confirm', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
getCancel: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteAll.cancel', {
|
||||
defaultMessage: 'Cancel',
|
||||
}),
|
||||
},
|
||||
discardChanges: {
|
||||
getTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.discard.title', {
|
||||
defaultMessage: 'Discard?',
|
||||
}),
|
||||
getSubtitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.discard.sub', {
|
||||
defaultMessage:
|
||||
'Discard changes to this control? Controls are not recoverable once removed.',
|
||||
}),
|
||||
getConfirm: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.discard.confirm', {
|
||||
defaultMessage: 'Discard',
|
||||
}),
|
||||
getCancel: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.discard.cancel', {
|
||||
defaultMessage: 'Cancel',
|
||||
}),
|
||||
},
|
||||
discardNewControl: {
|
||||
getTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteNew.title', {
|
||||
defaultMessage: 'Discard?',
|
||||
}),
|
||||
getSubtitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteNew.sub', {
|
||||
defaultMessage: 'Discard new control? Controls are not recoverable once removed.',
|
||||
}),
|
||||
getConfirm: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteNew.confirm', {
|
||||
defaultMessage: 'Discard',
|
||||
}),
|
||||
getCancel: () =>
|
||||
i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteNew.cancel', {
|
||||
defaultMessage: 'Cancel',
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* 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, { useEffect, useState } from 'react';
|
||||
import {
|
||||
EuiFlyoutHeader,
|
||||
EuiButtonGroup,
|
||||
EuiFlyoutBody,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTitle,
|
||||
EuiFieldText,
|
||||
EuiFlyoutFooter,
|
||||
EuiButton,
|
||||
EuiFormRow,
|
||||
EuiForm,
|
||||
EuiButtonEmpty,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { ControlEditorComponent, ControlWidth } from '../../types';
|
||||
import { CONTROL_WIDTH_OPTIONS } from '../control_group_constants';
|
||||
|
||||
interface ManageControlProps {
|
||||
title?: string;
|
||||
onSave: () => void;
|
||||
width: ControlWidth;
|
||||
onCancel: () => void;
|
||||
removeControl?: () => void;
|
||||
controlEditorComponent?: ControlEditorComponent;
|
||||
updateTitle: (title: string) => void;
|
||||
updateWidth: (newWidth: ControlWidth) => void;
|
||||
}
|
||||
|
||||
export const ManageControlComponent = ({
|
||||
controlEditorComponent,
|
||||
removeControl,
|
||||
updateTitle,
|
||||
updateWidth,
|
||||
onCancel,
|
||||
onSave,
|
||||
title,
|
||||
width,
|
||||
}: ManageControlProps) => {
|
||||
const [currentTitle, setCurrentTitle] = useState(title);
|
||||
const [currentWidth, setCurrentWidth] = useState(width);
|
||||
|
||||
const [controlEditorValid, setControlEditorValid] = useState(false);
|
||||
const [editorValid, setEditorValid] = useState(false);
|
||||
|
||||
useEffect(() => setEditorValid(Boolean(currentTitle)), [currentTitle]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>{ControlGroupStrings.manageControl.getFlyoutTitle()}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiForm>
|
||||
<EuiFormRow label={ControlGroupStrings.manageControl.getTitleInputTitle()}>
|
||||
<EuiFieldText
|
||||
placeholder="Placeholder text"
|
||||
value={currentTitle}
|
||||
onChange={(e) => {
|
||||
updateTitle(e.target.value);
|
||||
setCurrentTitle(e.target.value);
|
||||
}}
|
||||
aria-label="Use aria labels when no actual label is in use"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow label={ControlGroupStrings.manageControl.getWidthInputTitle()}>
|
||||
<EuiButtonGroup
|
||||
color="primary"
|
||||
legend={ControlGroupStrings.management.controlWidth.getWidthSwitchLegend()}
|
||||
options={CONTROL_WIDTH_OPTIONS}
|
||||
idSelected={currentWidth}
|
||||
onChange={(newWidth: string) => {
|
||||
setCurrentWidth(newWidth as ControlWidth);
|
||||
updateWidth(newWidth as ControlWidth);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
{controlEditorComponent &&
|
||||
controlEditorComponent({ setValidState: setControlEditorValid })}
|
||||
<EuiSpacer size="l" />
|
||||
{removeControl && (
|
||||
<EuiButtonEmpty
|
||||
aria-label={`delete-${title}`}
|
||||
iconType="trash"
|
||||
flush="left"
|
||||
color="danger"
|
||||
onClick={() => {
|
||||
onCancel();
|
||||
removeControl();
|
||||
}}
|
||||
>
|
||||
{ControlGroupStrings.management.getDeleteButtonTitle()}
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</EuiForm>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
aria-label={`delete-${title}`}
|
||||
iconType="cross"
|
||||
onClick={() => {
|
||||
onCancel();
|
||||
}}
|
||||
>
|
||||
{ControlGroupStrings.manageControl.getCancelTitle()}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
aria-label={`delete-${title}`}
|
||||
iconType="check"
|
||||
color="primary"
|
||||
disabled={!editorValid || !controlEditorValid}
|
||||
onClick={() => {
|
||||
onSave();
|
||||
}}
|
||||
>
|
||||
{ControlGroupStrings.manageControl.getSaveChangesTitle()}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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 useMount from 'react-use/lib/useMount';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
EuiFlyoutHeader,
|
||||
EuiButtonEmpty,
|
||||
EuiButtonGroup,
|
||||
EuiFlyoutBody,
|
||||
EuiFormRow,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { ControlsPanels } from '../types';
|
||||
import { ControlStyle, ControlWidth } from '../../types';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { CONTROL_LAYOUT_OPTIONS, CONTROL_WIDTH_OPTIONS } from '../control_group_constants';
|
||||
|
||||
interface ManageControlGroupProps {
|
||||
panels: ControlsPanels;
|
||||
controlStyle: ControlStyle;
|
||||
deleteAllEmbeddables: () => void;
|
||||
setControlStyle: (style: ControlStyle) => void;
|
||||
setAllPanelWidths: (newWidth: ControlWidth) => void;
|
||||
}
|
||||
|
||||
export const ManageControlGroup = ({
|
||||
panels,
|
||||
controlStyle,
|
||||
setControlStyle,
|
||||
setAllPanelWidths,
|
||||
deleteAllEmbeddables,
|
||||
}: ManageControlGroupProps) => {
|
||||
const [currentControlStyle, setCurrentControlStyle] = useState<ControlStyle>(controlStyle);
|
||||
const [selectedWidth, setSelectedWidth] = useState<ControlWidth>();
|
||||
const [selectionDisplay, setSelectionDisplay] = useState(false);
|
||||
|
||||
useMount(() => {
|
||||
if (!panels || Object.keys(panels).length === 0) return;
|
||||
const firstWidth = panels[Object.keys(panels)[0]].width;
|
||||
if (Object.values(panels).every((panel) => panel.width === firstWidth)) {
|
||||
setSelectedWidth(firstWidth);
|
||||
}
|
||||
});
|
||||
|
||||
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={currentControlStyle}
|
||||
onChange={(newControlStyle) => {
|
||||
setControlStyle(newControlStyle as ControlStyle);
|
||||
setCurrentControlStyle(newControlStyle as ControlStyle);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow label={ControlGroupStrings.management.getWidthTitle()}>
|
||||
<EuiSwitch
|
||||
label={ControlGroupStrings.management.controlWidth.getChangeAllControlWidthsTitle()}
|
||||
checked={selectionDisplay}
|
||||
onChange={() => setSelectionDisplay(!selectionDisplay)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{selectionDisplay ? (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiButtonGroup
|
||||
color="primary"
|
||||
idSelected={selectedWidth ?? ''}
|
||||
legend={ControlGroupStrings.management.controlWidth.getWidthSwitchLegend()}
|
||||
options={CONTROL_WIDTH_OPTIONS}
|
||||
onChange={(newWidth: string) => {
|
||||
setAllPanelWidths(newWidth as ControlWidth);
|
||||
setSelectedWidth(newWidth as ControlWidth);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : undefined}
|
||||
|
||||
<EuiSpacer size="xl" />
|
||||
|
||||
<EuiButtonEmpty
|
||||
onClick={deleteAllEmbeddables}
|
||||
aria-label={'delete-all'}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
flush="left"
|
||||
size="s"
|
||||
>
|
||||
{ControlGroupStrings.management.getDeleteAllButtonTitle()}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlyoutBody>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { PanelState, EmbeddableInput } from '../../../../../embeddable/public';
|
||||
import { ControlStyle, ControlWidth, InputControlInput } from '../types';
|
||||
|
||||
export interface ControlGroupInput
|
||||
extends EmbeddableInput,
|
||||
Omit<InputControlInput, 'twoLineLayout'> {
|
||||
inheritParentState: {
|
||||
useFilters: boolean;
|
||||
useQuery: boolean;
|
||||
useTimerange: boolean;
|
||||
};
|
||||
controlStyle: ControlStyle;
|
||||
panels: ControlsPanels;
|
||||
}
|
||||
|
||||
export interface ControlPanelState<TEmbeddableInput extends InputControlInput = InputControlInput>
|
||||
extends PanelState<TEmbeddableInput> {
|
||||
order: number;
|
||||
width: ControlWidth;
|
||||
}
|
||||
|
||||
export interface ControlsPanels {
|
||||
[panelId: string]: ControlPanelState;
|
||||
}
|
|
@ -15,7 +15,7 @@ import { OptionsListStrings } from './options_list_strings';
|
|||
import { OptionsListPopover } from './options_list_popover_component';
|
||||
|
||||
import './options_list.scss';
|
||||
import { useStateObservable } from '../../use_state_observable';
|
||||
import { useStateObservable } from '../../hooks/use_state_observable';
|
||||
|
||||
export interface OptionsListComponentState {
|
||||
availableOptions?: EuiSelectableOption[];
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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 { EuiFormRow, EuiSuperSelect, EuiSuperSelectOption } from '@elastic/eui';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import useMount from 'react-use/lib/useMount';
|
||||
import { ControlEditorProps, GetControlEditorComponentProps } from '../../types';
|
||||
import {
|
||||
OptionsListEmbeddableInput,
|
||||
OptionsListFieldFetcher,
|
||||
OptionsListIndexPatternFetcher,
|
||||
} from './options_list_embeddable';
|
||||
import { OptionsListStrings } from './options_list_strings';
|
||||
|
||||
interface OptionsListEditorProps extends ControlEditorProps {
|
||||
onChange: GetControlEditorComponentProps<OptionsListEmbeddableInput>['onChange'];
|
||||
fetchIndexPatterns: OptionsListIndexPatternFetcher;
|
||||
initialInput?: Partial<OptionsListEmbeddableInput>;
|
||||
fetchFields: OptionsListFieldFetcher;
|
||||
}
|
||||
|
||||
interface OptionsListEditorState {
|
||||
availableIndexPatterns: Array<EuiSuperSelectOption<string>>;
|
||||
indexPattern?: string;
|
||||
availableFields: Array<EuiSuperSelectOption<string>>;
|
||||
field?: string;
|
||||
}
|
||||
|
||||
export const OptionsListEditor = ({
|
||||
onChange,
|
||||
fetchFields,
|
||||
initialInput,
|
||||
setValidState,
|
||||
fetchIndexPatterns,
|
||||
}: OptionsListEditorProps) => {
|
||||
const [state, setState] = useState<OptionsListEditorState>({
|
||||
indexPattern: initialInput?.indexPattern,
|
||||
field: initialInput?.field,
|
||||
availableIndexPatterns: [],
|
||||
availableFields: [],
|
||||
});
|
||||
|
||||
const applySelection = ({ field, indexPattern }: { field?: string; indexPattern?: string }) => {
|
||||
const newState = { ...(field ? { field } : {}), ...(indexPattern ? { indexPattern } : {}) };
|
||||
/**
|
||||
* apply state and run onChange concurrently. State is copied here rather than by subscribing to embeddable
|
||||
* input so that the same editor component can cover the 'create' use case.
|
||||
*/
|
||||
|
||||
setState((currentState) => {
|
||||
return { ...currentState, ...newState };
|
||||
});
|
||||
onChange(newState);
|
||||
};
|
||||
|
||||
useMount(() => {
|
||||
(async () => {
|
||||
const indexPatterns = (await fetchIndexPatterns()).map((indexPattern) => ({
|
||||
value: indexPattern,
|
||||
inputDisplay: indexPattern,
|
||||
}));
|
||||
setState((currentState) => ({ ...currentState, availableIndexPatterns: indexPatterns }));
|
||||
})();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
let availableFields: Array<EuiSuperSelectOption<string>> = [];
|
||||
if (state.indexPattern) {
|
||||
availableFields = (await fetchFields(state.indexPattern)).map((field) => ({
|
||||
value: field,
|
||||
inputDisplay: field,
|
||||
}));
|
||||
}
|
||||
setState((currentState) => ({ ...currentState, availableFields }));
|
||||
})();
|
||||
}, [state.indexPattern, fetchFields]);
|
||||
|
||||
useEffect(
|
||||
() => setValidState(Boolean(state.field) && Boolean(state.indexPattern)),
|
||||
[state.field, setValidState, state.indexPattern]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow label={OptionsListStrings.editor.getIndexPatternTitle()}>
|
||||
<EuiSuperSelect
|
||||
options={state.availableIndexPatterns}
|
||||
onChange={(indexPattern) => applySelection({ indexPattern })}
|
||||
valueOfSelected={state.indexPattern}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow label={OptionsListStrings.editor.getFieldTitle()}>
|
||||
<EuiSuperSelect
|
||||
disabled={!state.indexPattern}
|
||||
options={state.availableFields}
|
||||
onChange={(field) => applySelection({ field })}
|
||||
valueOfSelected={state.field}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -15,9 +15,9 @@ import { tap, debounceTime, map, distinctUntilChanged } from 'rxjs/operators';
|
|||
|
||||
import { esFilters } from '../../../../../../data/public';
|
||||
import { OptionsListStrings } from './options_list_strings';
|
||||
import { Embeddable, IContainer } from '../../../../../../embeddable/public';
|
||||
import { InputControlInput, InputControlOutput } from '../../types';
|
||||
import { OptionsListComponent, OptionsListComponentState } from './options_list_component';
|
||||
import { Embeddable } from '../../../../../../embeddable/public';
|
||||
import { InputControlInput, InputControlOutput } from '../../embeddable/types';
|
||||
|
||||
const toggleAvailableOptions = (
|
||||
indices: number[],
|
||||
|
@ -50,6 +50,9 @@ interface OptionsListDataFetchProps {
|
|||
timeRange?: InputControlInput['timeRange'];
|
||||
}
|
||||
|
||||
export type OptionsListIndexPatternFetcher = () => Promise<string[]>; // TODO: use the proper types here.
|
||||
export type OptionsListFieldFetcher = (indexPattern: string) => Promise<string[]>; // TODO: use the proper types here.
|
||||
|
||||
export type OptionsListDataFetcher = (
|
||||
props: OptionsListDataFetchProps
|
||||
) => Promise<EuiSelectableOption[]>;
|
||||
|
@ -58,7 +61,7 @@ export const OPTIONS_LIST_CONTROL = 'optionsListControl';
|
|||
export interface OptionsListEmbeddableInput extends InputControlInput {
|
||||
field: string;
|
||||
indexPattern: string;
|
||||
multiSelect: boolean;
|
||||
singleSelect?: boolean;
|
||||
defaultSelections?: string[];
|
||||
}
|
||||
export class OptionsListEmbeddable extends Embeddable<
|
||||
|
@ -66,14 +69,11 @@ export class OptionsListEmbeddable extends Embeddable<
|
|||
InputControlOutput
|
||||
> {
|
||||
public readonly type = OPTIONS_LIST_CONTROL;
|
||||
|
||||
private node?: HTMLElement;
|
||||
private fetchData: OptionsListDataFetcher;
|
||||
|
||||
// internal state for this input control.
|
||||
private selectedOptions: Set<string>;
|
||||
private typeaheadSubject: Subject<string> = new Subject<string>();
|
||||
private searchString: string = '';
|
||||
|
||||
private componentState: OptionsListComponentState;
|
||||
private componentStateSubject$ = new Subject<OptionsListComponentState>();
|
||||
|
@ -88,9 +88,10 @@ export class OptionsListEmbeddable extends Embeddable<
|
|||
constructor(
|
||||
input: OptionsListEmbeddableInput,
|
||||
output: InputControlOutput,
|
||||
fetchData: OptionsListDataFetcher
|
||||
private fetchData: OptionsListDataFetcher,
|
||||
parent?: IContainer
|
||||
) {
|
||||
super(input, output);
|
||||
super(input, output, parent);
|
||||
this.fetchData = fetchData;
|
||||
|
||||
// populate default selections from input
|
||||
|
@ -99,7 +100,7 @@ export class OptionsListEmbeddable extends Embeddable<
|
|||
|
||||
// fetch available options when input changes or when search string has changed
|
||||
const typeaheadPipe = this.typeaheadSubject.pipe(
|
||||
tap((newSearchString) => (this.searchString = newSearchString)),
|
||||
tap((newSearchString) => this.updateComponentState({ searchString: newSearchString })),
|
||||
debounceTime(100)
|
||||
);
|
||||
const inputPipe = this.getInput$().pipe(
|
||||
|
@ -136,7 +137,7 @@ export class OptionsListEmbeddable extends Embeddable<
|
|||
|
||||
const { indexPattern, timeRange, filters, field, query } = this.getInput();
|
||||
let newOptions = await this.fetchData({
|
||||
search: this.searchString,
|
||||
search: this.componentState.searchString,
|
||||
indexPattern,
|
||||
timeRange,
|
||||
filters,
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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';
|
||||
import { EmbeddableFactoryDefinition, IContainer } from '../../../../../../embeddable/public';
|
||||
import {
|
||||
ControlEditorProps,
|
||||
GetControlEditorComponentProps,
|
||||
IEditableControlFactory,
|
||||
} from '../../types';
|
||||
import { OptionsListEditor } from './options_list_editor';
|
||||
import {
|
||||
OptionsListDataFetcher,
|
||||
OptionsListEmbeddable,
|
||||
OptionsListEmbeddableInput,
|
||||
OptionsListFieldFetcher,
|
||||
OptionsListIndexPatternFetcher,
|
||||
OPTIONS_LIST_CONTROL,
|
||||
} from './options_list_embeddable';
|
||||
|
||||
export class OptionsListEmbeddableFactory
|
||||
implements EmbeddableFactoryDefinition, IEditableControlFactory
|
||||
{
|
||||
public type = OPTIONS_LIST_CONTROL;
|
||||
|
||||
constructor(
|
||||
private fetchData: OptionsListDataFetcher,
|
||||
private fetchIndexPatterns: OptionsListIndexPatternFetcher,
|
||||
private fetchFields: OptionsListFieldFetcher
|
||||
) {
|
||||
this.fetchIndexPatterns = fetchIndexPatterns;
|
||||
this.fetchFields = fetchFields;
|
||||
this.fetchData = fetchData;
|
||||
}
|
||||
|
||||
public create(initialInput: OptionsListEmbeddableInput, parent?: IContainer) {
|
||||
return Promise.resolve(new OptionsListEmbeddable(initialInput, {}, this.fetchData, parent));
|
||||
}
|
||||
|
||||
public getControlEditor = ({
|
||||
onChange,
|
||||
initialInput,
|
||||
}: GetControlEditorComponentProps<OptionsListEmbeddableInput>) => {
|
||||
return ({ setValidState }: ControlEditorProps) => (
|
||||
<OptionsListEditor
|
||||
fetchIndexPatterns={this.fetchIndexPatterns}
|
||||
fetchFields={this.fetchFields}
|
||||
setValidState={setValidState}
|
||||
initialInput={initialInput}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
public isEditable = () => Promise.resolve(false);
|
||||
|
||||
public getDisplayName = () => 'Options List Control';
|
||||
}
|
|
@ -19,6 +19,16 @@ export const OptionsListStrings = {
|
|||
defaultMessage: 'Select...',
|
||||
}),
|
||||
},
|
||||
editor: {
|
||||
getIndexPatternTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.optionsList.editor.indexPatternTitle', {
|
||||
defaultMessage: 'Index pattern',
|
||||
}),
|
||||
getFieldTitle: () =>
|
||||
i18n.translate('presentationUtil.inputControls.optionsList.editor.fieldTitle', {
|
||||
defaultMessage: 'Field',
|
||||
}),
|
||||
},
|
||||
popover: {
|
||||
getLoadingMessage: () =>
|
||||
i18n.translate('presentationUtil.inputControls.optionsList.popover.loading', {
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { EmbeddableFactory } from '../../../../embeddable/public';
|
||||
import {
|
||||
ControlTypeRegistry,
|
||||
InputControlEmbeddable,
|
||||
InputControlFactory,
|
||||
InputControlInput,
|
||||
InputControlOutput,
|
||||
} from './types';
|
||||
|
||||
export class ControlsService {
|
||||
private controlsFactoriesMap: ControlTypeRegistry = {};
|
||||
|
||||
public registerInputControlType = (factory: InputControlFactory) => {
|
||||
this.controlsFactoriesMap[factory.type] = factory;
|
||||
};
|
||||
|
||||
public getControlFactory = <
|
||||
I extends InputControlInput = InputControlInput,
|
||||
O extends InputControlOutput = InputControlOutput,
|
||||
E extends InputControlEmbeddable<I, O> = InputControlEmbeddable<I, O>
|
||||
>(
|
||||
type: string
|
||||
) => {
|
||||
return this.controlsFactoriesMap[type] as EmbeddableFactory<I, O, E>;
|
||||
};
|
||||
|
||||
public getInputControlTypes = () => Object.keys(this.controlsFactoriesMap);
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { useEffect, useState } from 'react';
|
||||
import { InputControlEmbeddable } from '../types';
|
||||
import { IContainer } from '../../../../../embeddable/public';
|
||||
|
||||
export const useChildEmbeddable = ({
|
||||
container,
|
||||
embeddableId,
|
||||
}: {
|
||||
container: IContainer;
|
||||
embeddableId: string;
|
||||
}) => {
|
||||
const [embeddable, setEmbeddable] = useState<InputControlEmbeddable>();
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
(async () => {
|
||||
const newEmbeddable = await container.untilEmbeddableLoaded(embeddableId);
|
||||
if (!mounted) return;
|
||||
setEmbeddable(newEmbeddable);
|
||||
})();
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [container, embeddableId]);
|
||||
|
||||
return embeddable;
|
||||
};
|
|
@ -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 { Filter } from '@kbn/es-query';
|
||||
import { Query, TimeRange } from '../../../../data/public';
|
||||
import {
|
||||
EmbeddableFactory,
|
||||
EmbeddableInput,
|
||||
EmbeddableOutput,
|
||||
IEmbeddable,
|
||||
} from '../../../../embeddable/public';
|
||||
|
||||
export type ControlWidth = 'auto' | 'small' | 'medium' | 'large';
|
||||
export type ControlStyle = 'twoLine' | 'oneLine';
|
||||
|
||||
/**
|
||||
* Control embeddable types
|
||||
*/
|
||||
export type InputControlFactory = EmbeddableFactory<
|
||||
InputControlInput,
|
||||
InputControlOutput,
|
||||
InputControlEmbeddable
|
||||
>;
|
||||
|
||||
export interface ControlTypeRegistry {
|
||||
[key: string]: InputControlFactory;
|
||||
}
|
||||
|
||||
export type InputControlInput = EmbeddableInput & {
|
||||
query?: Query;
|
||||
filters?: Filter[];
|
||||
timeRange?: TimeRange;
|
||||
twoLineLayout?: boolean;
|
||||
};
|
||||
|
||||
export type InputControlOutput = EmbeddableOutput & {
|
||||
filters?: Filter[];
|
||||
};
|
||||
|
||||
export type InputControlEmbeddable<
|
||||
TInputControlEmbeddableInput extends InputControlInput = InputControlInput,
|
||||
TInputControlEmbeddableOutput extends InputControlOutput = InputControlOutput
|
||||
> = IEmbeddable<TInputControlEmbeddableInput, TInputControlEmbeddableOutput>;
|
||||
|
||||
/**
|
||||
* Control embeddable editor types
|
||||
*/
|
||||
export interface IEditableControlFactory<T extends InputControlInput = InputControlInput> {
|
||||
getControlEditor?: GetControlEditorComponent<T>;
|
||||
}
|
||||
|
||||
export type GetControlEditorComponent<T extends InputControlInput = InputControlInput> = (
|
||||
props: GetControlEditorComponentProps<T>
|
||||
) => ControlEditorComponent;
|
||||
export interface GetControlEditorComponentProps<T extends InputControlInput = InputControlInput> {
|
||||
onChange: (partial: Partial<T>) => void;
|
||||
initialInput?: Partial<T>;
|
||||
}
|
||||
|
||||
export type ControlEditorComponent = (props: ControlEditorProps) => JSX.Element;
|
||||
|
||||
export interface ControlEditorProps {
|
||||
setValidState: (valid: boolean) => void;
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import { decorators } from './decorators';
|
||||
import { getEuiSelectableOptions, flightFields, flightFieldLabels, FlightField } from './flights';
|
||||
import { OptionsListEmbeddableFactory, OptionsListEmbeddable } from '../control_types/options_list';
|
||||
import { ControlFrame } from '../control_frame/control_frame';
|
||||
|
||||
export default {
|
||||
title: 'Input Controls',
|
||||
description: '',
|
||||
decorators,
|
||||
};
|
||||
|
||||
interface OptionsListStorybookArgs {
|
||||
fields: string[];
|
||||
twoLine: boolean;
|
||||
}
|
||||
|
||||
const storybookArgs = {
|
||||
twoLine: false,
|
||||
fields: ['OriginCityName', 'OriginWeather', 'DestCityName', 'DestWeather'],
|
||||
};
|
||||
|
||||
const storybookArgTypes = {
|
||||
fields: {
|
||||
twoLine: {
|
||||
control: { type: 'bool' },
|
||||
},
|
||||
control: {
|
||||
type: 'check',
|
||||
options: flightFields,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const OptionsListStoryComponent = ({ fields, twoLine }: OptionsListStorybookArgs) => {
|
||||
const [embeddables, setEmbeddables] = useState<OptionsListEmbeddable[]>([]);
|
||||
|
||||
const optionsListEmbeddableFactory = useMemo(
|
||||
() =>
|
||||
new OptionsListEmbeddableFactory(
|
||||
({ field, search }) =>
|
||||
new Promise((r) => setTimeout(() => r(getEuiSelectableOptions(field, search)), 500))
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const embeddableCreatePromises = fields.map((field) => {
|
||||
return optionsListEmbeddableFactory.create({
|
||||
field,
|
||||
id: '',
|
||||
indexPattern: '',
|
||||
multiSelect: true,
|
||||
twoLineLayout: twoLine,
|
||||
title: flightFieldLabels[field as FlightField],
|
||||
});
|
||||
});
|
||||
Promise.all(embeddableCreatePromises).then((newEmbeddables) => setEmbeddables(newEmbeddables));
|
||||
}, [fields, optionsListEmbeddableFactory, twoLine]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" wrap={true} gutterSize={'s'}>
|
||||
{embeddables.map((embeddable) => (
|
||||
<EuiFlexItem key={embeddable.getInput().field}>
|
||||
<ControlFrame twoLine={twoLine} embeddable={embeddable} />
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const OptionsListStory = ({ fields, twoLine }: OptionsListStorybookArgs) => (
|
||||
<OptionsListStoryComponent fields={fields} twoLine={twoLine} />
|
||||
);
|
||||
|
||||
OptionsListStory.args = storybookArgs;
|
||||
OptionsListStory.argTypes = storybookArgTypes;
|
|
@ -1,14 +0,0 @@
|
|||
.controlFrame--formControlLayout {
|
||||
width: 100%;
|
||||
min-width: $euiSize * 12.5;
|
||||
}
|
||||
|
||||
.controlFrame--control {
|
||||
&.optionsList--filterBtnSingle {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.optionsList--filterBtnTwoLine {
|
||||
width: 100%;
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import useMount from 'react-use/lib/useMount';
|
||||
import classNames from 'classnames';
|
||||
import { EuiFormControlLayout, EuiFormLabel, EuiFormRow } from '@elastic/eui';
|
||||
|
||||
import { InputControlEmbeddable } from '../embeddable/types';
|
||||
|
||||
import './control_frame.scss';
|
||||
|
||||
interface ControlFrameProps {
|
||||
embeddable: InputControlEmbeddable;
|
||||
twoLine?: boolean;
|
||||
}
|
||||
|
||||
export const ControlFrame = ({ twoLine, embeddable }: ControlFrameProps) => {
|
||||
const embeddableRoot: React.RefObject<HTMLDivElement> = useMemo(() => React.createRef(), []);
|
||||
|
||||
useMount(() => {
|
||||
if (embeddableRoot.current && embeddable) embeddable.render(embeddableRoot.current);
|
||||
});
|
||||
|
||||
const form = (
|
||||
<EuiFormControlLayout
|
||||
className="controlFrame--formControlLayout"
|
||||
fullWidth
|
||||
prepend={
|
||||
twoLine ? undefined : (
|
||||
<EuiFormLabel htmlFor={embeddable.id}>{embeddable.getInput().title}</EuiFormLabel>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={classNames('controlFrame--control', {
|
||||
'optionsList--filterBtnTwoLine': twoLine,
|
||||
'optionsList--filterBtnSingle': !twoLine,
|
||||
})}
|
||||
id={embeddable.id}
|
||||
ref={embeddableRoot}
|
||||
/>
|
||||
</EuiFormControlLayout>
|
||||
);
|
||||
|
||||
return twoLine ? (
|
||||
<EuiFormRow fullWidth label={embeddable.getInput().title}>
|
||||
{form}
|
||||
</EuiFormRow>
|
||||
) : (
|
||||
form
|
||||
);
|
||||
};
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EmbeddableFactoryDefinition } from '../../../../../../embeddable/public';
|
||||
import {
|
||||
OptionsListDataFetcher,
|
||||
OptionsListEmbeddable,
|
||||
OptionsListEmbeddableInput,
|
||||
OPTIONS_LIST_CONTROL,
|
||||
} from './options_list_embeddable';
|
||||
|
||||
export class OptionsListEmbeddableFactory implements EmbeddableFactoryDefinition {
|
||||
public type = OPTIONS_LIST_CONTROL;
|
||||
private fetchData: OptionsListDataFetcher;
|
||||
|
||||
constructor(fetchData: OptionsListDataFetcher) {
|
||||
this.fetchData = fetchData;
|
||||
}
|
||||
|
||||
public create(initialInput: OptionsListEmbeddableInput) {
|
||||
return Promise.resolve(new OptionsListEmbeddable(initialInput, {}, this.fetchData));
|
||||
}
|
||||
|
||||
public isEditable = () => Promise.resolve(false);
|
||||
|
||||
public getDisplayName = () => 'Options List Control';
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Filter, Query, TimeRange } from '../../../../../data/public';
|
||||
import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from '../../../../../embeddable/public';
|
||||
|
||||
export type InputControlInput = EmbeddableInput & {
|
||||
filters?: Filter[];
|
||||
query?: Query;
|
||||
timeRange?: TimeRange;
|
||||
twoLineLayout?: boolean;
|
||||
};
|
||||
|
||||
export type InputControlOutput = EmbeddableOutput & {
|
||||
filters?: Filter[];
|
||||
};
|
||||
|
||||
export type InputControlEmbeddable = IEmbeddable<InputControlInput, InputControlOutput>;
|
|
@ -12,6 +12,7 @@ import { PresentationCapabilitiesService } from './capabilities';
|
|||
import { PresentationDashboardsService } from './dashboards';
|
||||
import { PresentationLabsService } from './labs';
|
||||
import { registry as stubRegistry } from './stub';
|
||||
import { PresentationOverlaysService } from './overlays';
|
||||
|
||||
export { PresentationCapabilitiesService } from './capabilities';
|
||||
export { PresentationDashboardsService } from './dashboards';
|
||||
|
@ -19,6 +20,7 @@ export { PresentationLabsService } from './labs';
|
|||
export interface PresentationUtilServices {
|
||||
dashboards: PresentationDashboardsService;
|
||||
capabilities: PresentationCapabilitiesService;
|
||||
overlays: PresentationOverlaysService;
|
||||
labs: PresentationLabsService;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { capabilitiesServiceFactory } from './capabilities';
|
||||
import { dashboardsServiceFactory } from './dashboards';
|
||||
import { overlaysServiceFactory } from './overlays';
|
||||
import { labsServiceFactory } from './labs';
|
||||
import {
|
||||
PluginServiceProviders,
|
||||
|
@ -20,6 +21,7 @@ import { PresentationUtilServices } from '..';
|
|||
|
||||
export { capabilitiesServiceFactory } from './capabilities';
|
||||
export { dashboardsServiceFactory } from './dashboards';
|
||||
export { overlaysServiceFactory } from './overlays';
|
||||
export { labsServiceFactory } from './labs';
|
||||
|
||||
export const providers: PluginServiceProviders<
|
||||
|
@ -29,6 +31,7 @@ export const providers: PluginServiceProviders<
|
|||
capabilities: new PluginServiceProvider(capabilitiesServiceFactory),
|
||||
labs: new PluginServiceProvider(labsServiceFactory),
|
||||
dashboards: new PluginServiceProvider(dashboardsServiceFactory),
|
||||
overlays: new PluginServiceProvider(overlaysServiceFactory),
|
||||
};
|
||||
|
||||
export const registry = new PluginServiceRegistry<
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 { PresentationUtilPluginStartDeps } from '../../types';
|
||||
import { KibanaPluginServiceFactory } from '../create';
|
||||
import { PresentationOverlaysService } from '../overlays';
|
||||
|
||||
export type OverlaysServiceFactory = KibanaPluginServiceFactory<
|
||||
PresentationOverlaysService,
|
||||
PresentationUtilPluginStartDeps
|
||||
>;
|
||||
export const overlaysServiceFactory: OverlaysServiceFactory = ({ coreStart }) => {
|
||||
const {
|
||||
overlays: { openFlyout, openConfirm },
|
||||
} = coreStart;
|
||||
|
||||
return {
|
||||
openFlyout,
|
||||
openConfirm,
|
||||
};
|
||||
};
|
19
src/plugins/presentation_util/public/services/overlays.ts
Normal file
19
src/plugins/presentation_util/public/services/overlays.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 {
|
||||
MountPoint,
|
||||
OverlayFlyoutOpenOptions,
|
||||
OverlayModalConfirmOptions,
|
||||
OverlayRef,
|
||||
} from '../../../../core/public';
|
||||
|
||||
export interface PresentationOverlaysService {
|
||||
openFlyout(mount: MountPoint, options?: OverlayFlyoutOpenOptions): OverlayRef;
|
||||
openConfirm(message: MountPoint | string, options?: OverlayModalConfirmOptions): Promise<boolean>;
|
||||
}
|
|
@ -11,6 +11,7 @@ import { dashboardsServiceFactory } from '../stub/dashboards';
|
|||
import { labsServiceFactory } from './labs';
|
||||
import { capabilitiesServiceFactory } from './capabilities';
|
||||
import { PresentationUtilServices } from '..';
|
||||
import { overlaysServiceFactory } from './overlays';
|
||||
|
||||
export { PluginServiceProviders, PluginServiceProvider, PluginServiceRegistry } from '../create';
|
||||
export { PresentationUtilServices } from '..';
|
||||
|
@ -25,6 +26,7 @@ export interface StorybookParams {
|
|||
export const providers: PluginServiceProviders<PresentationUtilServices, StorybookParams> = {
|
||||
capabilities: new PluginServiceProvider(capabilitiesServiceFactory),
|
||||
dashboards: new PluginServiceProvider(dashboardsServiceFactory),
|
||||
overlays: new PluginServiceProvider(overlaysServiceFactory),
|
||||
labs: new PluginServiceProvider(labsServiceFactory),
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* 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 { EuiConfirmModal, EuiFlyout } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { Subject } from 'rxjs';
|
||||
import {
|
||||
MountPoint,
|
||||
OverlayFlyoutOpenOptions,
|
||||
OverlayModalConfirmOptions,
|
||||
OverlayRef,
|
||||
} from '../../../../../core/public';
|
||||
import { MountWrapper } from '../../../../../core/public/utils';
|
||||
import { PluginServiceFactory } from '../create';
|
||||
import { PresentationOverlaysService } from '../overlays';
|
||||
|
||||
type OverlaysServiceFactory = PluginServiceFactory<PresentationOverlaysService>;
|
||||
|
||||
/**
|
||||
* This code is a storybook stub version of src/core/public/overlays/overlay_service.ts
|
||||
* Eventually, core services should have simple storybook representations, but until that happens
|
||||
* it is necessary to recreate their functionality here.
|
||||
*/
|
||||
class GenericOverlayRef implements OverlayRef {
|
||||
public readonly onClose: Promise<void>;
|
||||
private closeSubject = new Subject<void>();
|
||||
|
||||
constructor() {
|
||||
this.onClose = this.closeSubject.toPromise();
|
||||
}
|
||||
|
||||
public close(): Promise<void> {
|
||||
if (!this.closeSubject.closed) {
|
||||
this.closeSubject.next();
|
||||
this.closeSubject.complete();
|
||||
}
|
||||
return this.onClose;
|
||||
}
|
||||
}
|
||||
|
||||
export const overlaysServiceFactory: OverlaysServiceFactory = () => {
|
||||
const flyoutDomElement = document.createElement('div');
|
||||
const modalDomElement = document.createElement('div');
|
||||
let activeFlyout: OverlayRef | null;
|
||||
let activeModal: OverlayRef | null;
|
||||
|
||||
const cleanupModal = () => {
|
||||
if (modalDomElement != null) {
|
||||
unmountComponentAtNode(modalDomElement);
|
||||
modalDomElement.innerHTML = '';
|
||||
}
|
||||
activeModal = null;
|
||||
};
|
||||
|
||||
const cleanupFlyout = () => {
|
||||
if (flyoutDomElement != null) {
|
||||
unmountComponentAtNode(flyoutDomElement);
|
||||
flyoutDomElement.innerHTML = '';
|
||||
}
|
||||
activeFlyout = null;
|
||||
};
|
||||
|
||||
return {
|
||||
openFlyout: (mount: MountPoint, options?: OverlayFlyoutOpenOptions) => {
|
||||
if (activeFlyout) {
|
||||
activeFlyout.close();
|
||||
cleanupFlyout();
|
||||
}
|
||||
|
||||
const flyout = new GenericOverlayRef();
|
||||
|
||||
flyout.onClose.then(() => {
|
||||
if (activeFlyout === flyout) {
|
||||
cleanupFlyout();
|
||||
}
|
||||
});
|
||||
|
||||
activeFlyout = flyout;
|
||||
|
||||
const onCloseFlyout = () => {
|
||||
if (options?.onClose) {
|
||||
options?.onClose(flyout);
|
||||
return;
|
||||
}
|
||||
flyout.close();
|
||||
};
|
||||
|
||||
render(
|
||||
<EuiFlyout onClose={onCloseFlyout}>
|
||||
<MountWrapper mount={mount} className="kbnOverlayMountWrapper" />
|
||||
</EuiFlyout>,
|
||||
flyoutDomElement
|
||||
);
|
||||
|
||||
return flyout;
|
||||
},
|
||||
openConfirm: (message: MountPoint | string, options?: OverlayModalConfirmOptions) => {
|
||||
if (activeModal) {
|
||||
activeModal.close();
|
||||
cleanupModal();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let resolved = false;
|
||||
const closeModal = (confirmed: boolean) => {
|
||||
resolved = true;
|
||||
modal.close();
|
||||
resolve(confirmed);
|
||||
};
|
||||
|
||||
const modal = new GenericOverlayRef();
|
||||
modal.onClose.then(() => {
|
||||
if (activeModal === modal) {
|
||||
cleanupModal();
|
||||
}
|
||||
// modal.close can be called when opening a new modal/confirm, so we need to resolve the promise in that case.
|
||||
if (!resolved) {
|
||||
closeModal(false);
|
||||
}
|
||||
});
|
||||
activeModal = modal;
|
||||
|
||||
const props = {
|
||||
...options,
|
||||
children:
|
||||
typeof message === 'string' ? (
|
||||
message
|
||||
) : (
|
||||
<MountWrapper mount={message} className="kbnOverlayMountWrapper" />
|
||||
),
|
||||
onCancel: () => closeModal(false),
|
||||
onConfirm: () => closeModal(true),
|
||||
cancelButtonText: options?.cancelButtonText || '', // stub default cancel text
|
||||
confirmButtonText: options?.confirmButtonText || '', // stub default confirm text
|
||||
};
|
||||
|
||||
render(<EuiConfirmModal {...props} />, modalDomElement);
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
|
@ -11,6 +11,7 @@ import { dashboardsServiceFactory } from './dashboards';
|
|||
import { labsServiceFactory } from './labs';
|
||||
import { PluginServiceProviders, PluginServiceProvider, PluginServiceRegistry } from '../create';
|
||||
import { PresentationUtilServices } from '..';
|
||||
import { overlaysServiceFactory } from './overlays';
|
||||
|
||||
export { dashboardsServiceFactory } from './dashboards';
|
||||
export { capabilitiesServiceFactory } from './capabilities';
|
||||
|
@ -18,6 +19,7 @@ export { capabilitiesServiceFactory } from './capabilities';
|
|||
export const providers: PluginServiceProviders<PresentationUtilServices> = {
|
||||
dashboards: new PluginServiceProvider(dashboardsServiceFactory),
|
||||
capabilities: new PluginServiceProvider(capabilitiesServiceFactory),
|
||||
overlays: new PluginServiceProvider(overlaysServiceFactory),
|
||||
labs: new PluginServiceProvider(labsServiceFactory),
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 {
|
||||
MountPoint,
|
||||
OverlayFlyoutOpenOptions,
|
||||
OverlayModalConfirmOptions,
|
||||
OverlayRef,
|
||||
} from '../../../../../core/public';
|
||||
import { PluginServiceFactory } from '../create';
|
||||
import { PresentationOverlaysService } from '../overlays';
|
||||
|
||||
type OverlaysServiceFactory = PluginServiceFactory<PresentationOverlaysService>;
|
||||
|
||||
class StubRef implements OverlayRef {
|
||||
public readonly onClose: Promise<void> = Promise.resolve();
|
||||
|
||||
public close(): Promise<void> {
|
||||
return this.onClose;
|
||||
}
|
||||
}
|
||||
|
||||
export const overlaysServiceFactory: OverlaysServiceFactory = () => ({
|
||||
openFlyout: (mount: MountPoint, options?: OverlayFlyoutOpenOptions) => new StubRef(),
|
||||
openConfirm: (message: MountPoint | string, options?: OverlayModalConfirmOptions) =>
|
||||
Promise.resolve(true),
|
||||
});
|
31
yarn.lock
31
yarn.lock
|
@ -2228,6 +2228,37 @@
|
|||
enabled "2.0.x"
|
||||
kuler "^2.0.0"
|
||||
|
||||
"@dnd-kit/accessibility@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.0.0.tgz#b56e3750414fd907b7d6972b3116aa8f96d07fde"
|
||||
integrity sha512-QwaQ1IJHQHMMuAGOOYHQSx7h7vMZPfO97aDts8t5N/MY7n2QTDSnW+kF7uRQ1tVBkr6vJ+BqHWG5dlgGvwVjow==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/core@^3.1.1":
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-3.1.1.tgz#c5ad6665931f5a51e74226220e58ac7514f3faf0"
|
||||
integrity sha512-18YY5+1lTqJbGSg6JBSa/fjAOTUYAysFrQ5Ti8oppEPHFacQbC+owM51y2z2KN0LkDHBfGZKw2sFT7++ttwfpA==
|
||||
dependencies:
|
||||
"@dnd-kit/accessibility" "^3.0.0"
|
||||
"@dnd-kit/utilities" "^2.0.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/sortable@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-4.0.0.tgz#81dd2b014a16527cf89602dc40060d9ee4dad352"
|
||||
integrity sha512-teYVFy6mQG/u6F6CaGxAkzPfiNJvguFzWfJ/zonYQRxfANHX6QJ6GziMG9KON/Ae9Q2ODJP8vib+guWJrDXeGg==
|
||||
dependencies:
|
||||
"@dnd-kit/utilities" "^2.0.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/utilities@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-2.0.0.tgz#a8462dff65c6f606ecbe95273c7e263b14a1ab97"
|
||||
integrity sha512-bjs49yMNzMM+BYRsBUhTqhTk6HEvhuY3leFt6Em6NaYGgygaMbtGbbXof/UXBv7rqyyi0OkmBBnrCCcxqS2t/g==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dsherret/to-absolute-glob@^2.0.2":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@dsherret/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1f6475dc8bd974cea07a2daf3864b317b1dd332c"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue