Skip to content

Commit

Permalink
Added editor menu to canvas
Browse files Browse the repository at this point in the history
Fixed props

Fixed ts error
  • Loading branch information
cqliu1 committed Oct 5, 2021
1 parent 67f2878 commit 2e5bf8a
Show file tree
Hide file tree
Showing 13 changed files with 430 additions and 19 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/canvas/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"features",
"inspector",
"presentationUtil",
"visualizations",
"uiActions",
"share"
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { FC } from 'react';
import {
EuiContextMenu,
EuiContextMenuPanelItemDescriptor,
EuiContextMenuItemIcon,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EmbeddableFactoryDefinition } from '../../../../../../../src/plugins/embeddable/public';
import {
BaseVisType,
VisGroups,
VisTypeAlias,
} from '../../../../../../../src/plugins/visualizations/public';
import { SolutionToolbarPopover } from '../../../../../../../src/plugins/presentation_util/public';

interface FactoryGroup {
id: string;
appName: string;
icon: EuiContextMenuItemIcon;
panelId: number;
factories: EmbeddableFactoryDefinition[];
}

interface Props {
factories: EmbeddableFactoryDefinition[];
isDarkThemeEnabled: boolean;
visTypeAliases: VisTypeAlias[];
createNewAggsBasedVis: (visType?: BaseVisType) => () => void;
createNewVisType: (visType?: BaseVisType | VisTypeAlias) => () => void;
createNewEmbeddable: (factory: EmbeddableFactoryDefinition) => () => void;
getVisTypesByGroup: (group: VisGroups) => BaseVisType[];
}

