From 53ad55cf6f14c77b2def8c18a9c563d72f666c71 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Thu, 2 May 2019 16:38:46 -0500 Subject: [PATCH] [Canvas]Feat: Custom Elements (#34140) * Added custom element routes Added context menu to sidebar header Added custom element modal Added tabs to element_types Added handlers for retrieving custom elements sidebar header tweaks Added edit and delete element controls Added a selector for transient.selectedToplevelNodes Refactored element event handlers Added additional sidebar views Fixed adding custom element to workpad Converted element_controls to tsx Disabled group/ungroup buttons sidebar Disabled layer controls in multi_element_settings Cleaned up props for element_types Added story for element_controls Added stories for global_config, group_settings, and multi_element_config TSified element_handler_creators fixed ts errors more tsifying Added decorator to element_controls Updated stories for custom_element_modal Disabled global_config, group_settings, and multi_element_settings stories TSified sidebar disable layer controls in group_settings Removed save element shortcut added public/private keywords Converted sidebar_header to ts and added stories Refactored sidebar_header fix file extension design cleanup fix image in edit modal Fixed ts errors Update x-pack/plugins/canvas/server/routes/custom_elements.js Co-Authored-By: cqliu1 Update x-pack/plugins/canvas/server/routes/workpad.js Co-Authored-By: cqliu1 Reordered args for insertNodes and removeNodes to match corresponding redux actions Extracted PositionedElement interface Fixed TS issues Adjust title and desc lengths Added comments to props interface Refactored onClick handlers more ts Added types for ast Switched common/lib/constants back to JS Added comments Updated more comments Removed unused import Added snapshots Typed custom_element_service Fixed ts errors * Added comments to @ts-ignore's * Removed custom_element_modal stories * Added a few more comments * Update security tests with new canvas-element saved object * Updated privileges test * Updated ui_capabilities security_only saved_objects_management test * Added state interface for CustomElementTypes * Added state interfaces * fixed comment * Removed unnecessary exports --- x-pack/plugins/canvas/common/lib/constants.js | 2 + x-pack/plugins/canvas/common/lib/dataurl.ts | 4 +- x-pack/plugins/canvas/init.js | 4 +- .../element_controls.examples.storyshot | 98 +++++ .../element_controls.examples.tsx | 24 ++ .../element_types/element_controls.tsx | 49 +++ .../components/element_types/element_types.js | 254 ++++++++--- .../element_types/element_types.scss | 56 ++- .../public/components/element_types/index.js | 93 +++- .../element_settings/element_settings.tsx | 61 +++ .../sidebar/element_settings/index.tsx | 30 ++ .../{global_config.js => global_config.tsx} | 12 +- .../components/sidebar/group_settings.tsx | 21 + .../canvas/public/components/sidebar/index.js | 55 --- .../public/components/sidebar/index.tsx | 7 + .../sidebar/multi_element_settings.tsx | 24 ++ .../public/components/sidebar/sidebar.js | 13 - .../public/components/sidebar/sidebar.tsx | 15 + .../components/sidebar/sidebar_component.js | 133 ------ .../components/sidebar/sidebar_content.js | 42 ++ .../sidebar/sidebar_section_title.js | 9 +- .../sidebar_header.examples.storyshot | 398 ++++++++++++++++++ .../__examples__/sidebar_header.examples.tsx | 29 ++ .../sidebar_header/custom_element_modal.tsx | 217 ++++++++++ .../public/components/sidebar_header/index.js | 66 +++ .../sidebar_header/sidebar_header.scss | 24 ++ .../sidebar_header/sidebar_header.tsx | 377 +++++++++++++++++ .../public/components/workpad_header/index.js | 8 +- .../workpad_header/workpad_header.js | 80 ++-- .../components/workpad_page/prop_types.js | 1 + .../workpad_interactive_page/index.js | 14 +- .../interactive_workpad_page.js | 8 +- .../workpad_shortcuts.tsx | 118 ------ .../components/workpad_shortcuts/index.tsx | 33 ++ .../workpad_shortcuts/workpad_shortcuts.tsx | 95 +++++ .../public/lib/custom_element_service.ts | 69 +++ .../plugins/canvas/public/lib/elastic_logo.js | 2 - .../plugins/canvas/public/lib/elastic_logo.ts | 9 + .../public/lib/element_handler_creators.ts | 156 +++++++ .../public/lib/{get_id.js => get_id.ts} | 2 +- .../{is_text_input.js => is_text_input.ts} | 2 +- x-pack/plugins/canvas/public/lib/keymap.js | 16 +- .../canvas/public/lib/positioned_element.ts | 73 ++++ .../canvas/public/state/selectors/workpad.js | 8 +- x-pack/plugins/canvas/public/style/index.scss | 1 + x-pack/plugins/canvas/public/style/main.scss | 5 + .../canvas/server/lib/format_response.js | 30 ++ .../server/{mappings.js => mappings.ts} | 21 +- .../canvas/server/routes/custom_elements.js | 152 +++++++ x-pack/plugins/canvas/server/routes/index.js | 4 +- .../plugins/canvas/server/routes/workpad.js | 27 +- .../apis/security/privileges.ts | 42 ++ .../saved_objects_management_builder.ts | 1 + .../tests/saved_objects_management.ts | 2 + .../tests/saved_objects_management.ts | 2 + 55 files changed, 2581 insertions(+), 517 deletions(-) create mode 100644 x-pack/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_controls.examples.storyshot create mode 100644 x-pack/plugins/canvas/public/components/element_types/__examples__/element_controls.examples.tsx create mode 100644 x-pack/plugins/canvas/public/components/element_types/element_controls.tsx create mode 100644 x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx create mode 100644 x-pack/plugins/canvas/public/components/sidebar/element_settings/index.tsx rename x-pack/plugins/canvas/public/components/sidebar/{global_config.js => global_config.tsx} (69%) create mode 100644 x-pack/plugins/canvas/public/components/sidebar/group_settings.tsx delete mode 100644 x-pack/plugins/canvas/public/components/sidebar/index.js create mode 100644 x-pack/plugins/canvas/public/components/sidebar/index.tsx create mode 100644 x-pack/plugins/canvas/public/components/sidebar/multi_element_settings.tsx delete mode 100644 x-pack/plugins/canvas/public/components/sidebar/sidebar.js create mode 100644 x-pack/plugins/canvas/public/components/sidebar/sidebar.tsx delete mode 100644 x-pack/plugins/canvas/public/components/sidebar/sidebar_component.js create mode 100644 x-pack/plugins/canvas/public/components/sidebar/sidebar_content.js create mode 100644 x-pack/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.examples.storyshot create mode 100644 x-pack/plugins/canvas/public/components/sidebar_header/__examples__/sidebar_header.examples.tsx create mode 100644 x-pack/plugins/canvas/public/components/sidebar_header/custom_element_modal.tsx create mode 100644 x-pack/plugins/canvas/public/components/sidebar_header/index.js create mode 100644 x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.scss create mode 100644 x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx delete mode 100644 x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/workpad_shortcuts.tsx create mode 100644 x-pack/plugins/canvas/public/components/workpad_shortcuts/index.tsx create mode 100644 x-pack/plugins/canvas/public/components/workpad_shortcuts/workpad_shortcuts.tsx create mode 100644 x-pack/plugins/canvas/public/lib/custom_element_service.ts delete mode 100644 x-pack/plugins/canvas/public/lib/elastic_logo.js create mode 100644 x-pack/plugins/canvas/public/lib/elastic_logo.ts create mode 100644 x-pack/plugins/canvas/public/lib/element_handler_creators.ts rename x-pack/plugins/canvas/public/lib/{get_id.js => get_id.ts} (86%) rename x-pack/plugins/canvas/public/lib/{is_text_input.js => is_text_input.ts} (88%) create mode 100644 x-pack/plugins/canvas/public/lib/positioned_element.ts create mode 100644 x-pack/plugins/canvas/server/lib/format_response.js rename x-pack/plugins/canvas/server/{mappings.js => mappings.ts} (51%) create mode 100644 x-pack/plugins/canvas/server/routes/custom_elements.js diff --git a/x-pack/plugins/canvas/common/lib/constants.js b/x-pack/plugins/canvas/common/lib/constants.js index d996bc5e8c4b..8e34031ddb55 100644 --- a/x-pack/plugins/canvas/common/lib/constants.js +++ b/x-pack/plugins/canvas/common/lib/constants.js @@ -5,6 +5,7 @@ */ export const CANVAS_TYPE = 'canvas-workpad'; +export const CUSTOM_ELEMENT_TYPE = 'canvas-element'; export const CANVAS_APP = 'canvas'; export const APP_ROUTE = '/app/canvas'; export const APP_ROUTE_WORKPAD = `${APP_ROUTE}#/workpad`; @@ -12,6 +13,7 @@ export const API_ROUTE = '/api/canvas'; export const API_ROUTE_WORKPAD = `${API_ROUTE}/workpad`; export const API_ROUTE_WORKPAD_ASSETS = `${API_ROUTE}/workpad-assets`; export const API_ROUTE_WORKPAD_STRUCTURES = `${API_ROUTE}/workpad-structures`; +export const API_ROUTE_CUSTOM_ELEMENT = `${API_ROUTE}/custom-element`; export const LOCALSTORAGE_PREFIX = `kibana.canvas`; export const LOCALSTORAGE_CLIPBOARD = `${LOCALSTORAGE_PREFIX}.clipboard`; export const LOCALSTORAGE_AUTOCOMPLETE_ENABLED = `${LOCALSTORAGE_PREFIX}.isAutocompleteEnabled`; diff --git a/x-pack/plugins/canvas/common/lib/dataurl.ts b/x-pack/plugins/canvas/common/lib/dataurl.ts index 85fcdc21e645..4542a6a9fccd 100644 --- a/x-pack/plugins/canvas/common/lib/dataurl.ts +++ b/x-pack/plugins/canvas/common/lib/dataurl.ts @@ -49,9 +49,9 @@ export function isValidDataUrl(str: string) { export function encode(data: any | null, type = 'text/plain') { // use FileReader if it's available, like in the browser if (FileReader) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const reader = new FileReader(); - reader.onloadend = () => resolve(reader.result); + reader.onloadend = () => resolve(reader.result as string); reader.onerror = err => reject(err); reader.readAsDataURL(data); }); diff --git a/x-pack/plugins/canvas/init.js b/x-pack/plugins/canvas/init.js index 0c5d56134f57..f993c045e829 100644 --- a/x-pack/plugins/canvas/init.js +++ b/x-pack/plugins/canvas/init.js @@ -45,7 +45,7 @@ export default async function(server /*options*/) { privileges: { all: { savedObject: { - all: ['canvas-workpad'], + all: ['canvas-workpad', 'canvas-element'], read: ['index-pattern'], }, ui: ['save'], @@ -53,7 +53,7 @@ export default async function(server /*options*/) { read: { savedObject: { all: [], - read: ['index-pattern', 'canvas-workpad'], + read: ['index-pattern', 'canvas-workpad', 'canvas-element'], }, ui: [], }, diff --git a/x-pack/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_controls.examples.storyshot b/x-pack/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_controls.examples.storyshot new file mode 100644 index 000000000000..5e076ba76a9c --- /dev/null +++ b/x-pack/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_controls.examples.storyshot @@ -0,0 +1,98 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/ElementTypes/ElementControls has two buttons 1`] = ` +
+
+
+ + + +
+
+ + + +
+
+
+`; diff --git a/x-pack/plugins/canvas/public/components/element_types/__examples__/element_controls.examples.tsx b/x-pack/plugins/canvas/public/components/element_types/__examples__/element_controls.examples.tsx new file mode 100644 index 000000000000..daf34479bf24 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/element_types/__examples__/element_controls.examples.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { ElementControls } from '../element_controls'; + +storiesOf('components/ElementTypes/ElementControls', module) + .addDecorator(story => ( +
+ {story()} +
+ )) + .add('has two buttons', () => ( + + )); diff --git a/x-pack/plugins/canvas/public/components/element_types/element_controls.tsx b/x-pack/plugins/canvas/public/components/element_types/element_controls.tsx new file mode 100644 index 000000000000..1e95fa677f5c --- /dev/null +++ b/x-pack/plugins/canvas/public/components/element_types/element_controls.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent, MouseEvent } from 'react'; +import PropTypes from 'prop-types'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; + +interface Props { + /** + * A click handler for the delete button + */ + onDelete: (event: MouseEvent) => void; + /** + * A click handler for the edit button + */ + onEdit: (event: MouseEvent) => void; +} + +export const ElementControls: FunctionComponent = ({ onDelete, onEdit }) => ( + + + + + + + + + + + + +); + +ElementControls.propTypes = { + onDelete: PropTypes.func.isRequired, + onEdit: PropTypes.func.isRequired, +}; diff --git a/x-pack/plugins/canvas/public/components/element_types/element_types.js b/x-pack/plugins/canvas/public/components/element_types/element_types.js index ced13040a2d5..6bd7e034efe9 100644 --- a/x-pack/plugins/canvas/public/components/element_types/element_types.js +++ b/x-pack/plugins/canvas/public/components/element_types/element_types.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; +import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { EuiFieldSearch, @@ -14,72 +14,206 @@ import { EuiFlexItem, EuiModalHeader, EuiModalBody, + EuiTabbedContent, + EuiEmptyPrompt, + EuiSpacer, + EuiIcon, } from '@elastic/eui'; import lowerCase from 'lodash.lowercase'; import { map, includes, sortBy } from 'lodash'; +import { ConfirmModal } from '../confirm_modal/confirm_modal'; +import { CustomElementModal } from '../sidebar_header/custom_element_modal'; +import { ElementControls } from './element_controls'; -export const ElementTypes = ({ elements, onClick, search, setSearch }) => { - search = lowerCase(search); - elements = sortBy(map(elements, (element, name) => ({ name, ...element })), 'displayName'); - const elementList = map(elements, (element, name) => { - const { help, displayName, expression, filter, width, height, image } = element; - const whenClicked = () => onClick({ expression, filter, width, height }); +export class ElementTypes extends Component { + static propTypes = { + addCustomElement: PropTypes.func.isRequired, + addElement: PropTypes.func.isRequired, + customElements: PropTypes.array.isRequired, + elements: PropTypes.object, + findCustomElements: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + removeCustomElement: PropTypes.func.isRequired, + search: PropTypes.string, + setCustomElements: PropTypes.func.isRequired, + setSearch: PropTypes.func.isRequired, + updateCustomElement: PropTypes.func.isRequired, + }; - // Add back in icon={image} to this when Design has a full icon set - const card = ( - - - + state = { + isEditModalVisible: false, + isDeleteModalVisible: false, + elementToDelete: null, + elementToEdit: null, + }; + + componentDidMount() { + // fetch custom elements + this.props.findCustomElements(); + } + + _showEditModal = elementToEdit => this.setState({ isEditModalVisible: true, elementToEdit }); + + _hideEditModal = () => this.setState({ isEditModalVisible: false, elementToEdit: null }); + + _showDeleteModal = elementToDelete => + this.setState({ isDeleteModalVisible: true, elementToDelete }); + + _hideDeleteModal = () => this.setState({ isDeleteModalVisible: false, elementToDelete: null }); + + _getElementCards = (elements, handleClick, showControls = false) => { + const { search, onClose } = this.props; + return map(elements, (element, name) => { + const { help, displayName, image } = element; + const whenClicked = () => { + handleClick(element); + onClose(); + }; + + const card = ( + + } + title={displayName} + description={help} + onClick={whenClicked} + className={image ? 'canvasCard' : 'canvasCard canvasCard--hasIcon'} + /> + {showControls && ( + this._showEditModal(element)} + onDelete={() => this._showDeleteModal(element)} + /> + )} + + ); + + if (!search) { + return card; + } + if (includes(lowerCase(name), search)) { + return card; + } + if (includes(lowerCase(displayName), search)) { + return card; + } + if (includes(lowerCase(help), search)) { + return card; + } + return null; + }); + }; + + _renderEditModal = () => { + const { updateCustomElement } = this.props; + const { elementToEdit } = this.state; + return ( + + ); + }; + + _renderDeleteModal = () => { + const { removeCustomElement } = this.props; + const { elementToDelete } = this.state; + return ( + { + removeCustomElement(elementToDelete.id); + this._hideDeleteModal(); + }} + onCancel={this._hideDeleteModal} + /> + ); + }; + + render() { + const { setSearch, addElement, addCustomElement } = this.props; + let { elements, search, customElements } = this.props; + const { isEditModalVisible, isDeleteModalVisible, elementToEdit, elementToDelete } = this.state; + search = lowerCase(search); + elements = sortBy(map(elements, (element, name) => ({ name, ...element })), 'displayName'); + const elementList = this._getElementCards(elements, addElement); + + let customElementContent = ( + // TODO: update copy + Add new elements} + body={

Group and save workpad elements to create new elements

} + titleSize="s" + /> ); - if (!search) { - return card; + if (customElements.length) { + customElements = sortBy( + map(customElements, (element, name) => ({ name, ...element })), + 'name' + ); + customElementContent = this._getElementCards(customElements, addCustomElement, true); } - if (includes(lowerCase(name), search)) { - return card; - } - if (includes(lowerCase(displayName), search)) { - return card; - } - if (includes(lowerCase(help), search)) { - return card; - } - return null; - }); - return ( - - - - - setSearch(e.target.value)} - value={search} - /> - - - - - - {elementList} - - - - ); -}; + const tabs = [ + { + id: 'elements', + name: 'Elements', + content: ( + + + + {elementList} + + + ), + }, + { + id: 'customElements', + name: 'My elements', + content: ( + + + + {customElementContent} + + + ), + }, + ]; -ElementTypes.propTypes = { - elements: PropTypes.object, - onClick: PropTypes.func, - search: PropTypes.string, - setSearch: PropTypes.func, -}; + return ( + + + + + setSearch(e.target.value)} + value={search} + /> + + + + + + + + {isEditModalVisible && elementToEdit && this._renderEditModal()} + + {isDeleteModalVisible && elementToDelete && this._renderDeleteModal()} + + ); + } +} diff --git a/x-pack/plugins/canvas/public/components/element_types/element_types.scss b/x-pack/plugins/canvas/public/components/element_types/element_types.scss index bb58a60515c7..cc3cdec747e3 100644 --- a/x-pack/plugins/canvas/public/components/element_types/element_types.scss +++ b/x-pack/plugins/canvas/public/components/element_types/element_types.scss @@ -1,17 +1,45 @@ -.canvasCard { +.canvasElementCard { + position: relative; - .euiCard__top { - text-align: center; - width: calc(100% + #{$euiSize}*2); - height: 85px; - margin: calc(-1 * #{$euiSize}) calc(-1 * #{$euiSize}) 0; + .canvasCard { + .euiCard__top { + text-align: center; + width: calc(100% + #{$euiSize}* 2); + height: 85px; + margin: calc(-1 * #{$euiSize}) calc(-1 * #{$euiSize}) 0; + } + + .euiCard__image { + max-height: 100%; + max-width: 100%; + width: auto; + left: 0; + top: 0; + } + + &.canvasCard--hasIcon .euiCard__top { + min-width: $canvasElementCardWidth; + padding-top: $euiSize; + } } - - .euiCard__image { - max-height: 100%; - max-width: 100%; - width: auto; - left: 0; - top: 0; + + &:hover, + &:focus { + .canvasElementCard__controls { + visibility: visible; + opacity: 1; + } } -} \ No newline at end of file + + .canvasElementCard__controls { + position: absolute; + right: $euiSizeS; + top: $euiSizeS; + visibility: hidden; + opacity: 0; + transition: opacity $euiAnimSpeedFast $euiAnimSlightResistance; + transition-delay: $euiAnimSpeedNormal; + background: transparentize($euiColorGhost, 0.5); + border-radius: $euiBorderRadius; + } +} diff --git a/x-pack/plugins/canvas/public/components/element_types/index.js b/x-pack/plugins/canvas/public/components/element_types/index.js index 19ad1358b2e0..35a85516df27 100644 --- a/x-pack/plugins/canvas/public/components/element_types/index.js +++ b/x-pack/plugins/canvas/public/components/element_types/index.js @@ -4,16 +4,99 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pure, compose, withProps, withState } from 'recompose'; +import PropTypes from 'prop-types'; +import { compose, withProps, withState } from 'recompose'; +import { connect } from 'react-redux'; +import { camelCase } from 'lodash'; +import { cloneSubgraphs } from '../../lib/clone_subgraphs'; +import * as customElementService from '../../lib/custom_element_service'; import { elementsRegistry } from '../../lib/elements_registry'; - +import { notify } from '../../lib/notify'; +import { selectToplevelNodes } from '../../state/actions/transient'; +import { insertNodes, addElement } from '../../state/actions/elements'; +import { getSelectedPage } from '../../state/selectors/workpad'; import { ElementTypes as Component } from './element_types'; -const elementTypesState = withState('search', 'setSearch'); +const elementTypesState = withState('search', 'setSearch', ''); +const customElementsState = withState('customElements', 'setCustomElements', []); const elementTypeProps = withProps(() => ({ elements: elementsRegistry.toJS() })); +const mapStateToProps = state => ({ pageId: getSelectedPage(state) }); + +const mapDispatchToProps = dispatch => ({ + selectToplevelNodes: nodes => + dispatch(selectToplevelNodes(nodes.filter(e => !e.position.parent).map(e => e.id))), + insertNodes: (selectedNodes, pageId) => dispatch(insertNodes(selectedNodes, pageId)), + addElement: pageId => partialElement => dispatch(addElement(pageId, partialElement)), +}); + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { pageId, ...remainingStateProps } = stateProps; + const { addElement, insertNodes, selectToplevelNodes } = dispatchProps; + const { search, setCustomElements } = ownProps; + + return { + ...remainingStateProps, + ...ownProps, + // add built-in element to the page + addElement: addElement(pageId), + // add custom element to the page + addCustomElement: customElement => { + const { selectedNodes = [] } = JSON.parse(customElement.content) || {}; + const clonedNodes = selectedNodes && cloneSubgraphs(selectedNodes); + if (clonedNodes) { + insertNodes(clonedNodes, pageId); // first clone and persist the new node(s) + selectToplevelNodes(clonedNodes); // then select the cloned node(s) + } + }, + // custom element search + findCustomElements: async text => { + try { + const { customElements } = await customElementService.find(text); + setCustomElements(customElements); + } catch (err) { + notify.error(err, { title: `Couldn't find custom elements` }); + } + }, + // remove custom element + removeCustomElement: async id => { + try { + await customElementService.remove(id); + const { customElements } = await customElementService.find(search); + setCustomElements(customElements); + } catch (err) { + notify.error(err, { title: `Couldn't delete custom elements` }); + } + }, + // update custom element + updateCustomElement: id => async (name, description, image) => { + try { + await customElementService.update(id, { + name: camelCase(name), + displayName: name, + image, + help: description, + }); + const { customElements } = await customElementService.find(search); + setCustomElements(customElements); + } catch (err) { + notify.error(err, { title: `Couldn't update custom elements` }); + } + }, + }; +}; + export const ElementTypes = compose( - pure, elementTypesState, - elementTypeProps + elementTypeProps, + customElementsState, + connect( + mapStateToProps, + mapDispatchToProps, + mergeProps + ) )(Component); + +ElementTypes.propTypes = { + onClose: PropTypes.func, +}; diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx new file mode 100644 index 000000000000..141dc33b9609 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FunctionComponent } from 'react'; +import PropTypes from 'prop-types'; +import { EuiSpacer, EuiTabbedContent } from '@elastic/eui'; +// @ts-ignore unconverted component +import { Datasource } from '../../datasource'; +// @ts-ignore unconverted component +import { FunctionFormList } from '../../function_form_list'; +// @ts-ignore unconverted component +import { SidebarHeader } from '../../sidebar_header'; +import { PositionedElement } from '../../../lib/positioned_element'; + +interface Props { + /** + * a Canvas element used to populate config forms + */ + element: PositionedElement; +} + +export const ElementSettings: FunctionComponent = ({ element }) => { + const tabs = [ + { + id: 'edit', + name: 'Display', + content: ( +
+ +
+ +
+
+ ), + }, + { + id: 'data', + name: 'Data', + content: ( +
+ + +
+ ), + }, + ]; + + return ( + + + + + ); +}; + +ElementSettings.propTypes = { + element: PropTypes.object, +}; diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.tsx new file mode 100644 index 000000000000..0a3439a7cab1 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar/element_settings/index.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +// @ts-ignore unconverted local file +import { getElementById, getSelectedPage } from '../../../state/selectors/workpad'; +import { ElementSettings as Component } from './element_settings'; +import { PositionedElement } from '../../../lib/positioned_element'; + +interface State { + persistent: { workpad: { pages: Array<{ elements: PositionedElement[] }> } }; +} + +interface Props { + selectedElementId: string; +} + +const mapStateToProps = (state: State, { selectedElementId }: Props) => ({ + element: getElementById(state, selectedElementId, getSelectedPage(state)), +}); + +export const ElementSettings = connect(mapStateToProps)(Component); + +ElementSettings.propTypes = { + selectedElementId: PropTypes.string.isRequired, +}; diff --git a/x-pack/plugins/canvas/public/components/sidebar/global_config.js b/x-pack/plugins/canvas/public/components/sidebar/global_config.tsx similarity index 69% rename from x-pack/plugins/canvas/public/components/sidebar/global_config.js rename to x-pack/plugins/canvas/public/components/sidebar/global_config.tsx index 32536c3b7978..a5920ee19746 100644 --- a/x-pack/plugins/canvas/public/components/sidebar/global_config.js +++ b/x-pack/plugins/canvas/public/components/sidebar/global_config.tsx @@ -4,14 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Fragment, FunctionComponent } from 'react'; +// @ts-ignore unconverted component import { ElementConfig } from '../element_config'; +// @ts-ignore unconverted component import { PageConfig } from '../page_config'; +// @ts-ignore unconverted component import { WorkpadConfig } from '../workpad_config'; +// @ts-ignore unconverted component import { SidebarSection } from './sidebar_section'; -export const GlobalConfig = () => ( -
+export const GlobalConfig: FunctionComponent = () => ( + @@ -21,5 +25,5 @@ export const GlobalConfig = () => ( -
+ ); diff --git a/x-pack/plugins/canvas/public/components/sidebar/group_settings.tsx b/x-pack/plugins/canvas/public/components/sidebar/group_settings.tsx new file mode 100644 index 000000000000..632fc7ccb41b --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar/group_settings.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FunctionComponent } from 'react'; +import { EuiText, EuiSpacer } from '@elastic/eui'; +// @ts-ignore unconverted component +import { SidebarHeader } from '../sidebar_header/'; + +export const GroupSettings: FunctionComponent = () => ( + + + + +

