From 7878c0cf7221e9a59bbfc222afa036ce2fb86266 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 7 Aug 2018 15:40:12 -0600 Subject: [PATCH 01/28] just getting the popover to open and start laying out the context menu --- .../kibana/public/dashboard/dashboard_app.js | 22 +++--- .../dashboard/top_nav/get_top_nav_config.js | 8 +- .../public/dashboard/top_nav/share.html | 4 - src/ui/public/share/components/share_menu.js | 78 +++++++++++++++++++ src/ui/public/share/show_share_menu.js | 66 ++++++++++++++++ 5 files changed, 157 insertions(+), 21 deletions(-) delete mode 100644 src/core_plugins/kibana/public/dashboard/top_nav/share.html create mode 100644 src/ui/public/share/components/share_menu.js create mode 100644 src/ui/public/share/show_share_menu.js diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_app.js b/src/core_plugins/kibana/public/dashboard/dashboard_app.js index e2e12acafa407..c12254da3fe2c 100644 --- a/src/core_plugins/kibana/public/dashboard/dashboard_app.js +++ b/src/core_plugins/kibana/public/dashboard/dashboard_app.js @@ -43,6 +43,7 @@ import { showCloneModal } from './top_nav/show_clone_modal'; import { showSaveModal } from './top_nav/show_save_modal'; import { showAddPanel } from './top_nav/show_add_panel'; import { showOptionsPopover } from './top_nav/show_options_popover'; +import { showShareMenu } from 'ui/share/show_share_menu'; import { migrateLegacyQuery } from 'ui/utils/migrateLegacyQuery'; import * as filterActions from 'ui/doc_table/actions/filter'; import { FilterManagerProvider } from 'ui/filter_manager'; @@ -130,14 +131,6 @@ app.directive('dashboardApp', function ($injector) { dirty: !dash.id }; - this.getSharingTitle = () => { - return dash.title; - }; - - this.getSharingType = () => { - return 'dashboard'; - }; - dashboardStateManager.registerChangeListener(status => { this.appStatus.dirty = status.dirty || !dash.id; updateState(); @@ -399,6 +392,14 @@ app.directive('dashboardApp', function ($injector) { }, }); }; + navActions[TopNavIds.SHARE] = (menuItem, navController, anchorElement) => { + showShareMenu({ + anchorElement, + objectType: 'dashboard', + objectId: dash.id, + }); + }; + updateViewMode(dashboardStateManager.getViewMode()); // update root source when filters update @@ -438,11 +439,6 @@ app.directive('dashboardApp', function ($injector) { kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM); kbnUrl.removeParam(DashboardConstants.NEW_VISUALIZATION_ID_PARAM); } - - // TODO remove opts once share has been converted to react - $scope.opts = { - dashboard: dash, // used in share.html - }; } }; }); diff --git a/src/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.js b/src/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.js index 7343443f7bf9e..022f64348eb9b 100644 --- a/src/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.js +++ b/src/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.js @@ -38,7 +38,7 @@ export function getTopNavConfig(dashboardMode, actions, hideWriteControls) { ] : [ getFullScreenConfig(actions[TopNavIds.FULL_SCREEN]), - getShareConfig(), + getShareConfig(actions[TopNavIds.SHARE]), getCloneConfig(actions[TopNavIds.CLONE]), getEditConfig(actions[TopNavIds.ENTER_EDIT_MODE]) ] @@ -49,7 +49,7 @@ export function getTopNavConfig(dashboardMode, actions, hideWriteControls) { getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]), getAddConfig(actions[TopNavIds.ADD]), getOptionsConfig(actions[TopNavIds.OPTIONS]), - getShareConfig()]; + getShareConfig(actions[TopNavIds.SHARE])]; default: return []; } @@ -127,12 +127,12 @@ function getAddConfig(action) { /** * @returns {kbnTopNavConfig} */ -function getShareConfig() { +function getShareConfig(action) { return { key: TopNavIds.SHARE, description: 'Share Dashboard', testId: 'dashboardShareButton', - template: require('plugins/kibana/dashboard/top_nav/share.html') + run: action, }; } diff --git a/src/core_plugins/kibana/public/dashboard/top_nav/share.html b/src/core_plugins/kibana/public/dashboard/top_nav/share.html deleted file mode 100644 index 046acbb5c95b8..0000000000000 --- a/src/core_plugins/kibana/public/dashboard/top_nav/share.html +++ /dev/null @@ -1,4 +0,0 @@ - - diff --git a/src/ui/public/share/components/share_menu.js b/src/ui/public/share/components/share_menu.js new file mode 100644 index 0000000000000..6ce2d42c6ed6d --- /dev/null +++ b/src/ui/public/share/components/share_menu.js @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import { + EuiContextMenu, +} from '@elastic/eui'; + +export class ShareMenu extends Component { + + state = { + } + + panels = [ + { + id: 0, + title: `Share this ${this.props.objectType}`, + items: [{ + name: 'Embed code', + icon: 'console', + panel: 1 + }, { + name: 'Permalinks', + icon: 'link', + panel: 2 + }], + }, + { + id: 1, + title: 'Embed Code', + content: ( +
+ embed code content goes here +
+ ) + }, + { + id: 2, + title: 'Permalink', + content: ( +
+ Permalink content goes here +
+ ) + } + ]; + + render() { + return ( + + ); + } +} + +ShareMenu.propTypes = { + objectType: PropTypes.string.isRequired, +}; diff --git a/src/ui/public/share/show_share_menu.js b/src/ui/public/share/show_share_menu.js new file mode 100644 index 0000000000000..94a04dc9811bb --- /dev/null +++ b/src/ui/public/share/show_share_menu.js @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; + +import { ShareMenu } from './components/share_menu'; + +import { + EuiWrappingPopover, +} from '@elastic/eui'; + +let isOpen = false; + +const container = document.createElement('div'); + +const onClose = () => { + ReactDOM.unmountComponentAtNode(container); + isOpen = false; +}; + +export function showShareMenu({ + anchorElement, + objectType, + objectId, +}) { + if (isOpen) { + onClose(); + return; + } + + isOpen = true; + + document.body.appendChild(container); + const element = ( + + + + ); + ReactDOM.render(element, container); +} From b2660fe2690b89589cbb8deb8c56bcf51d02937b Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 8 Aug 2018 08:14:59 -0600 Subject: [PATCH 02/28] pass getUnhashableStates to ShareMenu --- src/core_plugins/kibana/public/dashboard/dashboard_app.js | 3 +++ src/ui/public/share/components/share_menu.js | 1 + 2 files changed, 4 insertions(+) diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_app.js b/src/core_plugins/kibana/public/dashboard/dashboard_app.js index c12254da3fe2c..8fbab0aa18212 100644 --- a/src/core_plugins/kibana/public/dashboard/dashboard_app.js +++ b/src/core_plugins/kibana/public/dashboard/dashboard_app.js @@ -51,6 +51,7 @@ import { EmbeddableFactoriesRegistryProvider } from 'ui/embeddable/embeddable_fa import { DashboardPanelActionsRegistryProvider } from 'ui/dashboard_panel_actions/dashboard_panel_actions_registry'; import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; import { timefilter } from 'ui/timefilter'; +import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing'; import { DashboardViewportProvider } from './viewport/dashboard_viewport_provider'; @@ -84,6 +85,7 @@ app.directive('dashboardApp', function ($injector) { const docTitle = Private(DocTitleProvider); const embeddableFactories = Private(EmbeddableFactoriesRegistryProvider); const panelActionsRegistry = Private(DashboardPanelActionsRegistryProvider); + const getUnhashableStates = Private(getUnhashableStatesProvider); panelActionsStore.initializeFromRegistry(panelActionsRegistry); @@ -397,6 +399,7 @@ app.directive('dashboardApp', function ($injector) { anchorElement, objectType: 'dashboard', objectId: dash.id, + getUnhashableStates, }); }; diff --git a/src/ui/public/share/components/share_menu.js b/src/ui/public/share/components/share_menu.js index 6ce2d42c6ed6d..87d30b096c469 100644 --- a/src/ui/public/share/components/share_menu.js +++ b/src/ui/public/share/components/share_menu.js @@ -75,4 +75,5 @@ export class ShareMenu extends Component { ShareMenu.propTypes = { objectType: PropTypes.string.isRequired, + getUnhashableStates: PropTypes.func.isRequired, }; From 8a84489c1dab404a5e0b5d0f27b456f7caa6922e Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 10 Aug 2018 11:24:10 -0600 Subject: [PATCH 03/28] generate original and snapshot ids --- .../kibana/public/dashboard/dashboard_app.js | 4 +- src/ui/public/share/components/share_menu.js | 125 +++++++++++++----- .../share/components/share_url_content.js | 40 ++++++ src/ui/public/share/show_share_menu.js | 5 +- 4 files changed, 137 insertions(+), 37 deletions(-) create mode 100644 src/ui/public/share/components/share_url_content.js diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_app.js b/src/core_plugins/kibana/public/dashboard/dashboard_app.js index 8fbab0aa18212..00a0c568ce522 100644 --- a/src/core_plugins/kibana/public/dashboard/dashboard_app.js +++ b/src/core_plugins/kibana/public/dashboard/dashboard_app.js @@ -397,9 +397,9 @@ app.directive('dashboardApp', function ($injector) { navActions[TopNavIds.SHARE] = (menuItem, navController, anchorElement) => { showShareMenu({ anchorElement, - objectType: 'dashboard', - objectId: dash.id, getUnhashableStates, + objectId: dash.id, + objectType: 'dashboard', }); }; diff --git a/src/ui/public/share/components/share_menu.js b/src/ui/public/share/components/share_menu.js index 87d30b096c469..8b0459419e04c 100644 --- a/src/ui/public/share/components/share_menu.js +++ b/src/ui/public/share/components/share_menu.js @@ -24,56 +24,113 @@ import { EuiContextMenu, } from '@elastic/eui'; +import { + parse as parseUrl, + format as formatUrl, +} from 'url'; + +import { unhashUrl } from '../../state_management/state_hashing'; + +import { ShareUrlContent } from './share_url_content'; + export class ShareMenu extends Component { + constructor(props) { + super(props); - state = { + this.state = { + panels: [ + { + id: 0, + title: `Share this ${this.props.objectType}`, + items: [{ + name: 'Embed code', + icon: 'console', + panel: 1 + }, { + name: 'Permalinks', + icon: 'link', + panel: 2 + }], + }, + { + id: 1, + title: 'Embed Code', + content: ( + + ) + }, + { + id: 2, + title: 'Permalink', + content: ( + + ) + } + ] + }; } - panels = [ - { - id: 0, - title: `Share this ${this.props.objectType}`, - items: [{ - name: 'Embed code', - icon: 'console', - panel: 1 - }, { - name: 'Permalinks', - icon: 'link', - panel: 2 - }], - }, - { - id: 1, - title: 'Embed Code', - content: ( -
- embed code content goes here -
- ) - }, - { - id: 2, - title: 'Permalink', - content: ( -
- Permalink content goes here -
- ) + getOriginalUrl = () => { + const { + objectId, + getUnhashableStates, + } = this.props; + + // If there is no objectId, then it isn't saved, so it has no original URL. + if (objectId === undefined || objectId === '') { + return; } - ]; + + const url = window.location.href; + // Replace hashes with original RISON values. + const unhashedUrl = unhashUrl(url, getUnhashableStates()); + + const parsedUrl = parseUrl(unhashedUrl); + // Get the application route, after the hash, and remove the #. + const parsedAppUrl = parseUrl(parsedUrl.hash.slice(1), true); + + return formatUrl({ + protocol: parsedUrl.protocol, + auth: parsedUrl.auth, + host: parsedUrl.host, + pathname: parsedUrl.pathname, + hash: formatUrl({ + pathname: parsedAppUrl.pathname, + query: { + // Add global state to the URL so that the iframe doesn't just show the time range + // default. + _g: parsedAppUrl.query._g, + }, + }), + }); + } + + getSnapshotUrl = () => { + const { getUnhashableStates } = this.props; + + const url = window.location.href; + // Replace hashes with original RISON values. + return unhashUrl(url, getUnhashableStates()); + } render() { return ( ); } } ShareMenu.propTypes = { + objectId: PropTypes.string, objectType: PropTypes.string.isRequired, getUnhashableStates: PropTypes.func.isRequired, }; diff --git a/src/ui/public/share/components/share_url_content.js b/src/ui/public/share/components/share_url_content.js new file mode 100644 index 0000000000000..9077623c2d524 --- /dev/null +++ b/src/ui/public/share/components/share_url_content.js @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import PropTypes from 'prop-types'; +import React from 'react'; + +export function ShareUrlContent(props) { + const { + getOriginalUrl, + getSnapshotUrl, + } = props; + + return ( +
+

{getOriginalUrl()}

+

{getSnapshotUrl()}

+
+ ); +} + +ShareUrlContent.propTypes = { + getOriginalUrl: PropTypes.func.isRequired, + getSnapshotUrl: PropTypes.func.isRequired, +}; diff --git a/src/ui/public/share/show_share_menu.js b/src/ui/public/share/show_share_menu.js index 94a04dc9811bb..fb77233af3b10 100644 --- a/src/ui/public/share/show_share_menu.js +++ b/src/ui/public/share/show_share_menu.js @@ -37,8 +37,9 @@ const onClose = () => { export function showShareMenu({ anchorElement, - objectType, + getUnhashableStates, objectId, + objectType, }) { if (isOpen) { onClose(); @@ -58,6 +59,8 @@ export function showShareMenu({ withTitle > From 8216b6e9d18421f61b04e9571d90a630bee36ada Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 10 Aug 2018 11:54:08 -0600 Subject: [PATCH 04/28] move state into ShareUrlContent --- .../kibana/public/dashboard/dashboard_app.js | 4 +- .../{share_menu.js => share_context_menu.js} | 62 +---------- .../share/components/share_url_content.js | 104 +++++++++++++++--- ...are_menu.js => show_share_context_menu.js} | 6 +- 4 files changed, 99 insertions(+), 77 deletions(-) rename src/ui/public/share/components/{share_menu.js => share_context_menu.js} (54%) rename src/ui/public/share/{show_share_menu.js => show_share_context_menu.js} (92%) diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_app.js b/src/core_plugins/kibana/public/dashboard/dashboard_app.js index 00a0c568ce522..a9e9711965f7a 100644 --- a/src/core_plugins/kibana/public/dashboard/dashboard_app.js +++ b/src/core_plugins/kibana/public/dashboard/dashboard_app.js @@ -43,7 +43,7 @@ import { showCloneModal } from './top_nav/show_clone_modal'; import { showSaveModal } from './top_nav/show_save_modal'; import { showAddPanel } from './top_nav/show_add_panel'; import { showOptionsPopover } from './top_nav/show_options_popover'; -import { showShareMenu } from 'ui/share/show_share_menu'; +import { showShareContextMenu } from 'ui/share/show_share_context_menu'; import { migrateLegacyQuery } from 'ui/utils/migrateLegacyQuery'; import * as filterActions from 'ui/doc_table/actions/filter'; import { FilterManagerProvider } from 'ui/filter_manager'; @@ -395,7 +395,7 @@ app.directive('dashboardApp', function ($injector) { }); }; navActions[TopNavIds.SHARE] = (menuItem, navController, anchorElement) => { - showShareMenu({ + showShareContextMenu({ anchorElement, getUnhashableStates, objectId: dash.id, diff --git a/src/ui/public/share/components/share_menu.js b/src/ui/public/share/components/share_context_menu.js similarity index 54% rename from src/ui/public/share/components/share_menu.js rename to src/ui/public/share/components/share_context_menu.js index 8b0459419e04c..9228187c803bb 100644 --- a/src/ui/public/share/components/share_menu.js +++ b/src/ui/public/share/components/share_context_menu.js @@ -24,16 +24,9 @@ import { EuiContextMenu, } from '@elastic/eui'; -import { - parse as parseUrl, - format as formatUrl, -} from 'url'; - -import { unhashUrl } from '../../state_management/state_hashing'; - import { ShareUrlContent } from './share_url_content'; -export class ShareMenu extends Component { +export class ShareContextMenu extends Component { constructor(props) { super(props); @@ -57,8 +50,8 @@ export class ShareMenu extends Component { title: 'Embed Code', content: ( ) }, @@ -67,8 +60,8 @@ export class ShareMenu extends Component { title: 'Permalink', content: ( ) } @@ -76,49 +69,6 @@ export class ShareMenu extends Component { }; } - getOriginalUrl = () => { - const { - objectId, - getUnhashableStates, - } = this.props; - - // If there is no objectId, then it isn't saved, so it has no original URL. - if (objectId === undefined || objectId === '') { - return; - } - - const url = window.location.href; - // Replace hashes with original RISON values. - const unhashedUrl = unhashUrl(url, getUnhashableStates()); - - const parsedUrl = parseUrl(unhashedUrl); - // Get the application route, after the hash, and remove the #. - const parsedAppUrl = parseUrl(parsedUrl.hash.slice(1), true); - - return formatUrl({ - protocol: parsedUrl.protocol, - auth: parsedUrl.auth, - host: parsedUrl.host, - pathname: parsedUrl.pathname, - hash: formatUrl({ - pathname: parsedAppUrl.pathname, - query: { - // Add global state to the URL so that the iframe doesn't just show the time range - // default. - _g: parsedAppUrl.query._g, - }, - }), - }); - } - - getSnapshotUrl = () => { - const { getUnhashableStates } = this.props; - - const url = window.location.href; - // Replace hashes with original RISON values. - return unhashUrl(url, getUnhashableStates()); - } - render() { return ( -

{getOriginalUrl()}

-

{getSnapshotUrl()}

- - ); +import React, { Component } from 'react'; + +import { + parse as parseUrl, + format as formatUrl, +} from 'url'; + +import { unhashUrl } from '../../state_management/state_hashing'; + +export class ShareUrlContent extends Component { + + state = {}; + + componentWillUnmount() { + window.removeEventListener('hashchange', this.resetShortUrls); + + this._isMounted = false; + } + + componentDidMount() { + this._isMounted = true; + this.resetShortUrls(); + + window.addEventListener('hashchange', this.resetShortUrls, false); + } + + resetShortUrls = () => { + if (this._isMounted) { + this.setState({ + shortSnanshotUrl: undefined, + shortOriginalUrl: undefined, + }); + } + } + + getOriginalUrl = () => { + const { + objectId, + getUnhashableStates, + } = this.props; + + // If there is no objectId, then it isn't saved, so it has no original URL. + if (objectId === undefined || objectId === '') { + return; + } + + const url = window.location.href; + // Replace hashes with original RISON values. + const unhashedUrl = unhashUrl(url, getUnhashableStates()); + + const parsedUrl = parseUrl(unhashedUrl); + // Get the application route, after the hash, and remove the #. + const parsedAppUrl = parseUrl(parsedUrl.hash.slice(1), true); + + return formatUrl({ + protocol: parsedUrl.protocol, + auth: parsedUrl.auth, + host: parsedUrl.host, + pathname: parsedUrl.pathname, + hash: formatUrl({ + pathname: parsedAppUrl.pathname, + query: { + // Add global state to the URL so that the iframe doesn't just show the time range + // default. + _g: parsedAppUrl.query._g, + }, + }), + }); + } + + getSnapshotUrl = () => { + const { getUnhashableStates } = this.props; + + const url = window.location.href; + // Replace hashes with original RISON values. + return unhashUrl(url, getUnhashableStates()); + } + + render() { + return ( +
+

{this.getOriginalUrl()}

+

{this.getSnapshotUrl()}

+
+ ); + } } ShareUrlContent.propTypes = { - getOriginalUrl: PropTypes.func.isRequired, - getSnapshotUrl: PropTypes.func.isRequired, + objectId: PropTypes.string, + getUnhashableStates: PropTypes.func.isRequired, }; diff --git a/src/ui/public/share/show_share_menu.js b/src/ui/public/share/show_share_context_menu.js similarity index 92% rename from src/ui/public/share/show_share_menu.js rename to src/ui/public/share/show_share_context_menu.js index fb77233af3b10..f11946cec64e3 100644 --- a/src/ui/public/share/show_share_menu.js +++ b/src/ui/public/share/show_share_context_menu.js @@ -20,7 +20,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { ShareMenu } from './components/share_menu'; +import { ShareContextMenu } from './components/share_context_menu'; import { EuiWrappingPopover, @@ -35,7 +35,7 @@ const onClose = () => { isOpen = false; }; -export function showShareMenu({ +export function showShareContextMenu({ anchorElement, getUnhashableStates, objectId, @@ -58,7 +58,7 @@ export function showShareMenu({ panelPaddingSize="none" withTitle > - Date: Fri, 10 Aug 2018 13:12:15 -0600 Subject: [PATCH 05/28] start working on form --- .../share/components/share_context_menu.js | 3 + .../share/components/share_url_content.js | 84 ++++++++++++++----- .../share/components/share_url_content.less | 3 + 3 files changed, 70 insertions(+), 20 deletions(-) create mode 100644 src/ui/public/share/components/share_url_content.less diff --git a/src/ui/public/share/components/share_context_menu.js b/src/ui/public/share/components/share_context_menu.js index 9228187c803bb..cf2e598ef44e0 100644 --- a/src/ui/public/share/components/share_context_menu.js +++ b/src/ui/public/share/components/share_context_menu.js @@ -50,7 +50,9 @@ export class ShareContextMenu extends Component { title: 'Embed Code', content: ( ) @@ -61,6 +63,7 @@ export class ShareContextMenu extends Component { content: ( ) diff --git a/src/ui/public/share/components/share_url_content.js b/src/ui/public/share/components/share_url_content.js index 1cd62de359efd..2e38986617336 100644 --- a/src/ui/public/share/components/share_url_content.js +++ b/src/ui/public/share/components/share_url_content.js @@ -17,9 +17,21 @@ * under the License. */ +import './share_url_content.less'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { + EuiForm, + EuiFormRow, + EuiSwitch, + EuiButton, + EuiIconTip, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; + + import { parse as parseUrl, format as formatUrl, @@ -29,7 +41,9 @@ import { unhashUrl } from '../../state_management/state_hashing'; export class ShareUrlContent extends Component { - state = {}; + state = { + shareSnapshot: true, + }; componentWillUnmount() { window.removeEventListener('hashchange', this.resetShortUrls); @@ -46,27 +60,22 @@ export class ShareUrlContent extends Component { resetShortUrls = () => { if (this._isMounted) { - this.setState({ - shortSnanshotUrl: undefined, - shortOriginalUrl: undefined, - }); + this.setState({ shortSnanshotUrl: undefined }); } } - getOriginalUrl = () => { - const { - objectId, - getUnhashableStates, - } = this.props; + hasSavedObjectUrl = () => { + return this.props.objectId === undefined || this.props.objectId === ''; + } - // If there is no objectId, then it isn't saved, so it has no original URL. - if (objectId === undefined || objectId === '') { + getSavedObjectUrl = () => { + if (!this.hasSavedObjectUrl()) { return; } const url = window.location.href; // Replace hashes with original RISON values. - const unhashedUrl = unhashUrl(url, getUnhashableStates()); + const unhashedUrl = unhashUrl(url, this.props.getUnhashableStates()); const parsedUrl = parseUrl(unhashedUrl); // Get the application route, after the hash, and remove the #. @@ -89,24 +98,59 @@ export class ShareUrlContent extends Component { } getSnapshotUrl = () => { - const { getUnhashableStates } = this.props; - const url = window.location.href; // Replace hashes with original RISON values. - return unhashUrl(url, getUnhashableStates()); + return unhashUrl(url, this.props.getUnhashableStates()); + } + + handleShareSnapshotChange = (evt) => { + this.setState({ shareSnapshot: evt.target.checked }); + } + + renderShareSnapshot = () => { + return ( + + + + + + + + + + + ); } render() { return ( -
-

{this.getOriginalUrl()}

-

{this.getSnapshotUrl()}

-
+ + + {this.renderShareSnapshot()} + + window.alert('Button clicked')} + > + Copy { this.props.isEmbedded ? 'iFrame code' : 'link' } + + + ); } } ShareUrlContent.propTypes = { + isEmbedded: PropTypes.bool, objectId: PropTypes.string, + objectType: PropTypes.string.isRequired, getUnhashableStates: PropTypes.func.isRequired, }; diff --git a/src/ui/public/share/components/share_url_content.less b/src/ui/public/share/components/share_url_content.less new file mode 100644 index 0000000000000..95b950e5b0e94 --- /dev/null +++ b/src/ui/public/share/components/share_url_content.less @@ -0,0 +1,3 @@ +.shareUrlContentForm{ + padding: 16px; +} From adacc967eb38e0b898ab6cc9e6da9bc8767563d4 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 10 Aug 2018 14:50:18 -0600 Subject: [PATCH 06/28] use radio group --- .../share/components/share_url_content.js | 85 +++++++++++++------ 1 file changed, 59 insertions(+), 26 deletions(-) diff --git a/src/ui/public/share/components/share_url_content.js b/src/ui/public/share/components/share_url_content.js index 2e38986617336..ad199bdf0ea99 100644 --- a/src/ui/public/share/components/share_url_content.js +++ b/src/ui/public/share/components/share_url_content.js @@ -29,6 +29,7 @@ import { EuiIconTip, EuiFlexGroup, EuiFlexItem, + EuiRadioGroup, } from '@elastic/eui'; @@ -39,11 +40,14 @@ import { import { unhashUrl } from '../../state_management/state_hashing'; +const RADIO_SAVED_OBJECT_ID = 'savedObject'; +const RADIO_SNAPSHOT_ID = 'snapshot'; + export class ShareUrlContent extends Component { state = { - shareSnapshot: true, - }; + radioIdSelected: RADIO_SNAPSHOT_ID, + } componentWillUnmount() { window.removeEventListener('hashchange', this.resetShortUrls); @@ -64,12 +68,12 @@ export class ShareUrlContent extends Component { } } - hasSavedObjectUrl = () => { + isNotSaved = () => { return this.props.objectId === undefined || this.props.objectId === ''; } getSavedObjectUrl = () => { - if (!this.hasSavedObjectUrl()) { + if (this.isNotSaved()) { return; } @@ -103,38 +107,67 @@ export class ShareUrlContent extends Component { return unhashUrl(url, this.props.getUnhashableStates()); } - handleShareSnapshotChange = (evt) => { - this.setState({ shareSnapshot: evt.target.checked }); + handleRadioChange = optionId => { + this.setState({ + radioIdSelected: optionId, + }); + }; + + renderRadioOptions = () => { + return [ + { + id: RADIO_SAVED_OBJECT_ID, + disabled: this.isNotSaved(), + label: this.renderRadio( + 'Saved object', + `You can share this URL with people to let them load the most recent saved version of this ${this.props.objectType}.` + ), + }, + { + id: RADIO_SNAPSHOT_ID, + label: this.renderRadio( + 'Snapshot', + `Snapshot URLs encode the current state of the ${this.props.objectType} in the URL itself. + Edits to the saved ${this.props.objectType} won't be visible via this URL.` + ), + } + ]; } - renderShareSnapshot = () => { + renderRadio = (label, tipContent) => { return ( - - - - - - - - - - + + + {label} + + + + + ); } render() { + const generateLinkAsHelp = this.isNotSaved() + ? `Can't share as saved object until the ${this.props.objectType} has been saved.` + : undefined; + return ( - {this.renderShareSnapshot()} + + + Date: Mon, 13 Aug 2018 09:11:23 -0600 Subject: [PATCH 07/28] add input for creating short URL --- .../share/components/share_url_content.js | 94 ++++++++++++++----- 1 file changed, 70 insertions(+), 24 deletions(-) diff --git a/src/ui/public/share/components/share_url_content.js b/src/ui/public/share/components/share_url_content.js index ad199bdf0ea99..1c31259341b8a 100644 --- a/src/ui/public/share/components/share_url_content.js +++ b/src/ui/public/share/components/share_url_content.js @@ -40,13 +40,15 @@ import { import { unhashUrl } from '../../state_management/state_hashing'; -const RADIO_SAVED_OBJECT_ID = 'savedObject'; -const RADIO_SNAPSHOT_ID = 'snapshot'; +const EXPORT_URL_AS_SAVED_OBJECT = 'savedObject'; +const EXPORT_URL_AS_SNAPSHOT = 'snapshot'; export class ShareUrlContent extends Component { state = { - radioIdSelected: RADIO_SNAPSHOT_ID, + exportUrlAs: EXPORT_URL_AS_SNAPSHOT, + useShortUrl: false, + isCreatingShortUrl: false, } componentWillUnmount() { @@ -64,10 +66,16 @@ export class ShareUrlContent extends Component { resetShortUrls = () => { if (this._isMounted) { - this.setState({ shortSnanshotUrl: undefined }); + this.setState({ shortUrl: undefined }); } } + createShortUrl = async () => { + this.setState({ isCreatingShortUrl: true }); + + // TODO create short URL + } + isNotSaved = () => { return this.props.objectId === undefined || this.props.objectId === ''; } @@ -109,23 +117,23 @@ export class ShareUrlContent extends Component { handleRadioChange = optionId => { this.setState({ - radioIdSelected: optionId, + exportUrlAs: optionId, }); }; renderRadioOptions = () => { return [ { - id: RADIO_SAVED_OBJECT_ID, + id: EXPORT_URL_AS_SAVED_OBJECT, disabled: this.isNotSaved(), - label: this.renderRadio( + label: this.renderWithIconTip( 'Saved object', `You can share this URL with people to let them load the most recent saved version of this ${this.props.objectType}.` ), }, { - id: RADIO_SNAPSHOT_ID, - label: this.renderRadio( + id: EXPORT_URL_AS_SNAPSHOT, + label: this.renderWithIconTip( 'Snapshot', `Snapshot URLs encode the current state of the ${this.props.objectType} in the URL itself. Edits to the saved ${this.props.objectType} won't be visible via this URL.` @@ -134,11 +142,11 @@ export class ShareUrlContent extends Component { ]; } - renderRadio = (label, tipContent) => { + renderWithIconTip = (child, tipContent) => { return ( - - - {label} + + + {child} { const generateLinkAsHelp = this.isNotSaved() ? `Can't share as saved object until the ${this.props.objectType} has been saved.` : undefined; + return ( + + + + ); + } + + renderShortUrlSwitch = () => { + if (this.state.exportUrlAs === EXPORT_URL_AS_SAVED_OBJECT) { + return; + } + const switchComponent = (); + const tipContent = `We recommend sharing shortened snapshot URLs for maximum compatibility. + Internet Explorer has URL length restrictions, + and some wiki and markup parsers don't do well with the full-length version of the snapshot URL, + but the short URL should work great.`; + return ( + + {this.renderWithIconTip(switchComponent, tipContent)} + + ); + } + + handleShortUrlChange = evt => { + if (this.state.shortUrl === undefined) { + this.createShortUrl(); + } + + this.setState({ + useShortUrl: evt.target.checked, + }); + } + + render() { return ( - - - + {this.renderExportAsRadioGroup()} + + {this.renderShortUrlSwitch()} Date: Mon, 13 Aug 2018 11:27:40 -0600 Subject: [PATCH 08/28] display URL in alert until copy functionallity gets migrated to EUI --- .../share/components/share_url_content.js | 129 +++++++++++++----- 1 file changed, 97 insertions(+), 32 deletions(-) diff --git a/src/ui/public/share/components/share_url_content.js b/src/ui/public/share/components/share_url_content.js index 1c31259341b8a..714785f1404a3 100644 --- a/src/ui/public/share/components/share_url_content.js +++ b/src/ui/public/share/components/share_url_content.js @@ -30,6 +30,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiRadioGroup, + EuiLoadingSpinner, } from '@elastic/eui'; @@ -39,45 +40,62 @@ import { } from 'url'; import { unhashUrl } from '../../state_management/state_hashing'; +import { shortenUrl } from '../lib/url_shortener'; const EXPORT_URL_AS_SAVED_OBJECT = 'savedObject'; const EXPORT_URL_AS_SNAPSHOT = 'snapshot'; export class ShareUrlContent extends Component { - state = { - exportUrlAs: EXPORT_URL_AS_SNAPSHOT, - useShortUrl: false, - isCreatingShortUrl: false, + constructor(props) { + super(props); + + this.state = { + exportUrlAs: EXPORT_URL_AS_SNAPSHOT, + useShortUrl: false, + isCreatingShortUrl: false, + url: this.getUrl(EXPORT_URL_AS_SNAPSHOT, false), + }; } componentWillUnmount() { - window.removeEventListener('hashchange', this.resetShortUrls); + window.removeEventListener('hashchange', this.resetUrl); this._isMounted = false; } componentDidMount() { this._isMounted = true; - this.resetShortUrls(); - window.addEventListener('hashchange', this.resetShortUrls, false); + window.addEventListener('hashchange', this.resetUrl, false); + } + + isNotSaved = () => { + return this.props.objectId === undefined || this.props.objectId === ''; } - resetShortUrls = () => { + resetUrl = () => { if (this._isMounted) { - this.setState({ shortUrl: undefined }); + this.setState({ + useShortUrl: false, + shortUrl: undefined, + url: undefined, + }, this.setUrl); } } - createShortUrl = async () => { + getShortUrl = async () => { this.setState({ isCreatingShortUrl: true }); - // TODO create short URL - } + const shortUrl = await shortenUrl(this.getSnapshotUrl()); + if (!this._isMounted) { + return; + } - isNotSaved = () => { - return this.props.objectId === undefined || this.props.objectId === ''; + this.setState({ + shortUrl, + isCreatingShortUrl: false, + }); } getSavedObjectUrl = () => { @@ -115,13 +133,63 @@ export class ShareUrlContent extends Component { return unhashUrl(url, this.props.getUnhashableStates()); } - handleRadioChange = optionId => { + makeUrlEmbeddable = url => { + const embedQueryParam = '?embed=true'; + const urlHasQueryString = url.indexOf('?') !== -1; + if (urlHasQueryString) { + return url.replace('?', `${embedQueryParam}&`); + } + return `${url}${embedQueryParam}`; + } + + makeIframeTag = url => { + if (!url) return; + + const embeddableUrl = this.makeUrlEmbeddable(url); + return ``; + } + + getUrl = (exportUrlAs, useShortUrl) => { + let url; + if (exportUrlAs === EXPORT_URL_AS_SAVED_OBJECT) { + url = this.getSavedObjectUrl(); + } else if (useShortUrl) { + url = this.state.shortUrl; + } else { + url = this.getSnapshotUrl(); + } + + if (this.props.isEmbedded) { + return this.makeIframeTag(url); + } + + return url; + } + + setUrl = () => { this.setState({ - exportUrlAs: optionId, + url: this.getUrl(this.state.exportUrlAs, this.state.useShortUrl) }); - }; + } + + handleExportUrlAs = optionId => { + this.setState({ + exportUrlAs: optionId, + }, this.setUrl); + } + + handleShortUrlChange = async evt => { + const isChecked = evt.target.checked; + if (this.state.shortUrl === undefined && isChecked) { + await this.getShortUrl(); + } - renderRadioOptions = () => { + this.setState({ + useShortUrl: isChecked, + }, this.setUrl); + } + + renderExportUrlAsOptions = () => { return [ { id: EXPORT_URL_AS_SAVED_OBJECT, @@ -168,9 +236,9 @@ export class ShareUrlContent extends Component { helpText={generateLinkAsHelp} > ); @@ -190,23 +258,19 @@ export class ShareUrlContent extends Component { Internet Explorer has URL length restrictions, and some wiki and markup parsers don't do well with the full-length version of the snapshot URL, but the short URL should work great.`; + let loadingShortUrl; + if (this.state.isCreatingShortUrl) { + loadingShortUrl = ( + + ); + } return ( - + {this.renderWithIconTip(switchComponent, tipContent)} ); } - handleShortUrlChange = evt => { - if (this.state.shortUrl === undefined) { - this.createShortUrl(); - } - - this.setState({ - useShortUrl: evt.target.checked, - }); - } - render() { return ( @@ -217,7 +281,8 @@ export class ShareUrlContent extends Component { window.alert('Button clicked')} + onClick={() => window.alert(this.state.url)} + disabled={this.state.isCreatingShortUrl} > Copy { this.props.isEmbedded ? 'iFrame code' : 'link' } From fccaa2aafe0f087859e145dcb2a752a20de08aa8 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 13 Aug 2018 16:29:40 -0600 Subject: [PATCH 09/28] allowEmbed prop --- .../kibana/public/dashboard/dashboard_app.js | 1 + .../share/components/share_context_menu.js | 105 +++++++++++------- .../public/share/show_share_context_menu.js | 2 + 3 files changed, 65 insertions(+), 43 deletions(-) diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_app.js b/src/core_plugins/kibana/public/dashboard/dashboard_app.js index a9e9711965f7a..1dee6962f53a6 100644 --- a/src/core_plugins/kibana/public/dashboard/dashboard_app.js +++ b/src/core_plugins/kibana/public/dashboard/dashboard_app.js @@ -397,6 +397,7 @@ app.directive('dashboardApp', function ($injector) { navActions[TopNavIds.SHARE] = (menuItem, navController, anchorElement) => { showShareContextMenu({ anchorElement, + allowEmbed: true, getUnhashableStates, objectId: dash.id, objectType: 'dashboard', diff --git a/src/ui/public/share/components/share_context_menu.js b/src/ui/public/share/components/share_context_menu.js index cf2e598ef44e0..f15bddf531df0 100644 --- a/src/ui/public/share/components/share_context_menu.js +++ b/src/ui/public/share/components/share_context_menu.js @@ -27,62 +27,81 @@ import { import { ShareUrlContent } from './share_url_content'; export class ShareContextMenu extends Component { - constructor(props) { - super(props); - this.state = { - panels: [ - { - id: 0, - title: `Share this ${this.props.objectType}`, - items: [{ - name: 'Embed code', - icon: 'console', - panel: 1 - }, { - name: 'Permalinks', - icon: 'link', - panel: 2 - }], - }, - { - id: 1, - title: 'Embed Code', - content: ( - - ) - }, - { - id: 2, - title: 'Permalink', - content: ( - - ) - } - ] + getPanels = () => { + const panels = []; + const menuItems = []; + + const permalinkPanel = { + id: panels.length + 1, + title: 'Permalink', + content: ( + + ) }; + menuItems.push({ + name: 'Permalinks', + icon: 'link', + panel: permalinkPanel.id + }); + panels.push(permalinkPanel); + + if (this.props.allowEmbed) { + const embedPanel = { + id: panels.length + 1, + title: 'Embed Code', + content: ( + + ) + }; + panels.push(embedPanel); + menuItems.push({ + name: 'Embed code', + icon: 'console', + panel: embedPanel.id + }); + } + + // TODO add plugable panels here + + if (menuItems.length > 1) { + const topLevelMenuPanel = { + id: panels.length + 1, + title: `Share this ${this.props.objectType}`, + items: menuItems.sort((a, b) => { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }), + }; + panels.push(topLevelMenuPanel); + } + + const lastPanelIndex = panels.length - 1; + const initialPanelId = panels[lastPanelIndex].id; + return { panels, initialPanelId }; } render() { + const { panels, initialPanelId } = this.getPanels(); return ( ); } } ShareContextMenu.propTypes = { + allowEmbed: PropTypes.bool.isRequired, objectId: PropTypes.string, objectType: PropTypes.string.isRequired, getUnhashableStates: PropTypes.func.isRequired, diff --git a/src/ui/public/share/show_share_context_menu.js b/src/ui/public/share/show_share_context_menu.js index f11946cec64e3..90813cb414785 100644 --- a/src/ui/public/share/show_share_context_menu.js +++ b/src/ui/public/share/show_share_context_menu.js @@ -37,6 +37,7 @@ const onClose = () => { export function showShareContextMenu({ anchorElement, + allowEmbed, getUnhashableStates, objectId, objectType, @@ -59,6 +60,7 @@ export function showShareContextMenu({ withTitle > Date: Tue, 14 Aug 2018 08:19:40 -0600 Subject: [PATCH 10/28] replace share directive with showShareContextMenu --- .../kibana/public/dashboard/dashboard_app.js | 2 +- .../public/discover/controllers/discover.js | 22 +- .../discover/partials/share_search.html | 5 - .../kibana/public/visualize/editor/editor.js | 16 +- .../public/visualize/editor/panels/share.html | 4 - src/ui/public/share/directives/share.js | 198 --------------- src/ui/public/share/index.js | 2 +- src/ui/public/share/views/share.html | 238 ------------------ 8 files changed, 31 insertions(+), 456 deletions(-) delete mode 100644 src/core_plugins/kibana/public/discover/partials/share_search.html delete mode 100644 src/core_plugins/kibana/public/visualize/editor/panels/share.html delete mode 100644 src/ui/public/share/directives/share.js delete mode 100644 src/ui/public/share/views/share.html diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_app.js b/src/core_plugins/kibana/public/dashboard/dashboard_app.js index 1dee6962f53a6..6869676c7b3b3 100644 --- a/src/core_plugins/kibana/public/dashboard/dashboard_app.js +++ b/src/core_plugins/kibana/public/dashboard/dashboard_app.js @@ -43,7 +43,7 @@ import { showCloneModal } from './top_nav/show_clone_modal'; import { showSaveModal } from './top_nav/show_save_modal'; import { showAddPanel } from './top_nav/show_add_panel'; import { showOptionsPopover } from './top_nav/show_options_popover'; -import { showShareContextMenu } from 'ui/share/show_share_context_menu'; +import { showShareContextMenu } from 'ui/share'; import { migrateLegacyQuery } from 'ui/utils/migrateLegacyQuery'; import * as filterActions from 'ui/doc_table/actions/filter'; import { FilterManagerProvider } from 'ui/filter_manager'; diff --git a/src/core_plugins/kibana/public/discover/controllers/discover.js b/src/core_plugins/kibana/public/discover/controllers/discover.js index 914cb67a88993..ac008303c53b3 100644 --- a/src/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/core_plugins/kibana/public/discover/controllers/discover.js @@ -31,7 +31,6 @@ import 'ui/filters/moment'; import 'ui/index_patterns'; import 'ui/state_management/app_state'; import { timefilter } from 'ui/timefilter'; -import 'ui/share'; import 'ui/query_bar'; import { hasSearchStategyForIndexPattern, isDefaultTypeIndexPattern } from 'ui/courier'; import { toastNotifications } from 'ui/notify'; @@ -54,6 +53,8 @@ import { recentlyAccessed } from 'ui/persisted_log'; import { getDocLink } from 'ui/documentation_links'; import '../components/fetch_error'; import { getPainlessError } from './get_painless_error'; +import { showShareContextMenu } from 'ui/share'; +import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing'; const app = uiModules.get('apps/discover', [ 'kibana/notify', @@ -157,6 +158,7 @@ function discoverController( const notify = new Notifier({ location: 'Discover' }); + const getUnhashableStates = Private(getUnhashableStatesProvider); $scope.getDocLink = getDocLink; $scope.intervalOptions = intervalOptions; @@ -167,6 +169,10 @@ function discoverController( return interval.val !== 'custom'; }; + // the saved savedSearch + const savedSearch = $route.current.locals.savedSearch; + $scope.$on('$destroy', savedSearch.destroy); + $scope.topNavMenu = [{ key: 'new', description: 'New Search', @@ -185,14 +191,18 @@ function discoverController( }, { key: 'share', description: 'Share Search', - template: require('plugins/kibana/discover/partials/share_search.html'), testId: 'discoverShareButton', + run: (menuItem, navController, anchorElement) => { + showShareContextMenu({ + anchorElement, + allowEmbed: false, + getUnhashableStates, + objectId: savedSearch.id, + objectType: 'search', + }); + } }]; - // the saved savedSearch - const savedSearch = $route.current.locals.savedSearch; - $scope.$on('$destroy', savedSearch.destroy); - // the actual courier.SearchSource $scope.searchSource = savedSearch.searchSource; $scope.indexPattern = resolveIndexPatternLoading(); diff --git a/src/core_plugins/kibana/public/discover/partials/share_search.html b/src/core_plugins/kibana/public/discover/partials/share_search.html deleted file mode 100644 index 69fee7ad756d0..0000000000000 --- a/src/core_plugins/kibana/public/discover/partials/share_search.html +++ /dev/null @@ -1,5 +0,0 @@ - - diff --git a/src/core_plugins/kibana/public/visualize/editor/editor.js b/src/core_plugins/kibana/public/visualize/editor/editor.js index b7507bf857cf2..55ee15861c375 100644 --- a/src/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/core_plugins/kibana/public/visualize/editor/editor.js @@ -23,7 +23,6 @@ import './visualization_editor'; import 'ui/vis/editors/default/sidebar'; import 'ui/visualize'; import 'ui/collapsible_sidebar'; -import 'ui/share'; import 'ui/query_bar'; import chrome from 'ui/chrome'; import angular from 'angular'; @@ -43,6 +42,8 @@ import { migrateLegacyQuery } from 'ui/utils/migrateLegacyQuery'; import { recentlyAccessed } from 'ui/persisted_log'; import { timefilter } from 'ui/timefilter'; import { getVisualizeLoader } from '../../../../../ui/public/visualize/loader'; +import { showShareContextMenu } from 'ui/share'; +import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing'; uiRoutes .when(VisualizeConstants.CREATE_PATH, { @@ -115,6 +116,7 @@ function VisEditor( ) { const docTitle = Private(DocTitleProvider); const queryFilter = Private(FilterBarQueryFilterProvider); + const getUnhashableStates = Private(getUnhashableStatesProvider); const notify = new Notifier({ location: 'Visualization Editor' @@ -156,8 +158,16 @@ function VisEditor( }, { key: 'share', description: 'Share Visualization', - template: require('plugins/kibana/visualize/editor/panels/share.html'), testId: 'visualizeShareButton', + run: (menuItem, navController, anchorElement) => { + showShareContextMenu({ + anchorElement, + allowEmbed: true, + getUnhashableStates, + objectId: savedVis.id, + objectType: 'visualization', + }); + } }, { key: 'inspect', description: 'Open Inspector for visualization', @@ -251,7 +261,7 @@ function VisEditor( $scope.isAddToDashMode = () => addToDashMode; $scope.timeRange = timefilter.getTime(); - $scope.opts = _.pick($scope, 'doSave', 'savedVis', 'shareData', 'isAddToDashMode'); + $scope.opts = _.pick($scope, 'doSave', 'savedVis', 'isAddToDashMode'); stateMonitor = stateMonitorFactory.create($state, stateDefaults); stateMonitor.ignoreProps([ 'vis.listeners' ]).onChange((status) => { diff --git a/src/core_plugins/kibana/public/visualize/editor/panels/share.html b/src/core_plugins/kibana/public/visualize/editor/panels/share.html deleted file mode 100644 index 1eeaf5afa608e..0000000000000 --- a/src/core_plugins/kibana/public/visualize/editor/panels/share.html +++ /dev/null @@ -1,4 +0,0 @@ - - diff --git a/src/ui/public/share/directives/share.js b/src/ui/public/share/directives/share.js deleted file mode 100644 index 9aada8b1dacd1..0000000000000 --- a/src/ui/public/share/directives/share.js +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { - parse as parseUrl, - format as formatUrl, -} from 'url'; - -import { - getUnhashableStatesProvider, - unhashUrl, -} from '../../state_management/state_hashing'; -import { toastNotifications } from '../../notify'; - -import { shortenUrl } from '../lib/url_shortener'; - -import { uiModules } from '../../modules'; -import shareTemplate from '../views/share.html'; -const app = uiModules.get('kibana'); - -app.directive('share', function (Private) { - const getUnhashableStates = Private(getUnhashableStatesProvider); - - return { - restrict: 'E', - scope: { - objectType: '@', - objectId: '@', - allowEmbed: '@', - }, - template: shareTemplate, - controllerAs: 'share', - controller: function ($scope, $document, $location) { - if ($scope.allowEmbed !== 'false' && $scope.allowEmbed !== undefined) { - throw new Error('allowEmbed must be "false" or undefined'); - } - - // Default to allowing an embedded IFRAME, unless it's explicitly set to false. - this.allowEmbed = $scope.allowEmbed === 'false' ? false : true; - this.objectType = $scope.objectType; - - function getOriginalUrl() { - // If there is no objectId, then it isn't saved, so it has no original URL. - if ($scope.objectId === undefined || $scope.objectId === '') { - return; - } - - const url = $location.absUrl(); - // Replace hashes with original RISON values. - const unhashedUrl = unhashUrl(url, getUnhashableStates()); - - const parsedUrl = parseUrl(unhashedUrl); - // Get the Angular route, after the hash, and remove the #. - const parsedAppUrl = parseUrl(parsedUrl.hash.slice(1), true); - - return formatUrl({ - protocol: parsedUrl.protocol, - auth: parsedUrl.auth, - host: parsedUrl.host, - pathname: parsedUrl.pathname, - hash: formatUrl({ - pathname: parsedAppUrl.pathname, - query: { - // Add global state to the URL so that the iframe doesn't just show the time range - // default. - _g: parsedAppUrl.query._g, - }, - }), - }); - } - - function getSnapshotUrl() { - const url = $location.absUrl(); - // Replace hashes with original RISON values. - return unhashUrl(url, getUnhashableStates()); - } - - this.makeUrlEmbeddable = url => { - const embedQueryParam = '?embed=true'; - const urlHasQueryString = url.indexOf('?') !== -1; - if (urlHasQueryString) { - return url.replace('?', `${embedQueryParam}&`); - } - return `${url}${embedQueryParam}`; - }; - - this.makeIframeTag = url => { - if (!url) return; - - const embeddableUrl = this.makeUrlEmbeddable(url); - return ``; - }; - - this.urls = { - original: undefined, - snapshot: undefined, - shortSnapshot: undefined, - shortSnapshotIframe: undefined, - }; - - this.urlFlags = { - shortSnapshot: false, - shortSnapshotIframe: false, - }; - - const updateUrls = () => { - this.urls = { - original: getOriginalUrl(), - snapshot: getSnapshotUrl(), - shortSnapshot: undefined, - shortSnapshotIframe: undefined, - }; - - // Whenever the URL changes, reset the Short URLs to regular URLs. - this.urlFlags = { - shortSnapshot: false, - shortSnapshotIframe: false, - }; - }; - - // When the URL changes, update the links in the UI. - $scope.$watch(() => $location.absUrl(), () => { - updateUrls(); - }); - - this.toggleShortSnapshotUrl = () => { - this.urlFlags.shortSnapshot = !this.urlFlags.shortSnapshot; - - if (this.urlFlags.shortSnapshot) { - shortenUrl(this.urls.snapshot) - .then(shortUrl => { - // We're using ES6 Promises, not $q, so we have to wrap this in $apply. - $scope.$apply(() => { - this.urls.shortSnapshot = shortUrl; - }); - }); - } - }; - - this.toggleShortSnapshotIframeUrl = () => { - this.urlFlags.shortSnapshotIframe = !this.urlFlags.shortSnapshotIframe; - - if (this.urlFlags.shortSnapshotIframe) { - const snapshotIframe = this.makeUrlEmbeddable(this.urls.snapshot); - shortenUrl(snapshotIframe) - .then(shortUrl => { - // We're using ES6 Promises, not $q, so we have to wrap this in $apply. - $scope.$apply(() => { - this.urls.shortSnapshotIframe = shortUrl; - }); - }); - } - }; - - this.copyToClipboard = selector => { - // Select the text to be copied. If the copy fails, the user can easily copy it manually. - const copyTextarea = $document.find(selector)[0]; - copyTextarea.select(); - - try { - const isCopied = document.execCommand('copy'); - if (isCopied) { - toastNotifications.add({ - title: 'URL was copied to the clipboard', - 'data-test-subj': 'shareCopyToClipboardSuccess', - }); - } else { - toastNotifications.add({ - title: 'URL selected. Press Ctrl+C to copy.', - 'data-test-subj': 'shareCopyToClipboardSuccess', - }); - } - } catch (err) { - toastNotifications.add({ - title: 'URL selected. Press Ctrl+C to copy.', - 'data-test-subj': 'shareCopyToClipboardSuccess', - }); - } - }; - } - }; -}); diff --git a/src/ui/public/share/index.js b/src/ui/public/share/index.js index ec8154739e042..99728720d526b 100644 --- a/src/ui/public/share/index.js +++ b/src/ui/public/share/index.js @@ -17,4 +17,4 @@ * under the License. */ -import './directives/share'; +export { showShareContextMenu } from './show_share_context_menu'; diff --git a/src/ui/public/share/views/share.html b/src/ui/public/share/views/share.html deleted file mode 100644 index 72ea890010a26..0000000000000 --- a/src/ui/public/share/views/share.html +++ /dev/null @@ -1,238 +0,0 @@ -
- -
- -

- Share saved {{share.objectType}} -

- - -
- You can share this URL with people to let them load the most recent saved version of this {{share.objectType}}. -
- -
- Please save this {{share.objectType}} to enable this sharing option. -
- -
- -
- -
- - -
- - - - - -
- Add to your HTML source. Note that all clients must be able to access Kibana. -
-
- - -
- -
- - -
- - - -
-
-
- - -
- -

- Share Snapshot -

- - -
- Snapshot URLs encode the current state of the {{share.objectType}} in the URL itself. Edits to the saved {{share.objectType}} won't be visible via this URL. -
- - -
- -
- - -
- - - - - -
- Add to your HTML source. Note that all clients must be able to access Kibana. -
-
- - -
- - - - - - - -
- We recommend sharing shortened snapshot URLs for maximum compatibility. Internet Explorer has URL length restrictions, and some wiki and markup parsers don't do well with the full-length version of the snapshot URL, but the short URL should work great. -
-
-
-
From b480e99c8ae3b02e012eaad7db65c564fcd79d87 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 14 Aug 2018 10:15:18 -0600 Subject: [PATCH 11/28] fix button styling --- .../kibana/public/dashboard/top_nav/options_popover.less | 7 ------- .../public/dashboard/top_nav/show_options_popover.js | 3 +-- src/ui/public/share/show_share_context_menu.js | 1 + src/ui/public/styles/navbar.less | 8 ++++++++ 4 files changed, 10 insertions(+), 9 deletions(-) delete mode 100644 src/core_plugins/kibana/public/dashboard/top_nav/options_popover.less diff --git a/src/core_plugins/kibana/public/dashboard/top_nav/options_popover.less b/src/core_plugins/kibana/public/dashboard/top_nav/options_popover.less deleted file mode 100644 index b9c1271573ecb..0000000000000 --- a/src/core_plugins/kibana/public/dashboard/top_nav/options_popover.less +++ /dev/null @@ -1,7 +0,0 @@ -.dashOptionsPopover { - height: 100%; - - .euiPopover__anchor { - height: 100%; - } -} diff --git a/src/core_plugins/kibana/public/dashboard/top_nav/show_options_popover.js b/src/core_plugins/kibana/public/dashboard/top_nav/show_options_popover.js index 68c95c6ad300a..2d2dd83b4c34e 100644 --- a/src/core_plugins/kibana/public/dashboard/top_nav/show_options_popover.js +++ b/src/core_plugins/kibana/public/dashboard/top_nav/show_options_popover.js @@ -17,7 +17,6 @@ * under the License. */ -import './options_popover.less'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -55,7 +54,7 @@ export function showOptionsPopover({ document.body.appendChild(container); const element = ( Date: Tue, 14 Aug 2018 13:31:29 -0600 Subject: [PATCH 12/28] add jest test for share_context_menu --- .../share_context_menu.test.js.snap | 65 +++++++++++++++++++ .../components/share_context_menu.test.js | 45 +++++++++++++ .../share/components/share_url_content.js | 1 - 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/ui/public/share/components/__snapshots__/share_context_menu.test.js.snap create mode 100644 src/ui/public/share/components/share_context_menu.test.js diff --git a/src/ui/public/share/components/__snapshots__/share_context_menu.test.js.snap b/src/ui/public/share/components/__snapshots__/share_context_menu.test.js.snap new file mode 100644 index 0000000000000..e4157f40f2ce9 --- /dev/null +++ b/src/ui/public/share/components/__snapshots__/share_context_menu.test.js.snap @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should only render permalink panel when there are no other panels 1`] = ` +, + "id": 1, + "title": "Permalink", + }, + ] + } +/> +`; + +exports[`should render context menu panel when there are more than one panel 1`] = ` +, + "id": 1, + "title": "Permalink", + }, + Object { + "content": , + "id": 2, + "title": "Embed Code", + }, + Object { + "id": 3, + "items": Array [ + Object { + "icon": "console", + "name": "Embed code", + "panel": 2, + }, + Object { + "icon": "link", + "name": "Permalinks", + "panel": 1, + }, + ], + "title": "Share this dashboard", + }, + ] + } +/> +`; diff --git a/src/ui/public/share/components/share_context_menu.test.js b/src/ui/public/share/components/share_context_menu.test.js new file mode 100644 index 0000000000000..385441fef5d8a --- /dev/null +++ b/src/ui/public/share/components/share_context_menu.test.js @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('../lib/url_shortener', () => ({})); + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { + ShareContextMenu, +} from './share_context_menu'; + +test('should render context menu panel when there are more than one panel', () => { + const component = shallow( {}} + />); + expect(component).toMatchSnapshot(); +}); + +test('should only render permalink panel when there are no other panels', () => { + const component = shallow( {}} + />); + expect(component).toMatchSnapshot(); +}); diff --git a/src/ui/public/share/components/share_url_content.js b/src/ui/public/share/components/share_url_content.js index 714785f1404a3..a0ec3b90ed27a 100644 --- a/src/ui/public/share/components/share_url_content.js +++ b/src/ui/public/share/components/share_url_content.js @@ -33,7 +33,6 @@ import { EuiLoadingSpinner, } from '@elastic/eui'; - import { parse as parseUrl, format as formatUrl, From 89f590b8943a8103fbff1b964bce2ce9e8a81621 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 15 Aug 2018 08:13:13 -0600 Subject: [PATCH 13/28] use EuiCopy to copy URL, add jest test for ShareUrlContent component --- .../share_url_content.test.js.snap | 258 ++++++++++++++++++ .../share/components/share_url_content.js | 19 +- .../components/share_url_content.test.js | 44 +++ 3 files changed, 314 insertions(+), 7 deletions(-) create mode 100644 src/ui/public/share/components/__snapshots__/share_url_content.test.js.snap create mode 100644 src/ui/public/share/components/share_url_content.test.js diff --git a/src/ui/public/share/components/__snapshots__/share_url_content.test.js.snap b/src/ui/public/share/components/__snapshots__/share_url_content.test.js.snap new file mode 100644 index 0000000000000..83658a83fe850 --- /dev/null +++ b/src/ui/public/share/components/__snapshots__/share_url_content.test.js.snap @@ -0,0 +1,258 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`render 1`] = ` + + + + + Saved object + + + + +
, + }, + Object { + "id": "snapshot", + "label": + + Snapshot + + + + + , + }, + ] + } + /> + + + + + + + + + + + + +
+`; + +exports[`should enable saved object export option when objectId is provided 1`] = ` + + + + + Saved object + + + + + , + }, + Object { + "id": "snapshot", + "label": + + Snapshot + + + + + , + }, + ] + } + /> + + + + + + + + + + + + + +`; diff --git a/src/ui/public/share/components/share_url_content.js b/src/ui/public/share/components/share_url_content.js index a0ec3b90ed27a..af826164350de 100644 --- a/src/ui/public/share/components/share_url_content.js +++ b/src/ui/public/share/components/share_url_content.js @@ -31,6 +31,7 @@ import { EuiFlexItem, EuiRadioGroup, EuiLoadingSpinner, + EuiCopy, } from '@elastic/eui'; import { @@ -278,13 +279,17 @@ export class ShareUrlContent extends Component { {this.renderShortUrlSwitch()} - window.alert(this.state.url)} - disabled={this.state.isCreatingShortUrl} - > - Copy { this.props.isEmbedded ? 'iFrame code' : 'link' } - + + {(copy) => ( + + Copy { this.props.isEmbedded ? 'iFrame code' : 'link' } + + )} + ); diff --git a/src/ui/public/share/components/share_url_content.test.js b/src/ui/public/share/components/share_url_content.test.js new file mode 100644 index 0000000000000..3ee722041eca4 --- /dev/null +++ b/src/ui/public/share/components/share_url_content.test.js @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('../lib/url_shortener', () => ({})); + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { + ShareUrlContent, +} from './share_url_content'; + +test('render', () => { + const component = shallow( {}} + />); + expect(component).toMatchSnapshot(); +}); + +test('should enable saved object export option when objectId is provided', () => { + const component = shallow( {}} + />); + expect(component).toMatchSnapshot(); +}); From 23fe41da4c2ac5c061a2e0adcf171a2c23094266 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 15 Aug 2018 08:26:04 -0600 Subject: [PATCH 14/28] clean up --- src/ui/public/share/components/share_url_content.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/ui/public/share/components/share_url_content.js b/src/ui/public/share/components/share_url_content.js index af826164350de..6ab62d6c88015 100644 --- a/src/ui/public/share/components/share_url_content.js +++ b/src/ui/public/share/components/share_url_content.js @@ -50,11 +50,14 @@ export class ShareUrlContent extends Component { constructor(props) { super(props); + const defaultExportUrlAs = EXPORT_URL_AS_SNAPSHOT; + const defaultUseShortUrl = false; + this.state = { - exportUrlAs: EXPORT_URL_AS_SNAPSHOT, - useShortUrl: false, + exportUrlAs: defaultExportUrlAs, + useShortUrl: defaultUseShortUrl, isCreatingShortUrl: false, - url: this.getUrl(EXPORT_URL_AS_SNAPSHOT, false), + url: this.getUrl(defaultExportUrlAs, defaultUseShortUrl), }; } From d98c2fab718bc5e67833edc6d37d6cf9e4bb096b Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 15 Aug 2018 09:13:45 -0600 Subject: [PATCH 15/28] display short URL create error message in form instead of with toast --- .../share/components/share_url_content.js | 44 ++++++++++++++----- src/ui/public/share/lib/url_shortener.js | 20 +++------ src/ui/public/share/lib/url_shortener.test.js | 7 --- 3 files changed, 38 insertions(+), 33 deletions(-) diff --git a/src/ui/public/share/components/share_url_content.js b/src/ui/public/share/components/share_url_content.js index 6ab62d6c88015..2c72b3be25406 100644 --- a/src/ui/public/share/components/share_url_content.js +++ b/src/ui/public/share/components/share_url_content.js @@ -87,10 +87,25 @@ export class ShareUrlContent extends Component { } } - getShortUrl = async () => { - this.setState({ isCreatingShortUrl: true }); + createShortUrl = async () => { + this.setState({ + isCreatingShortUrl: true, + shortUrlErrorMsg: undefined, + }); + + let shortUrl; + try { + shortUrl = await shortenUrl(this.getSnapshotUrl()); + } catch(fetchError) { + if (this._isMounted) { + this.setState({ + isCreatingShortUrl: false, + shortUrlErrorMsg: `Unable to create short URL. Error: ${fetchError.message}` + }); + } + throw fetchError; + } - const shortUrl = await shortenUrl(this.getSnapshotUrl()); if (!this._isMounted) { return; } @@ -184,7 +199,14 @@ export class ShareUrlContent extends Component { handleShortUrlChange = async evt => { const isChecked = evt.target.checked; if (this.state.shortUrl === undefined && isChecked) { - await this.getShortUrl(); + try { + await this.createShortUrl(); + } catch(fetchError) { + this.setState({ + useShortUrl: false, + }, this.setUrl); + return; + } } this.setState({ @@ -252,8 +274,11 @@ export class ShareUrlContent extends Component { return; } + const switchLabel = this.state.isCreatingShortUrl + ? ( Short URL) + : 'Short URL'; const switchComponent = (); @@ -261,14 +286,9 @@ export class ShareUrlContent extends Component { Internet Explorer has URL length restrictions, and some wiki and markup parsers don't do well with the full-length version of the snapshot URL, but the short URL should work great.`; - let loadingShortUrl; - if (this.state.isCreatingShortUrl) { - loadingShortUrl = ( - - ); - } + return ( - + {this.renderWithIconTip(switchComponent, tipContent)} ); diff --git a/src/ui/public/share/lib/url_shortener.js b/src/ui/public/share/lib/url_shortener.js index cf584a5673f3a..74652cfc9ac86 100644 --- a/src/ui/public/share/lib/url_shortener.js +++ b/src/ui/public/share/lib/url_shortener.js @@ -20,7 +20,6 @@ import chrome from '../../chrome'; import url from 'url'; import { kfetch } from 'ui/kfetch'; -import { toastNotifications } from 'ui/notify'; export async function shortenUrl(absoluteUrl) { const basePath = chrome.getBasePath(); @@ -32,17 +31,10 @@ export async function shortenUrl(absoluteUrl) { const body = JSON.stringify({ url: relativeUrl }); - try { - const resp = await kfetch({ method: 'POST', 'pathname': '/api/shorten_url', body }); - return url.format({ - protocol: parsedUrl.protocol, - host: parsedUrl.host, - pathname: `${basePath}/goto/${resp.urlId}` - }); - } catch (fetchError) { - toastNotifications.addDanger({ - title: `Unable to create short URL. Error: ${fetchError.message}`, - 'data-test-subj': 'shortenUrlFailure', - }); - } + const resp = await kfetch({ method: 'POST', 'pathname': '/api/shorten_url', body }); + return url.format({ + protocol: parsedUrl.protocol, + host: parsedUrl.host, + pathname: `${basePath}/goto/${resp.urlId}` + }); } diff --git a/src/ui/public/share/lib/url_shortener.test.js b/src/ui/public/share/lib/url_shortener.test.js index d971ab4d99b23..0c45220ff78d1 100644 --- a/src/ui/public/share/lib/url_shortener.test.js +++ b/src/ui/public/share/lib/url_shortener.test.js @@ -20,13 +20,6 @@ jest.mock('ui/kfetch', () => ({})); jest.mock('../../chrome', () => ({})); -jest.mock('ui/notify', - () => ({ - toastNotifications: { - addDanger: () => {}, - } - }), { virtual: true }); - import sinon from 'sinon'; import expect from 'expect.js'; import { shortenUrl } from './url_shortener'; From 8cf0a6e39b5b2afe12a67dbfeb653fcd0a4e0caf Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 15 Aug 2018 09:54:29 -0600 Subject: [PATCH 16/28] switch option order so disbaled option can not be first --- .../share_url_content.test.js.snap | 32 +++++++++---------- .../share/components/share_url_content.js | 16 +++++----- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/ui/public/share/components/__snapshots__/share_url_content.test.js.snap b/src/ui/public/share/components/__snapshots__/share_url_content.test.js.snap index 83658a83fe850..7a198ab66fabb 100644 --- a/src/ui/public/share/components/__snapshots__/share_url_content.test.js.snap +++ b/src/ui/public/share/components/__snapshots__/share_url_content.test.js.snap @@ -17,8 +17,7 @@ exports[`render 1`] = ` options={ Array [ Object { - "disabled": true, - "id": "savedObject", + "id": "snapshot", "label": - Saved object + Snapshot @@ -48,7 +48,8 @@ exports[`render 1`] = ` , }, Object { - "id": "snapshot", + "disabled": true, + "id": "savedObject", "label": - Snapshot + Saved object @@ -145,8 +145,7 @@ exports[`should enable saved object export option when objectId is provided 1`] options={ Array [ Object { - "disabled": false, - "id": "savedObject", + "id": "snapshot", "label": - Saved object + Snapshot @@ -176,7 +176,8 @@ exports[`should enable saved object export option when objectId is provided 1`] , }, Object { - "id": "snapshot", + "disabled": false, + "id": "savedObject", "label": - Snapshot + Saved object diff --git a/src/ui/public/share/components/share_url_content.js b/src/ui/public/share/components/share_url_content.js index 2c72b3be25406..ecf7e11121a9d 100644 --- a/src/ui/public/share/components/share_url_content.js +++ b/src/ui/public/share/components/share_url_content.js @@ -216,6 +216,14 @@ export class ShareUrlContent extends Component { renderExportUrlAsOptions = () => { return [ + { + id: EXPORT_URL_AS_SNAPSHOT, + label: this.renderWithIconTip( + 'Snapshot', + `Snapshot URLs encode the current state of the ${this.props.objectType} in the URL itself. + Edits to the saved ${this.props.objectType} won't be visible via this URL.` + ), + }, { id: EXPORT_URL_AS_SAVED_OBJECT, disabled: this.isNotSaved(), @@ -224,14 +232,6 @@ export class ShareUrlContent extends Component { `You can share this URL with people to let them load the most recent saved version of this ${this.props.objectType}.` ), }, - { - id: EXPORT_URL_AS_SNAPSHOT, - label: this.renderWithIconTip( - 'Snapshot', - `Snapshot URLs encode the current state of the ${this.props.objectType} in the URL itself. - Edits to the saved ${this.props.objectType} won't be visible via this URL.` - ), - } ]; } From f11e80e25e2a51083ab1d1c48fa0060a8e4c1d50 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 15 Aug 2018 13:23:54 -0600 Subject: [PATCH 17/28] fix discover share functional tests --- .../dashboard/top_nav/get_top_nav_config.js | 2 +- .../public/discover/controllers/discover.js | 2 +- .../kibana/public/visualize/editor/editor.js | 2 +- .../share_url_content.test.js.snap | 4 ++ .../share/components/share_url_content.js | 8 +++- .../functional/apps/discover/_shared_links.js | 42 ++++++------------ test/functional/config.js | 4 +- test/functional/page_objects/discover_page.js | 23 ---------- test/functional/page_objects/index.js | 1 + test/functional/page_objects/share_page.js | 43 +++++++++++++++++++ .../dashboard_mode/dashboard_view_mode.js | 2 +- 11 files changed, 74 insertions(+), 59 deletions(-) create mode 100644 test/functional/page_objects/share_page.js diff --git a/src/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.js b/src/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.js index 022f64348eb9b..c0ac1cb2702b2 100644 --- a/src/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.js +++ b/src/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.js @@ -131,7 +131,7 @@ function getShareConfig(action) { return { key: TopNavIds.SHARE, description: 'Share Dashboard', - testId: 'dashboardShareButton', + testId: 'shareTopNavButton', run: action, }; } diff --git a/src/core_plugins/kibana/public/discover/controllers/discover.js b/src/core_plugins/kibana/public/discover/controllers/discover.js index ac008303c53b3..67f6b22b7ed1d 100644 --- a/src/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/core_plugins/kibana/public/discover/controllers/discover.js @@ -191,7 +191,7 @@ function discoverController( }, { key: 'share', description: 'Share Search', - testId: 'discoverShareButton', + testId: 'shareTopNavButton', run: (menuItem, navController, anchorElement) => { showShareContextMenu({ anchorElement, diff --git a/src/core_plugins/kibana/public/visualize/editor/editor.js b/src/core_plugins/kibana/public/visualize/editor/editor.js index 55ee15861c375..2fe7b9db71f53 100644 --- a/src/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/core_plugins/kibana/public/visualize/editor/editor.js @@ -158,7 +158,7 @@ function VisEditor( }, { key: 'share', description: 'Share Visualization', - testId: 'visualizeShareButton', + testId: 'shareTopNavButton', run: (menuItem, navController, anchorElement) => { showShareContextMenu({ anchorElement, diff --git a/src/ui/public/share/components/__snapshots__/share_url_content.test.js.snap b/src/ui/public/share/components/__snapshots__/share_url_content.test.js.snap index 7a198ab66fabb..64623f40d508b 100644 --- a/src/ui/public/share/components/__snapshots__/share_url_content.test.js.snap +++ b/src/ui/public/share/components/__snapshots__/share_url_content.test.js.snap @@ -3,6 +3,7 @@ exports[`render 1`] = ` @@ -132,6 +134,7 @@ exports[`render 1`] = ` exports[`should enable saved object export option when objectId is provided 1`] = ` diff --git a/src/ui/public/share/components/share_url_content.js b/src/ui/public/share/components/share_url_content.js index ecf7e11121a9d..762ec89f180e6 100644 --- a/src/ui/public/share/components/share_url_content.js +++ b/src/ui/public/share/components/share_url_content.js @@ -281,6 +281,7 @@ export class ShareUrlContent extends Component { label={switchLabel} checked={this.state.useShortUrl} onChange={this.handleShortUrlChange} + data-test-subj="useShortUrl" />); const tipContent = `We recommend sharing shortened snapshot URLs for maximum compatibility. Internet Explorer has URL length restrictions, @@ -296,7 +297,10 @@ export class ShareUrlContent extends Component { render() { return ( - + {this.renderExportAsRadioGroup()} @@ -308,6 +312,8 @@ export class ShareUrlContent extends Component { fill onClick={copy} disabled={this.state.isCreatingShortUrl} + data-share-url={this.state.url} + data-test-subj="copyShareUrlButton" > Copy { this.props.isEmbedded ? 'iFrame code' : 'link' } diff --git a/test/functional/apps/discover/_shared_links.js b/test/functional/apps/discover/_shared_links.js index ae6fbfc517e4b..7f10639d5bcf1 100644 --- a/test/functional/apps/discover/_shared_links.js +++ b/test/functional/apps/discover/_shared_links.js @@ -24,7 +24,7 @@ export default function ({ getService, getPageObjects }) { const log = getService('log'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); - const PageObjects = getPageObjects(['common', 'discover', 'header']); + const PageObjects = getPageObjects(['common', 'discover', 'header', 'share']); describe('shared links', function describeIndexTests() { let baseUrl; @@ -59,20 +59,13 @@ export default function ({ getService, getPageObjects }) { //After hiding the time picker, we need to wait for //the refresh button to hide before clicking the share button - return PageObjects.common.sleep(1000); - }); - - describe('shared link', function () { - it('should show "Share a link" caption', async function () { - const expectedCaption = 'Share saved'; - - await PageObjects.discover.clickShare(); - const actualCaption = await PageObjects.discover.getShareCaption(); + await PageObjects.common.sleep(1000); - expect(actualCaption).to.contain(expectedCaption); - }); + await PageObjects.share.clickShareTopNavButton(); + }); - it('should show the correct formatted URL', async function () { + describe('permalink', function () { + it('should allow for copying the snapshot URL', async function () { const expectedUrl = baseUrl + '/app/kibana?_t=1453775307251#' + @@ -81,33 +74,22 @@ export default function ({ getService, getPageObjects }) { '-23T18:31:44.000Z\'))&_a=(columns:!(_source),index:\'logstash-' + '*\',interval:auto,query:(language:lucene,query:\'\')' + ',sort:!(\'@timestamp\',desc))'; - const actualUrl = await PageObjects.discover.getSharedUrl(); + const actualUrl = await PageObjects.share.getSharedUrl(); + log.debug('actualUrl', actualUrl); // strip the timestamp out of each URL expect(actualUrl.replace(/_t=\d{13}/, '_t=TIMESTAMP')).to.be( expectedUrl.replace(/_t=\d{13}/, '_t=TIMESTAMP') ); }); - it('gets copied to clipboard', async function () { - const isCopiedToClipboard = await PageObjects.discover.clickCopyToClipboard(); - expect(isCopiedToClipboard).to.eql(true); - }); - - // TODO: verify clipboard contents - it('shorten URL button should produce a short URL', async function () { + it('should allow for copying the snapshot URL as a short URL', async function () { const re = new RegExp(baseUrl + '/goto/[0-9a-f]{32}$'); - await PageObjects.discover.clickShortenUrl(); - await retry.try(async function tryingForTime() { - const actualUrl = await PageObjects.discover.getSharedUrl(); + await PageObjects.share.checkShortenUrl(); + await retry.try(async () => { + const actualUrl = await PageObjects.share.getSharedUrl(); expect(actualUrl).to.match(re); }); }); - - // NOTE: This test has to run immediately after the test above - it('copies short URL to clipboard', async function () { - const isCopiedToClipboard = await PageObjects.discover.clickCopyToClipboard(); - expect(isCopiedToClipboard).to.eql(true); - }); }); }); } diff --git a/test/functional/config.js b/test/functional/config.js index 03aec33947122..082a3ba12c2f5 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -32,6 +32,7 @@ import { PointSeriesPageProvider, VisualBuilderPageProvider, TimelionPageProvider, + SharePageProvider } from './page_objects'; import { @@ -85,7 +86,8 @@ export default async function ({ readConfigFile }) { monitoring: MonitoringPageProvider, pointSeries: PointSeriesPageProvider, visualBuilder: VisualBuilderPageProvider, - timelion: TimelionPageProvider + timelion: TimelionPageProvider, + share: SharePageProvider, }, services: { es: commonConfig.get('services.es'), diff --git a/test/functional/page_objects/discover_page.js b/test/functional/page_objects/discover_page.js index 3c7d282401aa4..c3283fc35ca15 100644 --- a/test/functional/page_objects/discover_page.js +++ b/test/functional/page_objects/discover_page.js @@ -226,29 +226,6 @@ export function DiscoverPageProvider({ getService, getPageObjects }) { .getVisibleText(); } - clickShare() { - return testSubjects.click('discoverShareButton'); - } - - clickShortenUrl() { - return testSubjects.click('sharedSnapshotShortUrlButton'); - } - - async clickCopyToClipboard() { - await testSubjects.click('sharedSnapshotCopyButton'); - - // Confirm that the content was copied to the clipboard. - return await testSubjects.exists('shareCopyToClipboardSuccess'); - } - - async getShareCaption() { - return await testSubjects.getVisibleText('shareUiTitle'); - } - - async getSharedUrl() { - return await testSubjects.getProperty('sharedSnapshotUrl', 'value'); - } - async toggleSidebarCollapse() { return await testSubjects.click('collapseSideBarButton'); } diff --git a/test/functional/page_objects/index.js b/test/functional/page_objects/index.js index 72f07453041d1..04fc7240480ff 100644 --- a/test/functional/page_objects/index.js +++ b/test/functional/page_objects/index.js @@ -31,3 +31,4 @@ export { MonitoringPageProvider } from './monitoring_page'; export { PointSeriesPageProvider } from './point_series_page'; export { VisualBuilderPageProvider } from './visual_builder_page'; export { TimelionPageProvider } from './timelion_page'; +export { SharePageProvider } from './share_page'; diff --git a/test/functional/page_objects/share_page.js b/test/functional/page_objects/share_page.js new file mode 100644 index 0000000000000..41ccdcbb77f4d --- /dev/null +++ b/test/functional/page_objects/share_page.js @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function SharePageProvider({ getService, getPageObjects }) { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['visualize']); + + class SharePage { + async clickShareTopNavButton() { + return testSubjects.click('shareTopNavButton'); + } + + async getSharedUrl() { + return await testSubjects.getAttribute('copyShareUrlButton', 'data-share-url'); + } + + async checkShortenUrl() { + await PageObjects.visualize.checkCheckbox('useShortUrl'); + + const shareForm = await testSubjects.find('shareUrlForm'); + await shareForm.waitForDeletedByClassName('euiLoadingSpinner'); + } + + } + + return new SharePage(); +} diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js index 7fb26877a4a6c..8dc31bb586911 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js @@ -158,7 +158,7 @@ export default function ({ getService, getPageObjects }) { }); it('does not show the sharing menu item', async () => { - const shareMenuItemExists = await testSubjects.exists('dashboardShareButton'); + const shareMenuItemExists = await testSubjects.exists('shareTopNavButton'); expect(shareMenuItemExists).to.be(false); }); From 5b5abc1b8a6a444fc835afd83e5be258bf3048c2 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 16 Aug 2018 05:18:29 -0600 Subject: [PATCH 18/28] add functions required by reporting --- src/core_plugins/kibana/public/dashboard/dashboard_app.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_app.js b/src/core_plugins/kibana/public/dashboard/dashboard_app.js index 6869676c7b3b3..43b29ab5a877a 100644 --- a/src/core_plugins/kibana/public/dashboard/dashboard_app.js +++ b/src/core_plugins/kibana/public/dashboard/dashboard_app.js @@ -133,6 +133,14 @@ app.directive('dashboardApp', function ($injector) { dirty: !dash.id }; + this.getSharingTitle = () => { + return dash.title; + }; + + this.getSharingType = () => { + return 'dashboard'; + }; + dashboardStateManager.registerChangeListener(status => { this.appStatus.dirty = status.dirty || !dash.id; updateState(); From ed6ae2a98f1b825f714ccd3477f01ff370d09245 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 21 Aug 2018 09:24:58 -0600 Subject: [PATCH 19/28] typescript --- ...context_menu.js => share_context_menu.tsx} | 47 ++- ...e_url_content.js => share_url_content.tsx} | 279 ++++++++++-------- .../{url_shortener.js => url_shortener.ts} | 13 +- .../share/show_share_context_menu.test.js | 0 ...xt_menu.js => show_share_context_menu.tsx} | 19 +- .../state_management/state_hashing/index.d.ts | 20 ++ 6 files changed, 214 insertions(+), 164 deletions(-) rename src/ui/public/share/components/{share_context_menu.js => share_context_menu.tsx} (81%) rename src/ui/public/share/components/{share_url_content.js => share_url_content.tsx} (67%) rename src/ui/public/share/lib/{url_shortener.js => url_shortener.ts} (83%) create mode 100644 src/ui/public/share/show_share_context_menu.test.js rename src/ui/public/share/{show_share_context_menu.js => show_share_context_menu.tsx} (83%) create mode 100644 src/ui/public/state_management/state_hashing/index.d.ts diff --git a/src/ui/public/share/components/share_context_menu.js b/src/ui/public/share/components/share_context_menu.tsx similarity index 81% rename from src/ui/public/share/components/share_context_menu.js rename to src/ui/public/share/components/share_context_menu.tsx index f15bddf531df0..70d248abbf25e 100644 --- a/src/ui/public/share/components/share_context_menu.js +++ b/src/ui/public/share/components/share_context_menu.tsx @@ -18,17 +18,25 @@ */ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { - EuiContextMenu, -} from '@elastic/eui'; +import { EuiContextMenu } from '@elastic/eui'; import { ShareUrlContent } from './share_url_content'; -export class ShareContextMenu extends Component { +interface Props { + allowEmbed: boolean; + objectId?: string; + objectType: string; + getUnhashableStates: () => any[]; +} - getPanels = () => { +export class ShareContextMenu extends Component { + public render() { + const { panels, initialPanelId } = this.getPanels(); + return ; + } + + private getPanels = () => { const panels = []; const menuItems = []; @@ -41,12 +49,12 @@ export class ShareContextMenu extends Component { objectType={this.props.objectType} getUnhashableStates={this.props.getUnhashableStates} /> - ) + ), }; menuItems.push({ name: 'Permalinks', icon: 'link', - panel: permalinkPanel.id + panel: permalinkPanel.id, }); panels.push(permalinkPanel); @@ -61,13 +69,13 @@ export class ShareContextMenu extends Component { objectType={this.props.objectType} getUnhashableStates={this.props.getUnhashableStates} /> - ) + ), }; panels.push(embedPanel); menuItems.push({ name: 'Embed code', icon: 'console', - panel: embedPanel.id + panel: embedPanel.id, }); } @@ -87,22 +95,5 @@ export class ShareContextMenu extends Component { const lastPanelIndex = panels.length - 1; const initialPanelId = panels[lastPanelIndex].id; return { panels, initialPanelId }; - } - - render() { - const { panels, initialPanelId } = this.getPanels(); - return ( - - ); - } + }; } - -ShareContextMenu.propTypes = { - allowEmbed: PropTypes.bool.isRequired, - objectId: PropTypes.string, - objectType: PropTypes.string.isRequired, - getUnhashableStates: PropTypes.func.isRequired, -}; diff --git a/src/ui/public/share/components/share_url_content.js b/src/ui/public/share/components/share_url_content.tsx similarity index 67% rename from src/ui/public/share/components/share_url_content.js rename to src/ui/public/share/components/share_url_content.tsx index 762ec89f180e6..cf10cbdf01f38 100644 --- a/src/ui/public/share/components/share_url_content.js +++ b/src/ui/public/share/components/share_url_content.tsx @@ -17,27 +17,29 @@ * under the License. */ -import './share_url_content.less'; -import PropTypes from 'prop-types'; +// TODO: Remove once typescript definitions are in EUI +declare module '@elastic/eui' { + export const EuiCopy: React.SFC; + export const EuiForm: React.SFC; +} + import React, { Component } from 'react'; +import './share_url_content.less'; import { - EuiForm, - EuiFormRow, - EuiSwitch, EuiButton, - EuiIconTip, + EuiCopy, EuiFlexGroup, EuiFlexItem, - EuiRadioGroup, + EuiForm, + EuiFormRow, + EuiIconTip, EuiLoadingSpinner, - EuiCopy, + EuiRadioGroup, + EuiSwitch, } from '@elastic/eui'; -import { - parse as parseUrl, - format as formatUrl, -} from 'url'; +import { format as formatUrl, parse as parseUrl } from 'url'; import { unhashUrl } from '../../state_management/state_hashing'; import { shortenUrl } from '../lib/url_shortener'; @@ -45,9 +47,26 @@ import { shortenUrl } from '../lib/url_shortener'; const EXPORT_URL_AS_SAVED_OBJECT = 'savedObject'; const EXPORT_URL_AS_SNAPSHOT = 'snapshot'; -export class ShareUrlContent extends Component { +interface Props { + isEmbedded?: boolean; + objectId?: string; + objectType: string; + getUnhashableStates: () => any[]; +} + +interface State { + exportUrlAs: string; + useShortUrl: boolean; + isCreatingShortUrl: boolean; + url?: string; + shortUrl?: string; + shortUrlErrorMsg?: string; +} + +export class ShareUrlContent extends Component { + private mounted?: boolean; - constructor(props) { + constructor(props: Props) { super(props); const defaultExportUrlAs = EXPORT_URL_AS_SNAPSHOT; @@ -61,33 +80,60 @@ export class ShareUrlContent extends Component { }; } - componentWillUnmount() { + public componentWillUnmount() { window.removeEventListener('hashchange', this.resetUrl); - this._isMounted = false; + this.mounted = false; } - componentDidMount() { - this._isMounted = true; + public componentDidMount() { + this.mounted = true; window.addEventListener('hashchange', this.resetUrl, false); } - isNotSaved = () => { - return this.props.objectId === undefined || this.props.objectId === ''; + public render() { + return ( + + {this.renderExportAsRadioGroup()} + + {this.renderShortUrlSwitch()} + + + {(copy: () => void) => ( + + Copy {this.props.isEmbedded ? 'iFrame code' : 'link'} + + )} + + + ); } - resetUrl = () => { - if (this._isMounted) { - this.setState({ - useShortUrl: false, - shortUrl: undefined, - url: undefined, - }, this.setUrl); + private isNotSaved = () => { + return this.props.objectId === undefined || this.props.objectId === ''; + }; + + private resetUrl = () => { + if (this.mounted) { + this.setState( + { + useShortUrl: false, + shortUrl: undefined, + url: undefined, + }, + this.setUrl + ); } - } + }; - createShortUrl = async () => { + private createShortUrl = async () => { this.setState({ isCreatingShortUrl: true, shortUrlErrorMsg: undefined, @@ -96,17 +142,17 @@ export class ShareUrlContent extends Component { let shortUrl; try { shortUrl = await shortenUrl(this.getSnapshotUrl()); - } catch(fetchError) { - if (this._isMounted) { + } catch (fetchError) { + if (this.mounted) { this.setState({ isCreatingShortUrl: false, - shortUrlErrorMsg: `Unable to create short URL. Error: ${fetchError.message}` + shortUrlErrorMsg: `Unable to create short URL. Error: ${fetchError.message}`, }); } throw fetchError; } - if (!this._isMounted) { + if (!this.mounted) { return; } @@ -114,9 +160,9 @@ export class ShareUrlContent extends Component { shortUrl, isCreatingShortUrl: false, }); - } + }; - getSavedObjectUrl = () => { + private getSavedObjectUrl = () => { if (this.isNotSaved()) { return; } @@ -126,6 +172,10 @@ export class ShareUrlContent extends Component { const unhashedUrl = unhashUrl(url, this.props.getUnhashableStates()); const parsedUrl = parseUrl(unhashedUrl); + if (!parsedUrl || !parsedUrl.hash) { + return; + } + // Get the application route, after the hash, and remove the #. const parsedAppUrl = parseUrl(parsedUrl.hash.slice(1), true); @@ -143,31 +193,33 @@ export class ShareUrlContent extends Component { }, }), }); - } + }; - getSnapshotUrl = () => { + private getSnapshotUrl = () => { const url = window.location.href; // Replace hashes with original RISON values. return unhashUrl(url, this.props.getUnhashableStates()); - } + }; - makeUrlEmbeddable = url => { + private makeUrlEmbeddable = (url: string) => { const embedQueryParam = '?embed=true'; const urlHasQueryString = url.indexOf('?') !== -1; if (urlHasQueryString) { return url.replace('?', `${embedQueryParam}&`); } return `${url}${embedQueryParam}`; - } + }; - makeIframeTag = url => { - if (!url) return; + private makeIframeTag = (url?: string) => { + if (!url) { + return; + } const embeddableUrl = this.makeUrlEmbeddable(url); return ``; - } + }; - getUrl = (exportUrlAs, useShortUrl) => { + private getUrl = (exportUrlAs: string, useShortUrl: boolean) => { let url; if (exportUrlAs === EXPORT_URL_AS_SAVED_OBJECT) { url = this.getSavedObjectUrl(); @@ -182,39 +234,48 @@ export class ShareUrlContent extends Component { } return url; - } + }; - setUrl = () => { + private setUrl = () => { this.setState({ - url: this.getUrl(this.state.exportUrlAs, this.state.useShortUrl) + url: this.getUrl(this.state.exportUrlAs, this.state.useShortUrl), }); - } + }; - handleExportUrlAs = optionId => { - this.setState({ - exportUrlAs: optionId, - }, this.setUrl); - } + private handleExportUrlAs = (optionId: string) => { + this.setState( + { + exportUrlAs: optionId, + }, + this.setUrl + ); + }; - handleShortUrlChange = async evt => { + private handleShortUrlChange = async (evt: any) => { const isChecked = evt.target.checked; if (this.state.shortUrl === undefined && isChecked) { try { await this.createShortUrl(); - } catch(fetchError) { - this.setState({ - useShortUrl: false, - }, this.setUrl); + } catch (fetchError) { + this.setState( + { + useShortUrl: false, + }, + this.setUrl + ); return; } } - this.setState({ - useShortUrl: isChecked, - }, this.setUrl); - } + this.setState( + { + useShortUrl: isChecked, + }, + this.setUrl + ); + }; - renderExportUrlAsOptions = () => { + private renderExportUrlAsOptions = () => { return [ { id: EXPORT_URL_AS_SNAPSHOT, @@ -229,37 +290,31 @@ export class ShareUrlContent extends Component { disabled: this.isNotSaved(), label: this.renderWithIconTip( 'Saved object', - `You can share this URL with people to let them load the most recent saved version of this ${this.props.objectType}.` + `You can share this URL with people to let them load the most recent saved version of this ${ + this.props.objectType + }.` ), }, ]; - } + }; - renderWithIconTip = (child, tipContent) => { + private renderWithIconTip = (child: React.ReactNode, tipContent: React.ReactNode) => { return ( - - {child} - + {child} - + ); - } + }; - renderExportAsRadioGroup = () => { + private renderExportAsRadioGroup = () => { const generateLinkAsHelp = this.isNotSaved() ? `Can't share as saved object until the ${this.props.objectType} has been saved.` : undefined; return ( - + ); - } + }; - renderShortUrlSwitch = () => { + private renderShortUrlSwitch = () => { if (this.state.exportUrlAs === EXPORT_URL_AS_SAVED_OBJECT) { return; } - const switchLabel = this.state.isCreatingShortUrl - ? ( Short URL) - : 'Short URL'; - const switchComponent = (); + const switchLabel = this.state.isCreatingShortUrl ? ( + + Short URL + + ) : ( + 'Short URL' + ); + const switchComponent = ( + + ); const tipContent = `We recommend sharing shortened snapshot URLs for maximum compatibility. Internet Explorer has URL length restrictions, and some wiki and markup parsers don't do well with the full-length version of the snapshot URL, @@ -293,41 +354,5 @@ export class ShareUrlContent extends Component { {this.renderWithIconTip(switchComponent, tipContent)} ); - } - - render() { - return ( - - - {this.renderExportAsRadioGroup()} - - {this.renderShortUrlSwitch()} - - - {(copy) => ( - - Copy { this.props.isEmbedded ? 'iFrame code' : 'link' } - - )} - - - - ); - } + }; } - -ShareUrlContent.propTypes = { - isEmbedded: PropTypes.bool, - objectId: PropTypes.string, - objectType: PropTypes.string.isRequired, - getUnhashableStates: PropTypes.func.isRequired, -}; diff --git a/src/ui/public/share/lib/url_shortener.js b/src/ui/public/share/lib/url_shortener.ts similarity index 83% rename from src/ui/public/share/lib/url_shortener.js rename to src/ui/public/share/lib/url_shortener.ts index 74652cfc9ac86..037214bd9b450 100644 --- a/src/ui/public/share/lib/url_shortener.js +++ b/src/ui/public/share/lib/url_shortener.ts @@ -17,24 +17,27 @@ * under the License. */ -import chrome from '../../chrome'; -import url from 'url'; import { kfetch } from 'ui/kfetch'; +import url from 'url'; +import chrome from '../../chrome'; -export async function shortenUrl(absoluteUrl) { +export async function shortenUrl(absoluteUrl: string) { const basePath = chrome.getBasePath(); const parsedUrl = url.parse(absoluteUrl); + if (!parsedUrl || !parsedUrl.path) { + return; + } const path = parsedUrl.path.replace(basePath, ''); const hash = parsedUrl.hash ? parsedUrl.hash : ''; const relativeUrl = path + hash; const body = JSON.stringify({ url: relativeUrl }); - const resp = await kfetch({ method: 'POST', 'pathname': '/api/shorten_url', body }); + const resp = await kfetch({ method: 'POST', pathname: '/api/shorten_url', body }); return url.format({ protocol: parsedUrl.protocol, host: parsedUrl.host, - pathname: `${basePath}/goto/${resp.urlId}` + pathname: `${basePath}/goto/${resp.urlId}`, }); } diff --git a/src/ui/public/share/show_share_context_menu.test.js b/src/ui/public/share/show_share_context_menu.test.js new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/ui/public/share/show_share_context_menu.js b/src/ui/public/share/show_share_context_menu.tsx similarity index 83% rename from src/ui/public/share/show_share_context_menu.js rename to src/ui/public/share/show_share_context_menu.tsx index dbd79d503a9e2..91506fd5e052e 100644 --- a/src/ui/public/share/show_share_context_menu.js +++ b/src/ui/public/share/show_share_context_menu.tsx @@ -17,14 +17,17 @@ * under the License. */ +// TODO: Remove once typescript definitions are in EUI +declare module '@elastic/eui' { + export const EuiWrappingPopover: React.SFC; +} + import React from 'react'; import ReactDOM from 'react-dom'; import { ShareContextMenu } from './components/share_context_menu'; -import { - EuiWrappingPopover, -} from '@elastic/eui'; +import { EuiWrappingPopover } from '@elastic/eui'; let isOpen = false; @@ -35,13 +38,21 @@ const onClose = () => { isOpen = false; }; +interface ShowProps { + anchorElement: any; + allowEmbed: boolean; + getUnhashableStates: () => any[]; + objectId?: string; + objectType: string; +} + export function showShareContextMenu({ anchorElement, allowEmbed, getUnhashableStates, objectId, objectType, -}) { +}: ShowProps) { if (isOpen) { onClose(); return; diff --git a/src/ui/public/state_management/state_hashing/index.d.ts b/src/ui/public/state_management/state_hashing/index.d.ts new file mode 100644 index 0000000000000..163d9ed07f2cc --- /dev/null +++ b/src/ui/public/state_management/state_hashing/index.d.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function unhashUrl(url: string, kbnStates: any[]): any; From d2725e62b74ebd08192e6795a1f9efe3e84629e1 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 21 Aug 2018 09:30:21 -0600 Subject: [PATCH 20/28] remove empty file --- src/ui/public/share/show_share_context_menu.test.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/ui/public/share/show_share_context_menu.test.js diff --git a/src/ui/public/share/show_share_context_menu.test.js b/src/ui/public/share/show_share_context_menu.test.js deleted file mode 100644 index e69de29bb2d1d..0000000000000 From d53bf1f78e391593d4fdb0bc2edf58e2bed479f3 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 21 Aug 2018 10:03:46 -0600 Subject: [PATCH 21/28] fix typescript compile error --- src/ui/public/share/components/share_url_content.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui/public/share/components/share_url_content.tsx b/src/ui/public/share/components/share_url_content.tsx index cf10cbdf01f38..20d74fa6bbe86 100644 --- a/src/ui/public/share/components/share_url_content.tsx +++ b/src/ui/public/share/components/share_url_content.tsx @@ -22,6 +22,7 @@ declare module '@elastic/eui' { export const EuiCopy: React.SFC; export const EuiForm: React.SFC; } +const FixedEuiIconTip = EuiIconTip as React.SFC; import React, { Component } from 'react'; import './share_url_content.less'; @@ -303,7 +304,7 @@ export class ShareUrlContent extends Component { {child} - + ); From b7d9553db1f564edb35a14d2ea8e5984e2a1b097 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 22 Aug 2018 08:56:31 -0600 Subject: [PATCH 22/28] move import so jest tests work --- src/ui/public/share/components/share_url_content.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ui/public/share/components/share_url_content.tsx b/src/ui/public/share/components/share_url_content.tsx index 20d74fa6bbe86..c33460b768907 100644 --- a/src/ui/public/share/components/share_url_content.tsx +++ b/src/ui/public/share/components/share_url_content.tsx @@ -22,7 +22,6 @@ declare module '@elastic/eui' { export const EuiCopy: React.SFC; export const EuiForm: React.SFC; } -const FixedEuiIconTip = EuiIconTip as React.SFC; import React, { Component } from 'react'; import './share_url_content.less'; @@ -45,6 +44,9 @@ import { format as formatUrl, parse as parseUrl } from 'url'; import { unhashUrl } from '../../state_management/state_hashing'; import { shortenUrl } from '../lib/url_shortener'; +// TODO: Remove once EuiIconTip supports "content" prop +const FixedEuiIconTip = EuiIconTip as React.SFC; + const EXPORT_URL_AS_SAVED_OBJECT = 'savedObject'; const EXPORT_URL_AS_SNAPSHOT = 'snapshot'; From aa4f0e28ab7aeacf798c8afc0aa1e6c15fcca27a Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 22 Aug 2018 14:16:47 -0600 Subject: [PATCH 23/28] fix Failed prop type: The proptextToCopyis marked as required inEuiCopy, but its value isundefined --- .../share/components/share_url_content.tsx | 117 ++++++++---------- 1 file changed, 49 insertions(+), 68 deletions(-) diff --git a/src/ui/public/share/components/share_url_content.tsx b/src/ui/public/share/components/share_url_content.tsx index c33460b768907..f96d67ca0c1bf 100644 --- a/src/ui/public/share/components/share_url_content.tsx +++ b/src/ui/public/share/components/share_url_content.tsx @@ -47,9 +47,6 @@ import { shortenUrl } from '../lib/url_shortener'; // TODO: Remove once EuiIconTip supports "content" prop const FixedEuiIconTip = EuiIconTip as React.SFC; -const EXPORT_URL_AS_SAVED_OBJECT = 'savedObject'; -const EXPORT_URL_AS_SNAPSHOT = 'snapshot'; - interface Props { isEmbedded?: boolean; objectId?: string; @@ -57,8 +54,13 @@ interface Props { getUnhashableStates: () => any[]; } +enum ExportUrlAsType { + EXPORT_URL_AS_SAVED_OBJECT = 'savedObject', + EXPORT_URL_AS_SNAPSHOT = 'snapshot', +} + interface State { - exportUrlAs: string; + exportUrlAs: ExportUrlAsType; useShortUrl: boolean; isCreatingShortUrl: boolean; url?: string; @@ -72,14 +74,11 @@ export class ShareUrlContent extends Component { constructor(props: Props) { super(props); - const defaultExportUrlAs = EXPORT_URL_AS_SNAPSHOT; - const defaultUseShortUrl = false; - this.state = { - exportUrlAs: defaultExportUrlAs, - useShortUrl: defaultUseShortUrl, + exportUrlAs: ExportUrlAsType.EXPORT_URL_AS_SNAPSHOT, + useShortUrl: false, isCreatingShortUrl: false, - url: this.getUrl(defaultExportUrlAs, defaultUseShortUrl), + url: '', }; } @@ -91,6 +90,7 @@ export class ShareUrlContent extends Component { public componentDidMount() { this.mounted = true; + this.setUrl(); window.addEventListener('hashchange', this.resetUrl, false); } @@ -107,7 +107,7 @@ export class ShareUrlContent extends Component { @@ -129,42 +129,12 @@ export class ShareUrlContent extends Component { { useShortUrl: false, shortUrl: undefined, - url: undefined, }, this.setUrl ); } }; - private createShortUrl = async () => { - this.setState({ - isCreatingShortUrl: true, - shortUrlErrorMsg: undefined, - }); - - let shortUrl; - try { - shortUrl = await shortenUrl(this.getSnapshotUrl()); - } catch (fetchError) { - if (this.mounted) { - this.setState({ - isCreatingShortUrl: false, - shortUrlErrorMsg: `Unable to create short URL. Error: ${fetchError.message}`, - }); - } - throw fetchError; - } - - if (!this.mounted) { - return; - } - - this.setState({ - shortUrl, - isCreatingShortUrl: false, - }); - }; - private getSavedObjectUrl = () => { if (this.isNotSaved()) { return; @@ -222,33 +192,27 @@ export class ShareUrlContent extends Component { return ``; }; - private getUrl = (exportUrlAs: string, useShortUrl: boolean) => { + private setUrl = () => { let url; - if (exportUrlAs === EXPORT_URL_AS_SAVED_OBJECT) { + if (this.state.exportUrlAs === ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT) { url = this.getSavedObjectUrl(); - } else if (useShortUrl) { + } else if (this.state.useShortUrl) { url = this.state.shortUrl; } else { url = this.getSnapshotUrl(); } if (this.props.isEmbedded) { - return this.makeIframeTag(url); + url = this.makeIframeTag(url); } - return url; - }; - - private setUrl = () => { - this.setState({ - url: this.getUrl(this.state.exportUrlAs, this.state.useShortUrl), - }); + this.setState({ url }); }; private handleExportUrlAs = (optionId: string) => { this.setState( { - exportUrlAs: optionId, + exportUrlAs: optionId as ExportUrlAsType, }, this.setUrl ); @@ -256,32 +220,49 @@ export class ShareUrlContent extends Component { private handleShortUrlChange = async (evt: any) => { const isChecked = evt.target.checked; - if (this.state.shortUrl === undefined && isChecked) { - try { - await this.createShortUrl(); - } catch (fetchError) { + + if (!isChecked || this.state.shortUrl !== undefined) { + this.setState({ useShortUrl: isChecked }, this.setUrl); + return; + } + + // "Use short URL" is checked but shortUrl has not been generated yet so one needs to be created. + this.setState({ + isCreatingShortUrl: true, + shortUrlErrorMsg: undefined, + }); + + try { + const shortUrl = await shortenUrl(this.getSnapshotUrl()); + if (this.mounted) { this.setState( { + shortUrl, + isCreatingShortUrl: false, + useShortUrl: isChecked, + }, + this.setUrl + ); + } + } catch (fetchError) { + if (this.mounted) { + this.setState( + { + shortUrl: undefined, useShortUrl: false, + isCreatingShortUrl: false, + shortUrlErrorMsg: `Unable to create short URL. Error: ${fetchError.message}`, }, this.setUrl ); - return; } } - - this.setState( - { - useShortUrl: isChecked, - }, - this.setUrl - ); }; private renderExportUrlAsOptions = () => { return [ { - id: EXPORT_URL_AS_SNAPSHOT, + id: ExportUrlAsType.EXPORT_URL_AS_SNAPSHOT, label: this.renderWithIconTip( 'Snapshot', `Snapshot URLs encode the current state of the ${this.props.objectType} in the URL itself. @@ -289,7 +270,7 @@ export class ShareUrlContent extends Component { ), }, { - id: EXPORT_URL_AS_SAVED_OBJECT, + id: ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT, disabled: this.isNotSaved(), label: this.renderWithIconTip( 'Saved object', @@ -328,7 +309,7 @@ export class ShareUrlContent extends Component { }; private renderShortUrlSwitch = () => { - if (this.state.exportUrlAs === EXPORT_URL_AS_SAVED_OBJECT) { + if (this.state.exportUrlAs === ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT) { return; } From 214a4e0e318a6cf51f1685c3fdce94d1a0019271 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 22 Aug 2018 14:24:36 -0600 Subject: [PATCH 24/28] move shortUrl out of react state and into Component object --- .../public/share/components/share_url_content.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/ui/public/share/components/share_url_content.tsx b/src/ui/public/share/components/share_url_content.tsx index f96d67ca0c1bf..4eec8e54413d6 100644 --- a/src/ui/public/share/components/share_url_content.tsx +++ b/src/ui/public/share/components/share_url_content.tsx @@ -64,16 +64,18 @@ interface State { useShortUrl: boolean; isCreatingShortUrl: boolean; url?: string; - shortUrl?: string; shortUrlErrorMsg?: string; } export class ShareUrlContent extends Component { private mounted?: boolean; + private shortUrlCache?: string; constructor(props: Props) { super(props); + this.shortUrlCache = undefined; + this.state = { exportUrlAs: ExportUrlAsType.EXPORT_URL_AS_SNAPSHOT, useShortUrl: false, @@ -125,10 +127,10 @@ export class ShareUrlContent extends Component { private resetUrl = () => { if (this.mounted) { + this.shortUrlCache = undefined; this.setState( { useShortUrl: false, - shortUrl: undefined, }, this.setUrl ); @@ -197,7 +199,7 @@ export class ShareUrlContent extends Component { if (this.state.exportUrlAs === ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT) { url = this.getSavedObjectUrl(); } else if (this.state.useShortUrl) { - url = this.state.shortUrl; + url = this.shortUrlCache; } else { url = this.getSnapshotUrl(); } @@ -221,7 +223,7 @@ export class ShareUrlContent extends Component { private handleShortUrlChange = async (evt: any) => { const isChecked = evt.target.checked; - if (!isChecked || this.state.shortUrl !== undefined) { + if (!isChecked || this.shortUrlCache !== undefined) { this.setState({ useShortUrl: isChecked }, this.setUrl); return; } @@ -235,9 +237,9 @@ export class ShareUrlContent extends Component { try { const shortUrl = await shortenUrl(this.getSnapshotUrl()); if (this.mounted) { + this.shortUrlCache = shortUrl; this.setState( { - shortUrl, isCreatingShortUrl: false, useShortUrl: isChecked, }, @@ -246,9 +248,9 @@ export class ShareUrlContent extends Component { } } catch (fetchError) { if (this.mounted) { + this.shortUrlCache = undefined; this.setState( { - shortUrl: undefined, useShortUrl: false, isCreatingShortUrl: false, shortUrlErrorMsg: `Unable to create short URL. Error: ${fetchError.message}`, From 6c825bb521391ffd5c7c62cc795fef94d7bcb967 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 22 Aug 2018 14:31:35 -0600 Subject: [PATCH 25/28] getUnhashableStates type from any[] to object[] --- src/ui/public/share/components/share_context_menu.tsx | 2 +- src/ui/public/share/components/share_url_content.tsx | 2 +- src/ui/public/share/show_share_context_menu.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/public/share/components/share_context_menu.tsx b/src/ui/public/share/components/share_context_menu.tsx index 70d248abbf25e..6846632ba63da 100644 --- a/src/ui/public/share/components/share_context_menu.tsx +++ b/src/ui/public/share/components/share_context_menu.tsx @@ -27,7 +27,7 @@ interface Props { allowEmbed: boolean; objectId?: string; objectType: string; - getUnhashableStates: () => any[]; + getUnhashableStates: () => object[]; } export class ShareContextMenu extends Component { diff --git a/src/ui/public/share/components/share_url_content.tsx b/src/ui/public/share/components/share_url_content.tsx index 4eec8e54413d6..329f7ade9f4c0 100644 --- a/src/ui/public/share/components/share_url_content.tsx +++ b/src/ui/public/share/components/share_url_content.tsx @@ -51,7 +51,7 @@ interface Props { isEmbedded?: boolean; objectId?: string; objectType: string; - getUnhashableStates: () => any[]; + getUnhashableStates: () => object[]; } enum ExportUrlAsType { diff --git a/src/ui/public/share/show_share_context_menu.tsx b/src/ui/public/share/show_share_context_menu.tsx index 91506fd5e052e..4adefcad539ea 100644 --- a/src/ui/public/share/show_share_context_menu.tsx +++ b/src/ui/public/share/show_share_context_menu.tsx @@ -41,7 +41,7 @@ const onClose = () => { interface ShowProps { anchorElement: any; allowEmbed: boolean; - getUnhashableStates: () => any[]; + getUnhashableStates: () => object[]; objectId?: string; objectType: string; } From 187e296be53f35b682987da29e3a9d157044105c Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 22 Aug 2018 14:35:52 -0600 Subject: [PATCH 26/28] add comment about type change once EUI issue is solved --- src/ui/public/share/components/share_url_content.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/public/share/components/share_url_content.tsx b/src/ui/public/share/components/share_url_content.tsx index 329f7ade9f4c0..959b1319ea310 100644 --- a/src/ui/public/share/components/share_url_content.tsx +++ b/src/ui/public/share/components/share_url_content.tsx @@ -220,6 +220,7 @@ export class ShareUrlContent extends Component { ); }; + // TODO: switch evt type to ChangeEvent once https://github.com/elastic/eui/issues/1134 is resolved private handleShortUrlChange = async (evt: any) => { const isChecked = evt.target.checked; From 7ef83c45c74a32638cf757645988b5be9b4c42a6 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 28 Aug 2018 07:01:44 -0600 Subject: [PATCH 27/28] add functional test for saved object URL sharing --- .../share_url_content.test.js.snap | 4 ++++ .../share/components/share_url_content.tsx | 2 ++ test/functional/apps/discover/_shared_links.js | 17 ++++++++++++++++- test/functional/page_objects/share_page.js | 7 +++++-- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/ui/public/share/components/__snapshots__/share_url_content.test.js.snap b/src/ui/public/share/components/__snapshots__/share_url_content.test.js.snap index 64623f40d508b..7898c7f0654a5 100644 --- a/src/ui/public/share/components/__snapshots__/share_url_content.test.js.snap +++ b/src/ui/public/share/components/__snapshots__/share_url_content.test.js.snap @@ -18,6 +18,7 @@ exports[`render 1`] = ` options={ Array [ Object { + "data-test-subj": "exportAsSnapshot", "id": "snapshot", "label": , }, Object { + "data-test-subj": "exportAsSavedObject", "disabled": true, "id": "savedObject", "label": , }, Object { + "data-test-subj": "exportAsSavedObject", "disabled": false, "id": "savedObject", "label": { `Snapshot URLs encode the current state of the ${this.props.objectType} in the URL itself. Edits to the saved ${this.props.objectType} won't be visible via this URL.` ), + ['data-test-subj']: 'exportAsSnapshot', }, { id: ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT, @@ -281,6 +282,7 @@ export class ShareUrlContent extends Component { this.props.objectType }.` ), + ['data-test-subj']: 'exportAsSavedObject', }, ]; }; diff --git a/test/functional/apps/discover/_shared_links.js b/test/functional/apps/discover/_shared_links.js index 7f10639d5bcf1..45ad1ac6f0d66 100644 --- a/test/functional/apps/discover/_shared_links.js +++ b/test/functional/apps/discover/_shared_links.js @@ -75,7 +75,6 @@ export default function ({ getService, getPageObjects }) { '*\',interval:auto,query:(language:lucene,query:\'\')' + ',sort:!(\'@timestamp\',desc))'; const actualUrl = await PageObjects.share.getSharedUrl(); - log.debug('actualUrl', actualUrl); // strip the timestamp out of each URL expect(actualUrl.replace(/_t=\d{13}/, '_t=TIMESTAMP')).to.be( expectedUrl.replace(/_t=\d{13}/, '_t=TIMESTAMP') @@ -90,6 +89,22 @@ export default function ({ getService, getPageObjects }) { expect(actualUrl).to.match(re); }); }); + + it('should allow for copying the saved object URL', async function () { + const expectedUrl = + baseUrl + + '/app/kibana#' + + '/discover/ab12e3c0-f231-11e6-9486-733b1ac9221a' + + '?_g=(refreshInterval%3A(pause%3A!t%2Cvalue%3A0)' + + '%2Ctime%3A(from%3A\'2015-09-19T06%3A31%3A44.000Z\'%2C' + + 'mode%3Aabsolute%2Cto%3A\'2015-09-23T18%3A31%3A44.000Z\'))'; + await PageObjects.discover.loadSavedSearch('A Saved Search'); + await PageObjects.share.clickShareTopNavButton(); + await PageObjects.share.exportAsSavedObject(); + const actualUrl = await PageObjects.share.getSharedUrl(); + // strip the timestamp out of each URL + expect(actualUrl).to.be(expectedUrl); + }); }); }); } diff --git a/test/functional/page_objects/share_page.js b/test/functional/page_objects/share_page.js index 41ccdcbb77f4d..07951ebf2cae5 100644 --- a/test/functional/page_objects/share_page.js +++ b/test/functional/page_objects/share_page.js @@ -31,12 +31,15 @@ export function SharePageProvider({ getService, getPageObjects }) { } async checkShortenUrl() { - await PageObjects.visualize.checkCheckbox('useShortUrl'); - const shareForm = await testSubjects.find('shareUrlForm'); + await PageObjects.visualize.checkCheckbox('useShortUrl'); await shareForm.waitForDeletedByClassName('euiLoadingSpinner'); } + async exportAsSavedObject() { + return await testSubjects.click('exportAsSavedObject'); + } + } return new SharePage(); From 0f5e3911af7ddb2d7aad881080ea571c56c54418 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 28 Aug 2018 07:03:05 -0600 Subject: [PATCH 28/28] remove commit --- test/functional/apps/discover/_shared_links.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/functional/apps/discover/_shared_links.js b/test/functional/apps/discover/_shared_links.js index 45ad1ac6f0d66..cac5954c4db2f 100644 --- a/test/functional/apps/discover/_shared_links.js +++ b/test/functional/apps/discover/_shared_links.js @@ -102,7 +102,6 @@ export default function ({ getService, getPageObjects }) { await PageObjects.share.clickShareTopNavButton(); await PageObjects.share.exportAsSavedObject(); const actualUrl = await PageObjects.share.getSharedUrl(); - // strip the timestamp out of each URL expect(actualUrl).to.be(expectedUrl); }); });