export const EditorMenu: FC<Props> = ({
factories,
isDarkThemeEnabled,
visTypeAliases,
createNewAggsBasedVis,
createNewVisType,
createNewEmbeddable,
getVisTypesByGroup,
}: Props) => {
const promotedVisTypes = getVisTypesByGroup(VisGroups.PROMOTED);
const aggsBasedVisTypes = getVisTypesByGroup(VisGroups.AGGBASED);
const toolVisTypes = getVisTypesByGroup(VisGroups.TOOLS);

const factoryGroupMap: Record<string, FactoryGroup> = {};
const ungroupedFactories: EmbeddableFactoryDefinition[] = [];
const aggBasedPanelID = 1;

let panelCount = 1 + aggBasedPanelID;

factories.forEach((factory: EmbeddableFactoryDefinition, index) => {
const { grouping } = factory;

if (grouping) {
grouping.forEach((group) => {
if (factoryGroupMap[group.id]) {
factoryGroupMap[group.id].factories.push(factory);
} else {
factoryGroupMap[group.id] = {
id: group.id,
appName: group.getDisplayName ? group.getDisplayName({}) : group.id,
icon: (group.getIconType ? group.getIconType({}) : 'empty') as EuiContextMenuItemIcon,
factories: [factory],
panelId: panelCount,
};

panelCount++;
}
});
} else {
ungroupedFactories.push(factory);
}
});

const getVisTypeMenuItem = (visType: BaseVisType): EuiContextMenuPanelItemDescriptor => {
const { name, title, titleInWizard, description, icon = 'empty', group } = visType;
return {
name: titleInWizard || title,
icon: icon as string,
onClick:
group === VisGroups.AGGBASED ? createNewAggsBasedVis(visType) : createNewVisType(visType),
'data-test-subj': `visType-${name}`,
toolTipContent: description,
};
};

const getVisTypeAliasMenuItem = (
visTypeAlias: VisTypeAlias
): EuiContextMenuPanelItemDescriptor => {
const { name, title, description, icon = 'empty' } = visTypeAlias;

return {
name: title,
icon,
onClick: createNewVisType(visTypeAlias),
'data-test-subj': `visType-${name}`,
toolTipContent: description,
};
};

const getEmbeddableFactoryMenuItem = (
factory: EmbeddableFactoryDefinition
): EuiContextMenuPanelItemDescriptor => {
const icon = factory?.getIconType ? factory.getIconType() : 'empty';

const toolTipContent = factory?.getDescription ? factory.getDescription() : undefined;

return {
name: factory.getDisplayName(),
icon,
toolTipContent,
onClick: createNewEmbeddable(factory),
'data-test-subj': `createNew-${factory.type}`,
};
};

const aggsPanelTitle = i18n.translate('dashboard.editorMenu.aggBasedGroupTitle', {
defaultMessage: 'Aggregation based',
});

const editorMenuPanels = [
{
id: 0,
items: [
...visTypeAliases.map(getVisTypeAliasMenuItem),
...Object.values(factoryGroupMap).map(({ id, appName, icon, panelId }) => ({
name: appName,
icon,
panel: panelId,
'data-test-subj': `dashboardEditorMenu-${id}Group`,
})),
...ungroupedFactories.map(getEmbeddableFactoryMenuItem),
...promotedVisTypes.map(getVisTypeMenuItem),
{
name: aggsPanelTitle,
icon: 'visualizeApp',
panel: aggBasedPanelID,
'data-test-subj': `dashboardEditorAggBasedMenuItem`,
},
...toolVisTypes.map(getVisTypeMenuItem),
],
},
{
id: aggBasedPanelID,
title: aggsPanelTitle,
items: aggsBasedVisTypes.map(getVisTypeMenuItem),
},
...Object.values(factoryGroupMap).map(
({ appName, panelId, factories: groupFactories }: FactoryGroup) => ({
id: panelId,
title: appName,
items: groupFactories.map(getEmbeddableFactoryMenuItem),
})
),
];

return (
<SolutionToolbarPopover
ownFocus
label={i18n.translate('dashboard.solutionToolbar.editorMenuButtonLabel', {
defaultMessage: 'All types',
})}
iconType="arrowDown"
iconSide="right"
panelPaddingSize="none"
data-test-subj="dashboardEditorMenuButton"
>
{() => (
<EuiContextMenu
initialPanelId={0}
panels={editorMenuPanels}
className={`dshSolutionToolbar__editorContextMenu ${
isDarkThemeEnabled
? 'dshSolutionToolbar__editorContextMenu--dark'
: 'dshSolutionToolbar__editorContextMenu--light'
}`}
data-test-subj="dashboardEditorContextMenu"
/>
)}
</SolutionToolbarPopover>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { FC, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { trackCanvasUiMetric, METRIC_TYPE } from '../../../../public/lib/ui_metric';
import {
useEmbeddablesService,
useNotifyService,
usePlatformService,
useVisualizationsService,
} from '../../../services';
import {
BaseVisType,
VisGroups,
VisTypeAlias,
} from '../../../../../../../src/plugins/visualizations/public';
import {
EmbeddableFactoryDefinition,
EmbeddableInput,
} from '../../../../../../../src/plugins/embeddable/public';
import { CANVAS_APP } from '../../../../common/lib';
import { encode } from '../../../../common/lib/embeddable_dataurl';
import { ElementSpec } from '../../../../types';
import { EditorMenu as Component } from './editor_menu.component';

interface Props {
/**
* Handler for adding a selected element to the workpad
*/
addElement: (element: Partial<ElementSpec>) => void;
}

export const EditorMenu: FC<Props> = ({ addElement }) => {
const embeddablesService = useEmbeddablesService();
const { pathname, search } = useLocation();
const notifyService = useNotifyService();
const platformService = usePlatformService();
const stateTransferService = embeddablesService.getStateTransfer();
const visualizationsService = useVisualizationsService();
const IS_DARK_THEME = platformService.getUISetting('theme:darkMode');

const createNewVisType = useCallback(
(visType?: BaseVisType | VisTypeAlias) => () => {
let path = '';
let appId = '';

if (visType) {
if (trackCanvasUiMetric) {
trackCanvasUiMetric(METRIC_TYPE.CLICK, `${visType.name}:create`);
}

if ('aliasPath' in visType) {
appId = visType.aliasApp;
path = visType.aliasPath;
} else {
appId = 'visualize';
path = `#/create?type=${encodeURIComponent(visType.name)}`;
}
} else {
appId = 'visualize';
path = '#/create?';
}

stateTransferService.navigateToEditor(appId, {
path,
state: {
originatingApp: CANVAS_APP,
originatingPath: `#/${pathname}${search}`,
},
});
},
[stateTransferService, pathname, search]
);

const createNewEmbeddable = useCallback(
(factory: EmbeddableFactoryDefinition) => async () => {
if (trackCanvasUiMetric) {
trackCanvasUiMetric(METRIC_TYPE.CLICK, factory.type);
}
let embeddableInput;
if (factory.getExplicitInput) {
embeddableInput = await factory.getExplicitInput();
} else {
const newEmbeddable = await factory.create({} as EmbeddableInput);
embeddableInput = newEmbeddable?.getInput();
}

if (embeddableInput) {
const config = encode(embeddableInput);
const expression = `embeddable config="${config}"
type="${factory.type}"
| render`;

addElement({ expression });

notifyService.success(`Successfully added '${embeddableInput.title}'`, {
'data-test-subj': 'addEmbeddableToWorkpadSuccess',
});
}
},
[addElement, notifyService]
);

const createNewAggsBasedVis = useCallback(
(visType?: BaseVisType) => () =>
visualizationsService.showNewVisModal({
originatingApp: CANVAS_APP,
outsideVisualizeApp: true,
showAggsSelection: true,
selectedVisType: visType,
}),
[visualizationsService]
);

const getVisTypesByGroup = (group: VisGroups): BaseVisType[] =>
visualizationsService
.getByGroup(group)
.sort(({ name: a }: BaseVisType | VisTypeAlias, { name: b }: BaseVisType | VisTypeAlias) => {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
})
.filter(({ hidden }: BaseVisType) => !hidden);

const visTypeAliases = visualizationsService
.getAliases()
.sort(({ promotion: a = false }: VisTypeAlias, { promotion: b = false }: VisTypeAlias) =>
a === b ? 0 : a ? -1 : 1
);

const factories = embeddablesService
? Array.from(embeddablesService.getEmbeddableFactories()).filter(
({ type, isEditable, canCreateNew, isContainerType }) =>
isEditable() && !isContainerType && canCreateNew() && type !== 'visualization'
)
: [];

return (
<Component
createNewVisType={createNewVisType}
createNewAggsBasedVis={createNewAggsBasedVis}
createNewEmbeddable={createNewEmbeddable}
getVisTypesByGroup={getVisTypesByGroup}
isDarkThemeEnabled={IS_DARK_THEME}
factories={factories}
visTypeAliases={visTypeAliases}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export { EditorMenu } from './editor_menu';
export { EditorMenu as EditorMenuComponent } from './editor_menu.component';
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,6 @@ You can use standard Markdown in here, but you can also access your piped-in dat
},
};

const mockRenderEmbedPanel = () => <div id="embeddablePanel" />;

storiesOf('components/WorkpadHeader/ElementMenu', module).add('default', () => (
<ElementMenu elements={testElements} addElement={action('addElement')} />
));
Loading

0 comments on commit 2e5bf8a

Please sign in to comment.