Ungroup (U) to edit individual element settings.

+

Save this group as a new element to re-use it throughout your workpad.

+
+
+); diff --git a/x-pack/plugins/canvas/public/components/sidebar/index.js b/x-pack/plugins/canvas/public/components/sidebar/index.js deleted file mode 100644 index 65204b62c379..000000000000 --- a/x-pack/plugins/canvas/public/components/sidebar/index.js +++ /dev/null @@ -1,55 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; -import { cloneSubgraphs } from '../../lib/clone_subgraphs'; -import { insertNodes, elementLayer } from '../../state/actions/elements'; -import { getSelectedPage, getSelectedElement } from '../../state/selectors/workpad'; -import { selectToplevelNodes } from '../../state/actions/transient'; - -import { Sidebar as Component } from './sidebar'; - -const mapStateToProps = state => ({ - selectedPage: getSelectedPage(state), - selectedElement: getSelectedElement(state), -}); - -const mapDispatchToProps = dispatch => ({ - duplicateElement: (pageId, selectedElement) => () => { - // gradually unifying code with copy/paste - // todo: more unification w/ copy/paste; group cloning - const newElements = cloneSubgraphs([selectedElement]); - dispatch(insertNodes(newElements, pageId)); - dispatch(selectToplevelNodes(newElements.map(e => e.id))); - }, - elementLayer: (pageId, selectedElement) => movement => - dispatch( - elementLayer({ - pageId, - elementId: selectedElement.id, - movement, - }) - ), -}); - -const mergeProps = (stateProps, dispatchProps, ownProps) => { - const { selectedElement, selectedPage } = stateProps; - - return { - ...stateProps, - ...dispatchProps, - ...ownProps, - elementIsSelected: Boolean(selectedElement), - duplicateElement: dispatchProps.duplicateElement(selectedPage, selectedElement), - elementLayer: dispatchProps.elementLayer(selectedPage, selectedElement), - }; -}; - -export const Sidebar = connect( - mapStateToProps, - mapDispatchToProps, - mergeProps -)(Component); diff --git a/x-pack/plugins/canvas/public/components/sidebar/index.tsx b/x-pack/plugins/canvas/public/components/sidebar/index.tsx new file mode 100644 index 000000000000..c27d9081dcc5 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar/index.tsx @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { Sidebar } from './sidebar'; diff --git a/x-pack/plugins/canvas/public/components/sidebar/multi_element_settings.tsx b/x-pack/plugins/canvas/public/components/sidebar/multi_element_settings.tsx new file mode 100644 index 000000000000..15c5974c9e89 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar/multi_element_settings.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FunctionComponent } from 'react'; +import { EuiText, EuiSpacer } from '@elastic/eui'; +// @ts-ignore unconverted component +import { SidebarHeader } from '../sidebar_header/'; + +export const MultiElementSettings: FunctionComponent = () => ( + + + + +

Multiple elements are currently selected.

+

+ Deselect these elements to edit their individual settings, press (G) to group them, or save + this selection as a new element to re-use it throughout your workpad. +

+
+
+); diff --git a/x-pack/plugins/canvas/public/components/sidebar/sidebar.js b/x-pack/plugins/canvas/public/components/sidebar/sidebar.js deleted file mode 100644 index d1a03674e81a..000000000000 --- a/x-pack/plugins/canvas/public/components/sidebar/sidebar.js +++ /dev/null @@ -1,13 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { compose, branch, renderComponent } from 'recompose'; -import { SidebarComponent } from './sidebar_component'; -import { GlobalConfig } from './global_config'; - -const branches = [branch(props => !props.selectedElement, renderComponent(GlobalConfig))]; - -export const Sidebar = compose(...branches)(SidebarComponent); diff --git a/x-pack/plugins/canvas/public/components/sidebar/sidebar.tsx b/x-pack/plugins/canvas/public/components/sidebar/sidebar.tsx new file mode 100644 index 000000000000..96802f2627e7 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar/sidebar.tsx @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +// @ts-ignore unconverted component +import { SidebarContent } from './sidebar_content'; + +export const Sidebar: FunctionComponent = () => ( +
+ +
+); diff --git a/x-pack/plugins/canvas/public/components/sidebar/sidebar_component.js b/x-pack/plugins/canvas/public/components/sidebar/sidebar_component.js deleted file mode 100644 index 3d82a61efd1c..000000000000 --- a/x-pack/plugins/canvas/public/components/sidebar/sidebar_component.js +++ /dev/null @@ -1,133 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { - EuiTitle, - EuiSpacer, - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiTabbedContent, - EuiToolTip, -} from '@elastic/eui'; -import { Datasource } from '../datasource'; -import { FunctionFormList } from '../function_form_list'; - -export const SidebarComponent = ({ - selectedElement, - duplicateElement, - elementLayer, - elementIsSelected, -}) => { - const tabs = [ - { - id: 'edit', - name: 'Display', - content: ( -
- -
- -
-
- ), - }, - { - id: 'data', - name: 'Data', - content: ( -
- - -
- ), - }, - ]; - - return ( -
- {elementIsSelected && ( -
- - - -

Selected layer

-
-
- - - - - - - elementLayer(Infinity)} - aria-label="Move element to top layer" - /> - - - - - elementLayer(1)} - aria-label="Move element up one layer" - /> - - - - - elementLayer(-1)} - aria-label="Move element down one layer" - /> - - - - - elementLayer(-Infinity)} - aria-label="Move element to bottom layer" - /> - - - - - duplicateElement()} - aria-label="Clone the selected element" - /> - - - - - - -
- -
- )} -
- ); -}; - -SidebarComponent.propTypes = { - selectedElement: PropTypes.object, - duplicateElement: PropTypes.func.isRequired, - elementLayer: PropTypes.func, - elementIsSelected: PropTypes.bool.isRequired, -}; diff --git a/x-pack/plugins/canvas/public/components/sidebar/sidebar_content.js b/x-pack/plugins/canvas/public/components/sidebar/sidebar_content.js new file mode 100644 index 000000000000..5e33a0810f06 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar/sidebar_content.js @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { compose, branch, renderComponent } from 'recompose'; +import { getSelectedToplevelNodes, getSelectedElementId } from '../../state/selectors/workpad'; +import { MultiElementSettings } from './multi_element_settings'; +import { GroupSettings } from './group_settings'; +import { GlobalConfig } from './global_config'; +import { ElementSettings } from './element_settings'; + +const mapStateToProps = state => ({ + selectedToplevelNodes: getSelectedToplevelNodes(state), + selectedElementId: getSelectedElementId(state), +}); + +const branches = [ + // no elements selected + branch( + ({ selectedToplevelNodes }) => !selectedToplevelNodes.length, + renderComponent(GlobalConfig) + ), + // multiple elements selected + branch( + ({ selectedToplevelNodes }) => selectedToplevelNodes.length > 1, + renderComponent(MultiElementSettings) + ), + // a single, grouped element is selected + branch( + ({ selectedToplevelNodes }) => + selectedToplevelNodes.length === 1 && selectedToplevelNodes[0].includes('group'), + renderComponent(GroupSettings) + ), +]; + +export const SidebarContent = compose( + connect(mapStateToProps), + ...branches +)(ElementSettings); diff --git a/x-pack/plugins/canvas/public/components/sidebar/sidebar_section_title.js b/x-pack/plugins/canvas/public/components/sidebar/sidebar_section_title.js index 2d9888b96945..192786ae86a4 100644 --- a/x-pack/plugins/canvas/public/components/sidebar/sidebar_section_title.js +++ b/x-pack/plugins/canvas/public/components/sidebar/sidebar_section_title.js @@ -10,7 +10,7 @@ import { EuiTitle, EuiFlexItem, EuiFlexGroup, EuiToolTip } from '@elastic/eui'; export const SidebarSectionTitle = ({ title, tip, children }) => { const formattedTitle = ( - +

{title}

); @@ -27,7 +27,12 @@ export const SidebarSectionTitle = ({ title, tip, children }) => { }; return ( - + {renderTitle(tip)} {children} diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.examples.storyshot b/x-pack/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.examples.storyshot new file mode 100644 index 000000000000..bd766dd480d2 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.examples.storyshot @@ -0,0 +1,398 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/SidebarHeader/ default 1`] = ` +
+
+
+

+ Selected layer +

+
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+
+
+
+`; + +exports[`Storyshots components/SidebarHeader/ without layer controls 1`] = ` +
+
+
+

+ Grouped element +

+
+
+
+
+ + + +
+
+ +
+
+
+
+
+`; diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/__examples__/sidebar_header.examples.tsx b/x-pack/plugins/canvas/public/components/sidebar_header/__examples__/sidebar_header.examples.tsx new file mode 100644 index 000000000000..b91eccef937c --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar_header/__examples__/sidebar_header.examples.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { SidebarHeader } from '../sidebar_header'; + +const handlers = { + cloneNodes: action('cloneNodes'), + copyNodes: action('copyNodes'), + cutNodes: action('cutNodes'), + pasteNodes: action('pasteNodes'), + deleteNodes: action('deleteNodes'), + bringToFront: action('bringToFront'), + bringForward: action('bringForward'), + sendBackward: action('sendBackward'), + sendToBack: action('sendToBack'), + createCustomElement: action('createCustomElement'), +}; +storiesOf('components/SidebarHeader/', module) + .addDecorator(story =>
{story()}
) + .add('default', () => ) + .add('without layer controls', () => ( + + )); diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/custom_element_modal.tsx b/x-pack/plugins/canvas/public/components/sidebar_header/custom_element_modal.tsx new file mode 100644 index 000000000000..5b80f4c21ff9 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar_header/custom_element_modal.tsx @@ -0,0 +1,217 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable react/forbid-elements */ +import React, { PureComponent } from 'react'; +import { get } from 'lodash'; +import PropTypes from 'prop-types'; +import { + EuiButton, + EuiButtonEmpty, + // @ts-ignore hasn't been converted to TypeScript yet + EuiCard, + EuiFieldText, + // @ts-ignore hasn't been converted to TypeScript yet + EuiFilePicker, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIcon, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, + EuiSpacer, + EuiText, + EuiTextArea, + EuiTitle, +} from '@elastic/eui'; +// @ts-ignore converting /libs/constants to TS breaks CI +import { VALID_IMAGE_TYPES } from '../../../common/lib/constants'; +import { encode } from '../../../common/lib/dataurl'; + +const MAX_NAME_LENGTH = 40; +const MAX_DESCRIPTION_LENGTH = 100; + +interface Props { + /** + * initial value of the name of the custom element + */ + name?: string; + /** + * initial value of the description of the custom element + */ + description?: string; + /** + * initial value of the preview image of the custom element as a base64 dataurl + */ + image?: string; + /** + * title of the modal + */ + title: string; + /** + * A click handler for the save button + */ + onSave: (name: string, description?: string, image?: string | null) => void; + /** + * A click handler for the cancel button + */ + onCancel: () => void; +} + +interface State { + /** + * name of the custom element to be saved + */ + name?: string; + /** + * description of the custom element to be saved + */ + description?: string; + /** + * image of the custom element to be saved + */ + image?: string | null; +} + +export class CustomElementModal extends PureComponent { + public static propTypes = { + name: PropTypes.string, + description: PropTypes.string, + image: PropTypes.string, + title: PropTypes.string.isRequired, + onSave: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + }; + + public state = { + name: this.props.name || '', + description: this.props.description || '', + image: this.props.image || null, + }; + + private _handleChange = (type: string, value: string | null) => { + this.setState({ [type]: value }); + }; + + private _handleUpload = (files: File[]) => { + const [file] = files; + const [type, subtype] = get(file, 'type', '').split('/'); + if (type === 'image' && VALID_IMAGE_TYPES.indexOf(subtype) >= 0) { + encode(file).then((dataurl: string) => this._handleChange('image', dataurl)); + } + }; + + public render() { + const { onSave, onCancel, title, ...rest } = this.props; + const { name, description, image } = this.state; + + return ( + + + + +

{title}

+
+
+ + + + + + e.target.value.length <= MAX_NAME_LENGTH && + this._handleChange('name', e.target.value) + } + required + /> + + + + e.target.value.length <= MAX_DESCRIPTION_LENGTH && + this._handleChange('description', e.target.value) + } + /> + + + + + +

+ Take a screenshot of your element and upload it here. This can also be done + after saving. +

+
+
+ + +

Element preview

+
+ + } + title={name} + description={description} + className={image ? 'canvasCard' : 'canvasCard canvasCard--hasIcon'} + /> +
+
+
+ + + + Cancel + + + { + onSave(name, description, image); + onCancel(); + }} + > + Save + + + + +
+
+ ); + } +} diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/index.js b/x-pack/plugins/canvas/public/components/sidebar_header/index.js new file mode 100644 index 000000000000..e9b4fc71fd27 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar_header/index.js @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { compose, withHandlers } from 'recompose'; +import { insertNodes, elementLayer, removeElements } from '../../state/actions/elements'; +import { getSelectedPage, getNodes, getSelectedToplevelNodes } from '../../state/selectors/workpad'; +import { flatten } from '../../lib/aeroelastic/functional'; +import { + layerHandlerCreators, + clipboardHandlerCreators, + basicHandlerCreators, +} from '../../lib/element_handler_creators'; +import { crawlTree } from '../workpad_page/integration_utils'; +import { selectToplevelNodes } from './../../state/actions/transient'; +import { SidebarHeader as Component } from './sidebar_header'; + +/* + * TODO: this is all copied from interactive_workpad_page and workpad_shortcuts + */ +const mapStateToProps = state => { + const pageId = getSelectedPage(state); + const nodes = getNodes(state, pageId); + const selectedToplevelNodes = getSelectedToplevelNodes(state); + const selectedPrimaryShapeObjects = selectedToplevelNodes + .map(id => nodes.find(s => s.id === id)) + .filter(shape => shape); + const selectedPersistentPrimaryNodes = flatten( + selectedPrimaryShapeObjects.map(shape => + nodes.find(n => n.id === shape.id) // is it a leaf or a persisted group? + ? [shape.id] + : nodes.filter(s => s.parent === shape.id).map(s => s.id) + ) + ); + const selectedNodeIds = flatten(selectedPersistentPrimaryNodes.map(crawlTree(nodes))); + + return { + pageId, + selectedNodes: selectedNodeIds.map(id => nodes.find(s => s.id === id)), + }; +}; + +const mapDispatchToProps = dispatch => ({ + insertNodes: (selectedNodes, pageId) => dispatch(insertNodes(selectedNodes, pageId)), + removeNodes: (nodeIds, pageId) => dispatch(removeElements(nodeIds, pageId)), + selectToplevelNodes: nodes => + dispatch(selectToplevelNodes(nodes.filter(e => !e.position.parent).map(e => e.id))), + elementLayer: (pageId, elementId, movement) => { + dispatch(elementLayer({ pageId, elementId, movement })); + }, +}); + +export const SidebarHeader = compose( + connect( + mapStateToProps, + mapDispatchToProps + ), + withHandlers(basicHandlerCreators), + withHandlers(clipboardHandlerCreators), + withHandlers(layerHandlerCreators) + // TODO: restore when group and ungroup can be triggered outside of workpad_page + // withHandlers(groupHandlerCreators), +)(Component); diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.scss b/x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.scss new file mode 100644 index 000000000000..5f31af697a73 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.scss @@ -0,0 +1,24 @@ +.canvasLayout__sidebarHeader { + padding: $euiSizeS 0; +} + +.canvasContextMenu--topBorder { + border-top: $euiBorderThin; +} + +.canvasCustomElementForm { + min-width: 400px; +} + +.canvasCustomElementForm__preview { + max-width: $canvasElementCardWidth; + min-height: $canvasElementCardWidth; +} + +.canvasCustomElementForm__thumbnail { + padding-bottom: 0; +} + +.canvasCustomElementForm__thumbnailHelp { + color: $euiColorDarkShade; +} \ No newline at end of file diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx b/x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx new file mode 100644 index 000000000000..57c0ee301ae1 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx @@ -0,0 +1,377 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component, Fragment, MouseEvent } from 'react'; +import PropTypes from 'prop-types'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiButtonIcon, + EuiContextMenu, + EuiToolTip, + EuiContextMenuPanelItemDescriptor, + EuiContextMenuPanelDescriptor, +} from '@elastic/eui'; +// @ts-ignore unconverted component +import { Popover } from '../popover'; +import { CustomElementModal } from './custom_element_modal'; + +const topBorderClassName = 'canvasContextMenu--topBorder'; + +interface Props { + /** + * title to display in the header + */ + title: string; + /** + * indicated whether or not layer controls should be displayed + */ + showLayerControls?: boolean; + /** + * cuts selected elements + */ + cutNodes: () => void; + /** + * copies selected elements to clipboard + */ + copyNodes: () => void; + /** + * pastes elements stored in clipboard to page + */ + pasteNodes: () => void; + /** + * clones selected elements + */ + cloneNodes: () => void; + /** + * deletes selected elements + */ + deleteNodes: () => void; + /** + * moves selected element to top layer + */ + bringToFront: () => void; + /** + * moves selected element up one layer + */ + bringForward: () => void; + /** + * moves selected element down one layer + */ + sendBackward: () => void; + /** + * moves selected element to bottom layer + */ + sendToBack: () => void; + /** + * saves the selected elements as an custom-element saved object + */ + createCustomElement: () => void; + // TODO: restore when group and ungroup can be triggered outside of workpad_page + // /** + // * indicated whether the selected element is a group or not + // */ + // groupIsSelected: boolean, + // /** + // * groups selected elements + // */ + // groupNodes: () => void; + // /** + // * ungroups selected group + // */ + // ungroupNodes: () => void; +} + +interface State { + /** + * indicates whether or not the custom element modal is open + */ + isModalVisible: boolean; +} + +const contextMenuButton = (handleClick: (event: MouseEvent) => void) => ( + +); + +export class SidebarHeader extends Component { + public static propTypes = { + title: PropTypes.string.isRequired, + showLayerControls: PropTypes.bool, // TODO: remove when we support relayering multiple elements + cutNodes: PropTypes.func.isRequired, + copyNodes: PropTypes.func.isRequired, + pasteNodes: PropTypes.func.isRequired, + cloneNodes: PropTypes.func.isRequired, + deleteNodes: PropTypes.func.isRequired, + bringToFront: PropTypes.func.isRequired, + bringForward: PropTypes.func.isRequired, + sendBackward: PropTypes.func.isRequired, + sendToBack: PropTypes.func.isRequired, + createCustomElement: PropTypes.func.isRequired, + // TODO: restore when group and ungroup can be triggered outside of workpad_page + // groupIsSelected: PropTypes.bool, + // groupNodes: PropTypes.func.isRequired, + // ungroupNodes: PropTypes.func.isRequired, + }; + + public static defaultProps = { + // TODO: restore when group and ungroup can be triggered outside of workpad_page + // groupIsSelected: false, + showLayerControls: true, + }; + + public state = { + isModalVisible: false, + }; + + private _isMounted = false; + private _showModal = () => this._isMounted && this.setState({ isModalVisible: true }); + private _hideModal = () => this._isMounted && this.setState({ isModalVisible: false }); + + public componentDidMount() { + this._isMounted = true; + } + + public componentWillUnmount() { + this._isMounted = false; + } + private _renderLayoutControls = () => { + const { bringToFront, bringForward, sendBackward, sendToBack } = this.props; + return ( + + + + + + + + + + + + + + + + + + + + + + + ); + }; + + private _getLayerMenuItems = (): { + menuItem: EuiContextMenuPanelItemDescriptor; + panel: EuiContextMenuPanelDescriptor; + } => { + const { bringToFront, bringForward, sendBackward, sendToBack } = this.props; + + return { + menuItem: { name: 'Order', className: topBorderClassName, panel: 1 }, + panel: { + id: 1, + title: 'Order', + items: [ + { + name: 'Bring to front', // TODO: check against current element position and disable if already top layer + icon: 'sortUp', + onClick: bringToFront, + }, + { + name: 'Bring forward', // TODO: same as above + icon: 'arrowUp', + onClick: bringForward, + }, + { + name: 'Send backward', // TODO: check against current element position and disable if already bottom layer + icon: 'arrowDown', + onClick: sendBackward, + }, + { + name: 'Send to back', // TODO: same as above + icon: 'sortDown', + onClick: sendToBack, + }, + ], + }, + }; + }; + + // TODO: restore when group and ungroup can be triggered outside of workpad_page + // private _getGroupMenuItem = ():EuiContextMenuPanelItemDescriptor => { + // const { groupIsSelected, ungroupNodes, groupNodes } = this.props; + // return groupIsSelected + // ? { + // name: 'Ungroup', + // className: topBorderClassName, + // onClick: close(ungroupNodes), + // } + // : { + // name: 'Group', + // className: topBorderClassName, + // onClick: close(groupNodes), + // }; + // }; + + private _getPanels = (closePopover: () => void): EuiContextMenuPanelDescriptor[] => { + const { + showLayerControls, + cutNodes, + copyNodes, + pasteNodes, + deleteNodes, + cloneNodes, + } = this.props; + + // closes popover after invoking fn + const close = (fn: () => void) => () => { + fn(); + closePopover(); + }; + + const items: EuiContextMenuPanelItemDescriptor[] = [ + { + name: 'Cut', + icon: 'cut', + onClick: close(cutNodes), + }, + { + name: 'Copy', + icon: 'copy', + onClick: copyNodes, + }, + { + name: 'Paste', // TODO: can this be disabled if clipboard is empty? + icon: 'copyClipboard', + onClick: close(pasteNodes), + }, + { + name: 'Delete', + icon: 'trash', + onClick: close(deleteNodes), + }, + { + name: 'Clone', + onClick: close(cloneNodes), + }, + // TODO: restore when group and ungroup can be triggered outside of workpad_page + // this._getGroupMenuItem(), + ]; + + const panels: EuiContextMenuPanelDescriptor[] = [ + { + id: 0, + title: 'Element options', + items, + }, + ]; + + if (showLayerControls) { + const { menuItem, panel } = this._getLayerMenuItems(); + // add Order menu item to first panel + items.push(menuItem); + // add nested panel for layers controls + panels.push(panel); + } + + items.push({ + name: 'Save as new element', + icon: 'indexOpen', + className: topBorderClassName, + onClick: this._showModal, + }); + + return panels; + }; + + private _renderContextMenu = () => ( + + {({ closePopover }: { closePopover: () => void }) => ( + + )} + + ); + + render() { + const { title, showLayerControls, createCustomElement } = this.props; + const { isModalVisible } = this.state; + + return ( + + + + +

{title}

+
+
+ + + {showLayerControls ? this._renderLayoutControls() : null} + + + + + + {this._renderContextMenu()} + + +
+ {isModalVisible ? ( + + ) : null} +
+ ); + } +} diff --git a/x-pack/plugins/canvas/public/components/workpad_header/index.js b/x-pack/plugins/canvas/public/components/workpad_header/index.js index 66b1988c83c8..b3de256155d5 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/index.js +++ b/x-pack/plugins/canvas/public/components/workpad_header/index.js @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { compose, withState } from 'recompose'; +import { compose } from 'recompose'; import { connect } from 'react-redux'; import { canUserWrite } from '../../state/selectors/app'; import { getSelectedPage, isWriteable } from '../../state/selectors/workpad'; import { setWriteable } from '../../state/actions/workpad'; -import { addElement } from '../../state/actions/elements'; import { WorkpadHeader as Component } from './workpad_header'; const mapStateToProps = state => ({ @@ -20,14 +19,12 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => ({ setWriteable: isWriteable => dispatch(setWriteable(isWriteable)), - addElement: pageId => partialElement => dispatch(addElement(pageId, partialElement)), }); const mergeProps = (stateProps, dispatchProps, ownProps) => ({ ...stateProps, ...dispatchProps, ...ownProps, - addElement: dispatchProps.addElement(stateProps.selectedPage), toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), }); @@ -36,6 +33,5 @@ export const WorkpadHeader = compose( mapStateToProps, mapDispatchToProps, mergeProps - ), - withState('showElementModal', 'setShowElementModal', false) + ) )(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.js b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.js index cf13476f71da..eb817c522308 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.js +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.js @@ -28,12 +28,11 @@ export class WorkpadHeader extends React.PureComponent { static propTypes = { isWriteable: PropTypes.bool, toggleWriteable: PropTypes.func, - addElement: PropTypes.func.isRequired, - showElementModal: PropTypes.bool, - setShowElementModal: PropTypes.func, }; - fullscreenButton = ({ toggleFullscreen }) => ( + state = { isModalVisible: false }; + + _fullscreenButton = ({ toggleFullscreen }) => ( ); - keyHandler = action => { + _keyHandler = action => { if (action === 'EDITING') { this.props.toggleWriteable(); } }; - elementAdd = () => { - const { addElement, setShowElementModal } = this.props; + _hideElementModal = () => this.setState({ isModalVisible: false }); + _showElementModal = () => this.setState({ isModalVisible: true }); - return ( - - setShowElementModal(false)} - className="canvasModal--fixedSize" - maxWidth="1000px" - initialFocus=".canvasElements__filter" - > - { - addElement(element); - setShowElementModal(false); - }} - /> - - setShowElementModal(false)}> - Close - - - - - ); - }; + _elementAdd = () => ( + + + + + + Close + + + + + ); - getTooltipText = () => { + _getTooltipText = () => { if (!this.props.canUserWrite) { return "You don't have permission to edit this workpad"; } else { @@ -85,17 +78,12 @@ export class WorkpadHeader extends React.PureComponent { }; render() { - const { - isWriteable, - canUserWrite, - toggleWriteable, - setShowElementModal, - showElementModal, - } = this.props; + const { isWriteable, canUserWrite, toggleWriteable } = this.props; + const { isModalVisible } = this.state; return (
- {showElementModal ? this.elementAdd() : null} + {isModalVisible ? this._elementAdd() : null} @@ -106,7 +94,7 @@ export class WorkpadHeader extends React.PureComponent { - {this.fullscreenButton} + {this._fullscreenButton} @@ -115,19 +103,17 @@ export class WorkpadHeader extends React.PureComponent { {canUserWrite && ( )} - + { - toggleWriteable(); - }} + onClick={toggleWriteable} size="s" - aria-label={this.getTooltipText()} + aria-label={this._getTooltipText()} isDisabled={!canUserWrite} /> @@ -146,7 +132,7 @@ export class WorkpadHeader extends React.PureComponent { size="s" iconType="vector" data-test-subj="add-element-button" - onClick={() => setShowElementModal(true)} + onClick={this._showElementModal} > Add element diff --git a/x-pack/plugins/canvas/public/components/workpad_page/prop_types.js b/x-pack/plugins/canvas/public/components/workpad_page/prop_types.js index 7cf7eac48ca5..dc5f76ee82e6 100644 --- a/x-pack/plugins/canvas/public/components/workpad_page/prop_types.js +++ b/x-pack/plugins/canvas/public/components/workpad_page/prop_types.js @@ -48,4 +48,5 @@ export const interactiveWorkpadPagePropTypes = { sendToBack: PropTypes.func, canvasOrigin: PropTypes.func, saveCanvasOrigin: PropTypes.func.isRequired, + commit: PropTypes.func.isRequired, }; diff --git a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js index b3686d9b1a3f..69feffdf9d71 100644 --- a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js +++ b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js @@ -51,11 +51,6 @@ const configuration = { tooltipZ: 1100, }; -const groupHandlerCreators = { - groupNodes: ({ commit }) => () => commit('actionEvent', { event: 'group' }), - ungroupNodes: ({ commit }) => () => commit('actionEvent', { event: 'ungroup' }), -}; - const componentLayoutState = ({ aeroStore, setAeroStore, elements, selectedToplevelNodes }) => { const shapes = shapesForNodes(elements); const selectedShapes = selectedToplevelNodes.filter(e => shapes.find(s => s.id === e)); @@ -111,13 +106,13 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = dispatch => ({ dispatch, - insertNodes: pageId => selectedNodes => dispatch(insertNodes(selectedNodes, pageId)), - removeNodes: pageId => nodeIds => dispatch(removeElements(nodeIds, pageId)), + insertNodes: (selectedNodes, pageId) => dispatch(insertNodes(selectedNodes, pageId)), + removeNodes: (nodeIds, pageId) => dispatch(removeElements(nodeIds, pageId)), selectToplevelNodes: nodes => dispatch(selectToplevelNodes(nodes.filter(e => !e.position.parent).map(e => e.id))), // TODO: Abstract this out, this is similar to layering code in sidebar/index.js: - elementLayer: (pageId, selectedElement, movement) => { - dispatch(elementLayer({ pageId, elementId: selectedElement.id, movement })); + elementLayer: (pageId, elementId, movement) => { + dispatch(elementLayer({ pageId, elementId, movement })); }, }); @@ -164,7 +159,6 @@ export const InteractivePage = compose( withProps(({ commit, forceRerender }) => ({ commit: (...args) => forceRerender(commit(...args)), })), - withHandlers(groupHandlerCreators), withHandlers(eventHandlers), // Captures user intent, needs to have reconciled state () => InteractiveComponent ); diff --git a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interactive_workpad_page.js b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interactive_workpad_page.js index 46c60f383e98..881503132d23 100644 --- a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interactive_workpad_page.js +++ b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interactive_workpad_page.js @@ -12,8 +12,8 @@ import { TooltipAnnotation } from '../../tooltip_annotation'; import { RotationHandle } from '../../rotation_handle'; import { BorderConnection } from '../../border_connection'; import { BorderResizeHandle } from '../../border_resize_handle'; +import { WorkpadShortcuts } from '../../workpad_shortcuts'; import { interactiveWorkpadPagePropTypes } from '../prop_types'; -import { WorkpadShortcuts } from './workpad_shortcuts'; export class InteractiveWorkpadPage extends PureComponent { static propTypes = interactiveWorkpadPagePropTypes; @@ -44,23 +44,21 @@ export class InteractiveWorkpadPage extends PureComponent { insertNodes, removeNodes, elementLayer, - groupNodes, - ungroupNodes, canvasOrigin, saveCanvasOrigin, + commit, } = this.props; let shortcuts = null; const shortcutProps = { elementLayer, - groupNodes, insertNodes, pageId, removeNodes, selectedNodes, selectToplevelNodes, - ungroupNodes, + commit, }; shortcuts = ; diff --git a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/workpad_shortcuts.tsx b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/workpad_shortcuts.tsx deleted file mode 100644 index 803b31dc3d76..000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/workpad_shortcuts.tsx +++ /dev/null @@ -1,118 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Component } from 'react'; - -import isEqual from 'react-fast-compare'; -// @ts-ignore -import { Shortcuts } from 'react-shortcuts'; -// @ts-ignore -import { getClipboardData, setClipboardData } from '../../../lib/clipboard'; -// @ts-ignore -import { cloneSubgraphs } from '../../../lib/clone_subgraphs'; -// @ts-ignore -import { notify } from '../../../lib/notify'; - -export interface Props { - pageId: string; - selectedNodes: any[]; - selectToplevelNodes: (...nodeIds: string[]) => void; - insertNodes: (pageId: string) => (selectedNodes: any[]) => void; - removeNodes: (pageId: string) => (selectedNodeIds: string[]) => void; - elementLayer: (pageId: string, selectedNode: any, movement: any) => void; - groupNodes: () => void; - ungroupNodes: () => void; -} - -const id = (node: any): string => node.id; - -const keyMap = { - DELETE: function deleteNodes({ pageId, removeNodes, selectedNodes }: any): any { - // currently, handle the removal of one node, exploiting multiselect subsequently - if (selectedNodes.length) { - removeNodes(pageId)(selectedNodes.map(id)); - } - }, - COPY: function copyNodes({ selectedNodes }: any): any { - if (selectedNodes.length) { - setClipboardData({ selectedNodes }); - notify.success('Copied element to clipboard'); - } - }, - CUT: function cutNodes({ pageId, removeNodes, selectedNodes }: any): any { - if (selectedNodes.length) { - setClipboardData({ selectedNodes }); - removeNodes(pageId)(selectedNodes.map(id)); - notify.success('Cut element to clipboard'); - } - }, - CLONE: function duplicateNodes({ - insertNodes, - pageId, - selectToplevelNodes, - selectedNodes, - }: any): any { - // TODO: This is slightly different from the duplicateNodes function in sidebar/index.js. Should they be doing the same thing? - // This should also be abstracted. - const clonedNodes = selectedNodes && cloneSubgraphs(selectedNodes); - if (clonedNodes) { - insertNodes(pageId)(clonedNodes); - selectToplevelNodes(clonedNodes); - } - }, - PASTE: function pasteNodes({ insertNodes, pageId, selectToplevelNodes }: any): any { - const { selectedNodes } = JSON.parse(getClipboardData()) || { selectedNodes: [] }; - const clonedNodes = selectedNodes && cloneSubgraphs(selectedNodes); - if (clonedNodes) { - insertNodes(pageId)(clonedNodes); // first clone and persist the new node(s) - selectToplevelNodes(clonedNodes); // then select the cloned node(s) - } - }, - BRING_FORWARD: function bringForward({ elementLayer, pageId, selectedNodes }: any): any { - // TODO: Same as above. Abstract these out. This is the same code as in sidebar/index.js - // Note: these layer actions only work when a single node is selected - if (selectedNodes.length === 1) { - elementLayer(pageId, selectedNodes[0], 1); - } - }, - BRING_TO_FRONT: function bringToFront({ elementLayer, pageId, selectedNodes }: any): any { - if (selectedNodes.length === 1) { - elementLayer(pageId, selectedNodes[0], Infinity); - } - }, - SEND_BACKWARD: function sendBackward({ elementLayer, pageId, selectedNodes }: any): any { - if (selectedNodes.length === 1) { - elementLayer(pageId, selectedNodes[0], -1); - } - }, - SEND_TO_BACK: function sendToBack({ elementLayer, pageId, selectedNodes }: any): any { - if (selectedNodes.length === 1) { - elementLayer(pageId, selectedNodes[0], -Infinity); - } - }, - GROUP: ({ groupNodes }: any): any => groupNodes(), - UNGROUP: ({ ungroupNodes }: any): any => ungroupNodes(), -} as any; - -export class WorkpadShortcuts extends Component { - public render() { - return ( - { - event.preventDefault(); - keyMap[action](this.props); - }} - targetNodeSelector={`#${this.props.pageId}`} - global - /> - ); - } - - public shouldComponentUpdate(nextProps: Props) { - return !isEqual(nextProps, this.props); - } -} diff --git a/x-pack/plugins/canvas/public/components/workpad_shortcuts/index.tsx b/x-pack/plugins/canvas/public/components/workpad_shortcuts/index.tsx new file mode 100644 index 000000000000..e9e947ae5fd5 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_shortcuts/index.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import PropTypes from 'prop-types'; +import { withHandlers, compose } from 'recompose'; +import { WorkpadShortcuts as Component, Props as WorkpadShortcutsProps } from './workpad_shortcuts'; +import { + groupHandlerCreators, + layerHandlerCreators, + basicHandlerCreators, + clipboardHandlerCreators, + Props as HandlerCreatorProps, +} from '../../lib/element_handler_creators'; + +export const WorkpadShortcuts = compose( + withHandlers(groupHandlerCreators), + withHandlers(layerHandlerCreators), + withHandlers(basicHandlerCreators), + withHandlers(clipboardHandlerCreators) +)(Component); + +WorkpadShortcuts.propTypes = { + pageId: PropTypes.string.isRequired, + selectedNodes: PropTypes.arrayOf(PropTypes.object), + elementLayer: PropTypes.func.isRequired, + insertNodes: PropTypes.func.isRequired, + removeNodes: PropTypes.func.isRequired, + selectToplevelNodes: PropTypes.func.isRequired, + commit: PropTypes.func.isRequired, +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_shortcuts/workpad_shortcuts.tsx b/x-pack/plugins/canvas/public/components/workpad_shortcuts/workpad_shortcuts.tsx new file mode 100644 index 000000000000..545042eba31a --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_shortcuts/workpad_shortcuts.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component, KeyboardEvent } from 'react'; + +import isEqual from 'react-fast-compare'; +// @ts-ignore no @types definition +import { Shortcuts } from 'react-shortcuts'; +import { isTextInput } from '../../lib/is_text_input'; + +export interface Props { + /** + * cuts selected elements + */ + cutNodes: () => void; + /** + * copies selected elements to clipboard + */ + copyNodes: () => void; + /** + * pastes elements stored in clipboard to page + */ + pasteNodes: () => void; + /** + * clones selected elements + */ + cloneNodes: () => void; + /** + * deletes selected elements + */ + deleteNodes: () => void; + /** + * moves selected element to top layer + */ + bringToFront: () => void; + /** + * moves selected element up one layer + */ + bringForward: () => void; + /** + * moves selected element down one layer + */ + sendBackward: () => void; + /** + * moves selected element to bottom layer + */ + sendToBack: () => void; + /** + * groups selected elements + */ + groupNodes: () => void; + /** + * ungroups selected group + */ + ungroupNodes: () => void; +} + +export class WorkpadShortcuts extends Component { + private _keyMap: { [key: string]: () => void } = { + CUT: this.props.cutNodes, + COPY: this.props.copyNodes, + PASTE: this.props.pasteNodes, + CLONE: this.props.cloneNodes, + DELETE: this.props.deleteNodes, + BRING_TO_FRONT: this.props.bringToFront, + BRING_FORWARD: this.props.bringForward, + SEND_BACKWARD: this.props.sendBackward, + SEND_TO_BACK: this.props.sendToBack, + GROUP: this.props.groupNodes, + UNGROUP: this.props.ungroupNodes, + }; + + public render() { + return ( + { + if (!isTextInput(event.target as HTMLInputElement)) { + event.preventDefault(); + this._keyMap[action](); + } + }} + targetNodeSelector={`body`} + global + /> + ); + } + + public shouldComponentUpdate(nextProps: Props) { + return !isEqual(nextProps, this.props); + } +} diff --git a/x-pack/plugins/canvas/public/lib/custom_element_service.ts b/x-pack/plugins/canvas/public/lib/custom_element_service.ts new file mode 100644 index 000000000000..56c3b79ce37f --- /dev/null +++ b/x-pack/plugins/canvas/public/lib/custom_element_service.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +// @ts-ignore unconverted Elastic lib +import chrome from 'ui/chrome'; +import { AxiosPromise } from 'axios'; +// @ts-ignore unconverted local file +import { API_ROUTE_CUSTOM_ELEMENT } from '../../common/lib/constants'; +// @ts-ignore unconverted local file +import { fetch } from '../../common/lib/fetch'; + +interface CustomElement { + /** + * unique ID for the custom element + */ + id: string; + /** + * name of the custom element + */ + name: string; + /** + * name to be displayed from element picker + */ + displayName: string; + /** + * description of the custom element + */ + help?: string; + /** + * base 64 data URL string of the preview image + */ + image?: string; + /** + * the element object stringified + */ + content: string; +} + +const basePath = chrome.getBasePath(); +const apiPath = `${basePath}${API_ROUTE_CUSTOM_ELEMENT}`; + +export const create = (customElement: CustomElement): AxiosPromise => + fetch.post(apiPath, customElement); + +export const get = (customElementId: string): Promise => + fetch + .get(`${apiPath}/${customElementId}`) + .then(({ data: element }: { data: CustomElement }) => element); + +export const update = (id: string, element: CustomElement): AxiosPromise => + fetch.put(`${apiPath}/${id}`, element); + +export const remove = (id: string): AxiosPromise => fetch.delete(`${apiPath}/${id}`); + +export const find = async ( + searchTerm: string +): Promise<{ total: number; customElements: CustomElement[] }> => { + const validSearchTerm = typeof searchTerm === 'string' && searchTerm.length > 0; + + return fetch + .get(`${apiPath}/find?name=${validSearchTerm ? searchTerm : ''}&perPage=10000`) + .then( + ({ data: customElements }: { data: { total: number; customElements: CustomElement[] } }) => + customElements + ); +}; diff --git a/x-pack/plugins/canvas/public/lib/elastic_logo.js b/x-pack/plugins/canvas/public/lib/elastic_logo.js deleted file mode 100644 index 1ade7f1f269c..000000000000 --- a/x-pack/plugins/canvas/public/lib/elastic_logo.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-disable */ -export const elasticLogo = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+'; diff --git a/x-pack/plugins/canvas/public/lib/elastic_logo.ts b/x-pack/plugins/canvas/public/lib/elastic_logo.ts new file mode 100644 index 000000000000..e73b8615045a --- /dev/null +++ b/x-pack/plugins/canvas/public/lib/elastic_logo.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable */ +export const elasticLogo = + 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+'; diff --git a/x-pack/plugins/canvas/public/lib/element_handler_creators.ts b/x-pack/plugins/canvas/public/lib/element_handler_creators.ts new file mode 100644 index 000000000000..ee402a07497f --- /dev/null +++ b/x-pack/plugins/canvas/public/lib/element_handler_creators.ts @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Http2ServerResponse } from 'http2'; +import { camelCase } from 'lodash'; +// @ts-ignore unconverted local file +import { getClipboardData, setClipboardData } from './clipboard'; +// @ts-ignore unconverted local file +import { cloneSubgraphs } from './clone_subgraphs'; +// @ts-ignore unconverted local file +import { notify } from './notify'; +import * as customElementService from './custom_element_service'; +import { getId } from './get_id'; +import { PositionedElement } from './positioned_element'; + +const extractId = (node: { id: string }): string => node.id; + +export interface Props { + /** + * ID of the active page + */ + pageId: string; + /** + * array of selected elements + */ + selectedNodes: PositionedElement[]; + /** + * adds elements to the page + */ + insertNodes: (elements: PositionedElement[], pageId: string) => void; + /** + * changes the layer position of an element + */ + elementLayer: (pageId: string, elementId: string, movement: number) => void; + /** + * selects elements on the page + */ + selectToplevelNodes: (elements: PositionedElement) => void; + /** + * deletes elements from the page + */ + removeNodes: (nodeIds: string[], pageId: string) => void; + /** + * commits events to layout engine + */ + commit: (eventType: string, config: { event: string }) => void; +} + +// handlers for clone and delete +export const basicHandlerCreators = { + cloneNodes: ({ insertNodes, pageId, selectToplevelNodes, selectedNodes }: Props) => (): void => { + const clonedNodes = selectedNodes && cloneSubgraphs(selectedNodes); + if (clonedNodes) { + insertNodes(clonedNodes, pageId); + selectToplevelNodes(clonedNodes); + } + }, + deleteNodes: ({ pageId, removeNodes, selectedNodes }: Props) => (): void => { + if (selectedNodes.length) { + removeNodes(selectedNodes.map(extractId), pageId); + } + }, + createCustomElement: ({ selectedNodes }: Props) => ( + name = '', + description = '', + image = '' + ): void => { + if (selectedNodes.length) { + const content = JSON.stringify({ selectedNodes }); + const customElement = { + id: getId('custom-element'), + name: camelCase(name), + displayName: name, + help: description, + image, + content, + }; + customElementService + .create(customElement) + .then(() => + notify.success( + `Custom element '${customElement.displayName || customElement.id}' was saved` + ) + ) + .catch((result: Http2ServerResponse) => + notify.warning(result, { + title: `Custom element '${customElement.displayName || + customElement.id}' was not saved`, + }) + ); + } + }, +}; + +// handlers for group and ungroup +export const groupHandlerCreators = { + groupNodes: ({ commit }: Props) => (): void => { + commit('actionEvent', { event: 'group' }); + }, + ungroupNodes: ({ commit }: Props) => (): void => { + commit('actionEvent', { event: 'ungroup' }); + }, +}; + +// handlers for cut/copy/paste +export const clipboardHandlerCreators = { + cutNodes: ({ pageId, removeNodes, selectedNodes }: Props) => (): void => { + if (selectedNodes.length) { + setClipboardData({ selectedNodes }); + removeNodes(selectedNodes.map(extractId), pageId); + notify.success('Cut element to clipboard'); + } + }, + copyNodes: ({ selectedNodes }: Props) => (): void => { + if (selectedNodes.length) { + setClipboardData({ selectedNodes }); + notify.success('Copied element to clipboard'); + } + }, + pasteNodes: ({ insertNodes, pageId, selectToplevelNodes }: Props) => (): void => { + const { selectedNodes = [] } = JSON.parse(getClipboardData()) || {}; + const clonedNodes = selectedNodes && cloneSubgraphs(selectedNodes); + if (clonedNodes) { + insertNodes(clonedNodes, pageId); // first clone and persist the new node(s) + selectToplevelNodes(clonedNodes); // then select the cloned node(s) + } + }, +}; + +// handlers for changing element layer position +// TODO: support relayering multiple elements +export const layerHandlerCreators = { + bringToFront: ({ elementLayer, pageId, selectedNodes }: Props) => (): void => { + if (selectedNodes.length === 1) { + elementLayer(pageId, selectedNodes[0].id, Infinity); + } + }, + bringForward: ({ elementLayer, pageId, selectedNodes }: Props) => (): void => { + if (selectedNodes.length === 1) { + elementLayer(pageId, selectedNodes[0].id, 1); + } + }, + sendBackward: ({ elementLayer, pageId, selectedNodes }: Props) => (): void => { + if (selectedNodes.length === 1) { + elementLayer(pageId, selectedNodes[0].id, -1); + } + }, + sendToBack: ({ elementLayer, pageId, selectedNodes }: Props) => (): void => { + if (selectedNodes.length === 1) { + elementLayer(pageId, selectedNodes[0].id, -Infinity); + } + }, +}; diff --git a/x-pack/plugins/canvas/public/lib/get_id.js b/x-pack/plugins/canvas/public/lib/get_id.ts similarity index 86% rename from x-pack/plugins/canvas/public/lib/get_id.js rename to x-pack/plugins/canvas/public/lib/get_id.ts index 7518503e5f24..0927a277f7ca 100644 --- a/x-pack/plugins/canvas/public/lib/get_id.js +++ b/x-pack/plugins/canvas/public/lib/get_id.ts @@ -6,6 +6,6 @@ import uuid from 'uuid/v4'; -export function getId(type) { +export function getId(type: string): string { return `${type}-${uuid()}`; } diff --git a/x-pack/plugins/canvas/public/lib/is_text_input.js b/x-pack/plugins/canvas/public/lib/is_text_input.ts similarity index 88% rename from x-pack/plugins/canvas/public/lib/is_text_input.js rename to x-pack/plugins/canvas/public/lib/is_text_input.ts index b61a5df0f4c8..d0ae85844d09 100644 --- a/x-pack/plugins/canvas/public/lib/is_text_input.js +++ b/x-pack/plugins/canvas/public/lib/is_text_input.ts @@ -17,7 +17,7 @@ const nonTextInputs = [ 'submit', ]; -export const isTextInput = ({ tagName, type }) => { +export const isTextInput = ({ tagName, type }: HTMLInputElement): boolean => { switch (tagName.toLowerCase()) { case 'input': return !nonTextInputs.includes(type); diff --git a/x-pack/plugins/canvas/public/lib/keymap.js b/x-pack/plugins/canvas/public/lib/keymap.js index 669fea9ed6c2..138b0696a1ca 100644 --- a/x-pack/plugins/canvas/public/lib/keymap.js +++ b/x-pack/plugins/canvas/public/lib/keymap.js @@ -49,10 +49,10 @@ const fullscreentExitShortcut = ['esc']; export const keymap = { ELEMENT: { displayName: 'Element controls', - COPY: { ...getCtrlShortcuts('c'), help: 'Copy' }, - CLONE: { ...getCtrlShortcuts('d'), help: 'Clone' }, CUT: { ...getCtrlShortcuts('x'), help: 'Cut' }, + COPY: { ...getCtrlShortcuts('c'), help: 'Copy' }, PASTE: { ...getCtrlShortcuts('v'), help: 'Paste' }, + CLONE: { ...getCtrlShortcuts('d'), help: 'Clone' }, DELETE: { osx: deleteElementShortcuts, windows: deleteElementShortcuts, @@ -60,13 +60,13 @@ export const keymap = { other: deleteElementShortcuts, help: 'Delete', }, - BRING_FORWARD: { - ...getCtrlShortcuts('up'), - help: 'Send forward', - }, BRING_TO_FRONT: { ...getCtrlShortcuts('shift+up'), - help: 'Send to front', + help: 'Bring to front', + }, + BRING_FORWARD: { + ...getCtrlShortcuts('up'), + help: 'Bring forward', }, SEND_BACKWARD: { ...getCtrlShortcuts('down'), @@ -102,7 +102,7 @@ export const keymap = { REFRESH: refreshShortcut, }, PRESENTATION: { - displayName: 'Presentation mode', + displayName: 'Presentation controls', FULLSCREEN: { ...getAltShortcuts(['p', 'f']), help: 'Enter presentation mode' }, FULLSCREEN_EXIT: { osx: fullscreentExitShortcut, diff --git a/x-pack/plugins/canvas/public/lib/positioned_element.ts b/x-pack/plugins/canvas/public/lib/positioned_element.ts new file mode 100644 index 000000000000..1ac27a3d9d99 --- /dev/null +++ b/x-pack/plugins/canvas/public/lib/positioned_element.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +type ArgumentValue = null | string | number | boolean | Ast; + +interface Function { + type: 'function'; + /** + * name of the function + */ + function: string; + /** + * arguments passed into the function + */ + arguments: { [argument: string]: ArgumentValue } | {}; +} + +interface Ast { + type: 'expression'; + /** + * array of functions in the expression + */ + chain: Function[]; +} + +interface Position { + /** + * distance from the left edge of the page + */ + left: number; + /** + * distance from the top edge of the page + * */ + top: number; + /** + * width of the element + */ + width: number; + /** + * height of the element + */ + height: number; + /** + * angle of rotation + */ + angle: number; + /** + * the id of the parent of this element part of a group + */ + parent: string | null; +} + +export interface PositionedElement { + /** + * a Canvas element used to populate config forms + */ + id: string; + /** + * layout engine settings + */ + position: Position; + /** + * Canvas expression used to generate the element + */ + expression: string; + /** + * AST of the Canvas expression for the element + */ + ast: Ast; +} diff --git a/x-pack/plugins/canvas/public/state/selectors/workpad.js b/x-pack/plugins/canvas/public/state/selectors/workpad.js index 6959fb8eeeca..4bfa748e51fd 100644 --- a/x-pack/plugins/canvas/public/state/selectors/workpad.js +++ b/x-pack/plugins/canvas/public/state/selectors/workpad.js @@ -128,8 +128,12 @@ export function getGlobalFilterExpression(state) { } // element getters -function getSelectedElementId(state) { - const toplevelNodes = get(state, 'transient.selectedToplevelNodes') || []; +export function getSelectedToplevelNodes(state) { + return get(state, 'transient.selectedToplevelNodes', []); +} + +export function getSelectedElementId(state) { + const toplevelNodes = getSelectedToplevelNodes(state); return toplevelNodes.length === 1 ? toplevelNodes[0] : null; } diff --git a/x-pack/plugins/canvas/public/style/index.scss b/x-pack/plugins/canvas/public/style/index.scss index 0690bbdf397e..adb605b53ff4 100644 --- a/x-pack/plugins/canvas/public/style/index.scss +++ b/x-pack/plugins/canvas/public/style/index.scss @@ -45,6 +45,7 @@ @import '../components/shape_preview/shape_preview'; @import '../components/shape_picker_mini/shape_picker_mini'; @import '../components/sidebar/sidebar'; +@import '../components/sidebar_header/sidebar_header'; @import '../components/toolbar/toolbar'; @import '../components/toolbar/tray/tray'; @import '../components/tooltip_annotation/tooltip_annotation'; diff --git a/x-pack/plugins/canvas/public/style/main.scss b/x-pack/plugins/canvas/public/style/main.scss index ca22b3648a4d..ef3057ca539a 100644 --- a/x-pack/plugins/canvas/public/style/main.scss +++ b/x-pack/plugins/canvas/public/style/main.scss @@ -1,3 +1,8 @@ +/* + Canvas global SASS varialbes +*/ +$canvasElementCardWidth: 210px; + .canvas.canvasContainer { display: flex; flex-grow: 1; diff --git a/x-pack/plugins/canvas/server/lib/format_response.js b/x-pack/plugins/canvas/server/lib/format_response.js new file mode 100644 index 000000000000..d52e2819465f --- /dev/null +++ b/x-pack/plugins/canvas/server/lib/format_response.js @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import boom from 'boom'; + +export const formatResponse = esErrors => resp => { + if (resp.isBoom) { + return resp; + } // can't wrap it if it's already a boom error + + if (resp instanceof esErrors['400']) { + return boom.badRequest(resp); + } + + if (resp instanceof esErrors['401']) { + return boom.unauthorized(); + } + + if (resp instanceof esErrors['403']) { + return boom.forbidden("Sorry, you don't have access to that"); + } + + if (resp instanceof esErrors['404']) { + return boom.boomify(resp, { statusCode: 404 }); + } + + return resp; +}; diff --git a/x-pack/plugins/canvas/server/mappings.js b/x-pack/plugins/canvas/server/mappings.ts similarity index 51% rename from x-pack/plugins/canvas/server/mappings.js rename to x-pack/plugins/canvas/server/mappings.ts index 25ca8670b734..bf2be51882b1 100644 --- a/x-pack/plugins/canvas/server/mappings.js +++ b/x-pack/plugins/canvas/server/mappings.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CANVAS_TYPE } from '../common/lib/constants'; +// @ts-ignore converting /libs/constants to TS breaks CI +import { CANVAS_TYPE, CUSTOM_ELEMENT_TYPE } from '../common/lib/constants'; export const mappings = { [CANVAS_TYPE]: { @@ -22,4 +23,22 @@ export const mappings = { '@created': { type: 'date' }, }, }, + [CUSTOM_ELEMENT_TYPE]: { + dynamic: false, + properties: { + name: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, + }, + help: { type: 'text' }, + content: { type: 'text' }, + image: { type: 'text' }, + '@timestamp': { type: 'date' }, + '@created': { type: 'date' }, + }, + }, }; diff --git a/x-pack/plugins/canvas/server/routes/custom_elements.js b/x-pack/plugins/canvas/server/routes/custom_elements.js new file mode 100644 index 000000000000..372b01e6fa59 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements.js @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import boom from 'boom'; +import { omit } from 'lodash'; +import { API_ROUTE_CUSTOM_ELEMENT, CUSTOM_ELEMENT_TYPE } from '../../common/lib/constants'; +import { getId } from '../../public/lib/get_id'; +import { formatResponse as formatRes } from '../lib/format_response'; + +export function customElements(server) { + const { errors: esErrors } = server.plugins.elasticsearch.getCluster('data'); + const routePrefix = API_ROUTE_CUSTOM_ELEMENT; + const formatResponse = formatRes(esErrors); + + const createCustomElement = req => { + const savedObjectsClient = req.getSavedObjectsClient(); + + if (!req.payload) { + return Promise.reject(boom.badRequest('A custom element payload is required')); + } + + const now = new Date().toISOString(); + const { id, ...payload } = req.payload; + return savedObjectsClient.create( + CUSTOM_ELEMENT_TYPE, + { + ...payload, + '@timestamp': now, + '@created': now, + }, + { id: id || getId('custom-element') } + ); + }; + + const updateCustomElement = (req, newPayload) => { + const savedObjectsClient = req.getSavedObjectsClient(); + const { id } = req.params; + const payload = newPayload ? newPayload : req.payload; + + const now = new Date().toISOString(); + + return savedObjectsClient.get(CUSTOM_ELEMENT_TYPE, id).then(element => { + // TODO: Using create with force over-write because of version conflict issues with update + return savedObjectsClient.create( + CUSTOM_ELEMENT_TYPE, + { + ...element.attributes, + ...omit(payload, 'id'), // never write the id property + '@timestamp': now, // always update the modified time + '@created': element.attributes['@created'], // ensure created is not modified + }, + { overwrite: true, id } + ); + }); + }; + + const deleteCustomElement = req => { + const savedObjectsClient = req.getSavedObjectsClient(); + const { id } = req.params; + + return savedObjectsClient.delete(CUSTOM_ELEMENT_TYPE, id); + }; + + const findCustomElement = req => { + const savedObjectsClient = req.getSavedObjectsClient(); + const { name, page, perPage } = req.query; + + return savedObjectsClient.find({ + type: CUSTOM_ELEMENT_TYPE, + sortField: '@timestamp', + sortOrder: 'desc', + search: name ? `${name}* | ${name}` : '*', + searchFields: ['name'], + fields: ['id', 'name', 'displayName', 'help', 'image', 'content', '@created', '@timestamp'], + page, + perPage, + }); + }; + + const getCustomElementById = req => { + const savedObjectsClient = req.getSavedObjectsClient(); + const { id } = req.params; + return savedObjectsClient.get(CUSTOM_ELEMENT_TYPE, id); + }; + + // get custom element by id + server.route({ + method: 'GET', + path: `${routePrefix}/{id}`, + handler: req => + getCustomElementById(req) + .then(obj => ({ id: obj.id, ...obj.attributes })) + .then(formatResponse) + .catch(formatResponse), + }); + + // create custom element + server.route({ + method: 'POST', + path: routePrefix, + config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit + handler: req => + createCustomElement(req) + .then(() => ({ ok: true })) + .catch(formatResponse), + }); + + // update custom element + server.route({ + method: 'PUT', + path: `${routePrefix}/{id}`, + config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit + handler: req => + updateCustomElement(req) + .then(() => ({ ok: true })) + .catch(formatResponse), + }); + + // delete custom element + server.route({ + method: 'DELETE', + path: `${routePrefix}/{id}`, + handler: req => + deleteCustomElement(req) + .then(() => ({ ok: true })) + .catch(formatResponse), + }); + + // find custom elements + server.route({ + method: 'GET', + path: `${routePrefix}/find`, + handler: req => + findCustomElement(req) + .then(formatResponse) + .then(resp => { + return { + total: resp.total, + customElements: resp.saved_objects.map(hit => ({ id: hit.id, ...hit.attributes })), + }; + }) + .catch(() => { + return { + total: 0, + customElements: [], + }; + }), + }); +} diff --git a/x-pack/plugins/canvas/server/routes/index.js b/x-pack/plugins/canvas/server/routes/index.js index 45f26a423fc8..209851ee3f20 100644 --- a/x-pack/plugins/canvas/server/routes/index.js +++ b/x-pack/plugins/canvas/server/routes/index.js @@ -6,8 +6,10 @@ import { workpad } from './workpad'; import { esFields } from './es_fields'; +import { customElements } from './custom_elements'; export function routes(server) { - workpad(server); + customElements(server); esFields(server); + workpad(server); } diff --git a/x-pack/plugins/canvas/server/routes/workpad.js b/x-pack/plugins/canvas/server/routes/workpad.js index d45c197d5f80..9a9367cbf224 100644 --- a/x-pack/plugins/canvas/server/routes/workpad.js +++ b/x-pack/plugins/canvas/server/routes/workpad.js @@ -13,37 +13,14 @@ import { API_ROUTE_WORKPAD_STRUCTURES, } from '../../common/lib/constants'; import { getId } from '../../public/lib/get_id'; +import { formatResponse as formatRes } from '../lib/format_response'; export function workpad(server) { - //const config = server.config(); const { errors: esErrors } = server.plugins.elasticsearch.getCluster('data'); const routePrefix = API_ROUTE_WORKPAD; const routePrefixAssets = API_ROUTE_WORKPAD_ASSETS; const routePrefixStructures = API_ROUTE_WORKPAD_STRUCTURES; - - function formatResponse(resp) { - if (resp.isBoom) { - return resp; - } // can't wrap it if it's already a boom error - - if (resp instanceof esErrors['400']) { - return boom.badRequest(resp); - } - - if (resp instanceof esErrors['401']) { - return boom.unauthorized(); - } - - if (resp instanceof esErrors['403']) { - return boom.forbidden("Sorry, you don't have access to that"); - } - - if (resp instanceof esErrors['404']) { - return boom.boomify(resp, { statusCode: 404 }); - } - - return resp; - } + const formatResponse = formatRes(esErrors); function createWorkpad(req) { const savedObjectsClient = req.getSavedObjectsClient(); diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index e521e8176344..d47cc16bc869 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -716,6 +716,13 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { `saved_object:${version}:canvas-workpad/bulk_create`, `saved_object:${version}:canvas-workpad/update`, `saved_object:${version}:canvas-workpad/delete`, + `saved_object:${version}:canvas-element/bulk_get`, + `saved_object:${version}:canvas-element/get`, + `saved_object:${version}:canvas-element/find`, + `saved_object:${version}:canvas-element/create`, + `saved_object:${version}:canvas-element/bulk_create`, + `saved_object:${version}:canvas-element/update`, + `saved_object:${version}:canvas-element/delete`, `saved_object:${version}:telemetry/bulk_get`, `saved_object:${version}:telemetry/get`, `saved_object:${version}:telemetry/find`, @@ -732,6 +739,9 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { `ui:${version}:savedObjectsManagement/canvas-workpad/delete`, `ui:${version}:savedObjectsManagement/canvas-workpad/edit`, `ui:${version}:savedObjectsManagement/canvas-workpad/read`, + `ui:${version}:savedObjectsManagement/canvas-element/delete`, + `ui:${version}:savedObjectsManagement/canvas-element/edit`, + `ui:${version}:savedObjectsManagement/canvas-element/read`, `ui:${version}:savedObjectsManagement/telemetry/delete`, `ui:${version}:savedObjectsManagement/telemetry/edit`, `ui:${version}:savedObjectsManagement/telemetry/read`, @@ -753,11 +763,15 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { `saved_object:${version}:canvas-workpad/bulk_get`, `saved_object:${version}:canvas-workpad/get`, `saved_object:${version}:canvas-workpad/find`, + `saved_object:${version}:canvas-element/bulk_get`, + `saved_object:${version}:canvas-element/get`, + `saved_object:${version}:canvas-element/find`, `saved_object:${version}:config/bulk_get`, `saved_object:${version}:config/get`, `saved_object:${version}:config/find`, `ui:${version}:savedObjectsManagement/index-pattern/read`, `ui:${version}:savedObjectsManagement/canvas-workpad/read`, + `ui:${version}:savedObjectsManagement/canvas-element/read`, `ui:${version}:savedObjectsManagement/config/read`, ], }, @@ -1089,8 +1103,18 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { `saved_object:${version}:canvas-workpad/bulk_create`, `saved_object:${version}:canvas-workpad/update`, `saved_object:${version}:canvas-workpad/delete`, + `saved_object:${version}:canvas-element/bulk_get`, + `saved_object:${version}:canvas-element/get`, + `saved_object:${version}:canvas-element/find`, + `saved_object:${version}:canvas-element/create`, + `saved_object:${version}:canvas-element/bulk_create`, + `saved_object:${version}:canvas-element/update`, + `saved_object:${version}:canvas-element/delete`, `ui:${version}:savedObjectsManagement/canvas-workpad/delete`, `ui:${version}:savedObjectsManagement/canvas-workpad/edit`, + `ui:${version}:savedObjectsManagement/canvas-element/delete`, + `ui:${version}:savedObjectsManagement/canvas-element/edit`, + `ui:${version}:savedObjectsManagement/canvas-element/read`, `ui:${version}:canvas/save`, `api:${version}:infra`, `app:${version}:infra`, @@ -1206,6 +1230,10 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { `app:${version}:canvas`, `ui:${version}:catalogue/canvas`, `ui:${version}:navLinks/canvas`, + `saved_object:${version}:canvas-element/bulk_get`, + `saved_object:${version}:canvas-element/get`, + `saved_object:${version}:canvas-element/find`, + `ui:${version}:savedObjectsManagement/canvas-element/read`, `api:${version}:infra`, `app:${version}:infra`, `ui:${version}:catalogue/infraops`, @@ -1395,8 +1423,18 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { `saved_object:${version}:canvas-workpad/bulk_create`, `saved_object:${version}:canvas-workpad/update`, `saved_object:${version}:canvas-workpad/delete`, + `saved_object:${version}:canvas-element/bulk_get`, + `saved_object:${version}:canvas-element/get`, + `saved_object:${version}:canvas-element/find`, + `saved_object:${version}:canvas-element/create`, + `saved_object:${version}:canvas-element/bulk_create`, + `saved_object:${version}:canvas-element/update`, + `saved_object:${version}:canvas-element/delete`, `ui:${version}:savedObjectsManagement/canvas-workpad/delete`, `ui:${version}:savedObjectsManagement/canvas-workpad/edit`, + `ui:${version}:savedObjectsManagement/canvas-element/delete`, + `ui:${version}:savedObjectsManagement/canvas-element/edit`, + `ui:${version}:savedObjectsManagement/canvas-element/read`, `ui:${version}:canvas/save`, `api:${version}:infra`, `app:${version}:infra`, @@ -1512,6 +1550,10 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { `app:${version}:canvas`, `ui:${version}:catalogue/canvas`, `ui:${version}:navLinks/canvas`, + `saved_object:${version}:canvas-element/bulk_get`, + `saved_object:${version}:canvas-element/get`, + `saved_object:${version}:canvas-element/find`, + `ui:${version}:savedObjectsManagement/canvas-element/read`, `api:${version}:infra`, `app:${version}:infra`, `ui:${version}:catalogue/infraops`, diff --git a/x-pack/test/ui_capabilities/common/saved_objects_management_builder.ts b/x-pack/test/ui_capabilities/common/saved_objects_management_builder.ts index c522d68180fd..c2cfb00a9490 100644 --- a/x-pack/test/ui_capabilities/common/saved_objects_management_builder.ts +++ b/x-pack/test/ui_capabilities/common/saved_objects_management_builder.ts @@ -38,6 +38,7 @@ export class SavedObjectsManagementBuilder { 'map', 'maps-telemetry', 'canvas-workpad', + 'canvas-element', 'infrastructure-ui-source', 'upgrade-assistant-reindex-operation', 'upgrade-assistant-telemetry', diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/saved_objects_management.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/saved_objects_management.ts index 2accc571b628..f3e9937239d0 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/saved_objects_management.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/saved_objects_management.ts @@ -55,6 +55,7 @@ export default function savedObjectsManagementTests({ 'graph-workspace', 'map', 'canvas-workpad', + 'canvas-element', 'index-pattern', 'visualization', 'search', @@ -82,6 +83,7 @@ export default function savedObjectsManagementTests({ 'graph-workspace', 'map', 'canvas-workpad', + 'canvas-element', 'index-pattern', 'visualization', 'search', diff --git a/x-pack/test/ui_capabilities/security_only/tests/saved_objects_management.ts b/x-pack/test/ui_capabilities/security_only/tests/saved_objects_management.ts index 5c7fd0f84790..588b3adb13a3 100644 --- a/x-pack/test/ui_capabilities/security_only/tests/saved_objects_management.ts +++ b/x-pack/test/ui_capabilities/security_only/tests/saved_objects_management.ts @@ -49,6 +49,7 @@ export default function savedObjectsManagementTests({ 'graph-workspace', 'map', 'canvas-workpad', + 'canvas-element', 'index-pattern', 'visualization', 'search', @@ -72,6 +73,7 @@ export default function savedObjectsManagementTests({ 'graph-workspace', 'map', 'canvas-workpad', + 'canvas-element', 'index-pattern', 'visualization', 'search',